# Claude Code Configuration - Skillsmith

## Sub-Documentation

Detailed guides extracted via progressive disclosure. CLAUDE.md contains essentials; sub-docs contain deep dives.

| Document | Description |
|----------|-------------|
| [docker-guide.md](.claude/development/docker-guide.md) | Container rebuild scenarios, DNS failure, native modules, troubleshooting |
| [git-crypt-guide.md](.claude/development/git-crypt-guide.md) | Unlock, worktree setup, hooks-in-worktrees, Docker bind-mounts (SMI-4689/4738), pre-push (SMI-4767), rebase workaround, smudge filter fixes |
| [ci-reference.md](.claude/development/ci-reference.md) | Branch protection, change classification, Turborepo, npm overrides, release-PR `Package Validation`, vitest split rationale |
| [deployment-guide.md](.claude/development/deployment-guide.md) | Edge function deploy, CORS, website, full monitoring & alerts table |
| [branch-management.md](.claude/development/branch-management.md) | Pre-commit auto-restore prose, post-commit fallback recovery, direct-to-main SQL rule (SMI-2598) |
| [edge-function-patterns.md](.claude/development/edge-function-patterns.md) | Function-auth matrix, project refs, auto-deploy mechanics |
| [mcp-tools-guide.md](.claude/development/mcp-tools-guide.md) | Team-tool resolution chain (SMI-4312/ADR-116), CLI surface (SMI-4590) |
| [publishing-guide.md](.claude/development/publishing-guide.md) | Local-fallback deprecation (SMI-4533), publish-order rationale, version-pin rules |
| [claude-flow-guide.md](.claude/development/claude-flow-guide.md) | Agent spawning, swarm orchestration, hive mind, SPARC modes; `scripts/mcp-ruflo-launcher.sh` (guard checks, exit codes) serving the dedicated `skillsmith-ruflo-1` compose service (ADR-170) |
| [cloudinary-guide.md](.claude/development/cloudinary-guide.md) | Blog image upload workflow, URL transforms, folder conventions |
| [vscode-publishing-guide.md](.claude/development/vscode-publishing-guide.md) | VS Code Marketplace publishing, local/CI workflow, PAT rotation |
| [subagent-tool-permissions-guide.md](.claude/development/subagent-tool-permissions-guide.md) | Subagent tool access by type, foreground/background behavior, skill author checklist |
| [supabase-migration-safety.md](.claude/development/supabase-migration-safety.md) | Pre/post-apply query catalog, ACCESS EXCLUSIVE locks, rollback, pooler. Invoke via `supabase-migration-reviewer` skill |
| [ruvector-dev-tooling.md](.claude/development/ruvector-dev-tooling.md) | `skillsmith-doc-retrieval` MCP (SMI-4417) — local semantic doc search, post-commit hook, token-delta gate |
| [skill-invoke-telemetry-guide.md](.claude/development/skill-invoke-telemetry-guide.md) | Skill-invocation telemetry pipeline (SMI-5012) — wire format, consent gate, dispatcher coverage, rotation policy |
| [smoke-prod-guide.md](.claude/development/smoke-prod-guide.md) | Post-deploy smoke harness (SMI-4459) — surface manifest, failure triage, phase rollout |
| [vercel-deploy-hook.md](.claude/development/vercel-deploy-hook.md) | Vercel→GitHub `repository_dispatch` triggering `smoke-prod.yml` post-deploy |
| [e2e-staging-runbook.md](.claude/development/e2e-staging-runbook.md) | `device-login-roundtrip.yml` (SMI-4460) — secret rotation, Docker carve-out, prod-ref grep gate |
| [eval-cron-setup.md](.claude/development/eval-cron-setup.md) | Canonical-dev retrieval-eval cron (SMI-4764 W2) — launchd/systemd, heartbeat, replacement protocol |
| [edge-function-attribution-queries.md](.claude/development/edge-function-attribution-queries.md) | Canonical pooler queries for edge function attribution monitoring (SMI-4370 / Wave 4d) |
| [concurrency-patterns.md](.claude/development/concurrency-patterns.md) | Pattern-to-incident-to-canonical-fix index for the six `concurrency-auditor` patterns (SMI-4895/4896/4861/4887/6428) |
| [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md) | Canonical Guards & Opt-Outs registry (guard → trigger → marker → disable var → scope) — SMI-5418 DoD #5 |
| [upstash-redis-operations.md](docs/internal/runbooks/upstash-redis-operations.md) | Upstash Redis architecture (4 consumers), setup, credential rotation, health-check monitoring |

**Implementation plan template**: [.claude/templates/implementation-plan.md](.claude/templates/implementation-plan.md) — use this structure for all plans in `docs/internal/implementation/`.

---

## Docker-First Development

**All code execution MUST happen in Docker** for any path that loads native modules (`better-sqlite3`, `onnxruntime-node`, etc.). Native modules require glibc — see [ADR-002](docs/internal/adr/002-docker-glibc-requirement.md), whose scope is narrowly the choice of `node:22-slim` over Alpine, *not* a project-wide mandate that every CI job run in Docker.

**CI carve-out (SMI-4647)**: two pure-JS jobs run on the host runner — `quality-checks` (bundles lint + typecheck + audit:standards since SMI-4908) and `code-review`. New jobs default to Docker; opt-in requires `# audit:carveout-pure-js` marker. Full rationale: [ci-reference.md § Docker-First CI Carve-out](.claude/development/ci-reference.md#docker-first-ci-carve-out-smi-4647).

```bash
docker compose --profile dev up -d                    # Start container (REQUIRED first)
docker exec skillsmith-dev-1 npm run build             # Build
docker exec skillsmith-dev-1 npm test                  # Test
docker exec skillsmith-dev-1 npm run lint              # Lint
docker exec skillsmith-dev-1 npm run typecheck         # Typecheck
docker exec skillsmith-dev-1 npm run audit:standards   # Standards audit
docker exec skillsmith-dev-1 npm run preflight         # All checks before push
```

**From a worktree (SMI-5559)**: the `docker exec skillsmith-dev-1 <cmd>` form above is for the **main checkout only** — its container is long-lived, so that exact command silently "succeeds" from any worktree even if the worktree's own container never started. From `.worktrees/<name>/`, use `./scripts/worktree-docker.sh exec -- <cmd>` instead — it resolves the container matching your actual cwd and errors loudly (not silently) if it isn't running, e.g. `./scripts/worktree-docker.sh exec -- npm run preflight`. **Starting the container is the same trap (SMI-4298, retro 2026-08-25)**: a bare `docker compose --profile dev up -d` in a worktree publishes an EXTRA host port 3001 alongside the worktree's own bucketed pair (Compose concatenates `ports:` across `-f` files rather than replacing them), colliding with the main checkout's `skillsmith-dev-1` if it's already running. Use `./scripts/worktree-docker.sh start` instead — it exports `DEV_PORT` from the worktree's own override before `up`, so no extra port is published and no manual `.env` `DEV_PORT=` hack is needed.

**`git push` from a worktree (SMI-5570/SMI-5074)**: pre-push routes through **this worktree's own dedicated container**, not `skillsmith-dev-1` — an earlier default (SMI-5548) routed through main's shared container reached via the worktree's nested path, which silently tested main's own dependency state instead of the worktree branch's own (a Docker `mount(2)` behavior, not macOS-specific). Start this worktree's container before pushing (`./scripts/worktree-docker.sh start`, not a bare `docker compose --profile dev up -d` — see the SMI-4298 note above); pre-push hard-fails with the exact remediation command if it isn't running, unless the push is docs-only or you opt out with `SKILLSMITH_PRE_PUSH_HOST=1` (one push) or `SKILLSMITH_WORKTREE_PREPUSH_HARDFAIL_DISABLE=1` (registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md)).

**After pulling**: post-merge never installs (SMI-6614, ADR-158). If it prints one info line, the lockfile change was cosmetic and there's nothing to do. If it prints a refresh sequence, a real dependency changed: run that sequence yourself from the main checkout, in order, and don't substitute a bare `npm install` on the host or in a container. **Full rebuild** (native modules, major upgrades): [docker-guide.md](.claude/development/docker-guide.md#full-rebuild-thorough). **Stop**: `docker compose --profile dev down`. **Logs**: `docker logs skillsmith-dev-1`. **Submodule**: `git submodule update --init` before `docker compose up` if internal docs needed inside container.

**Auto-recovery (SMI-5245)**: the `dev` service sets `restart: unless-stopped`, so the container comes back on its own after a Docker Desktop / machine restart — this keeps the `skillsmith-doc-retrieval` + `skillsmith` MCP servers (launched via `docker exec` in `.mcp.json`) from silently dropping with a `-32000` reconnect error. An explicit `docker compose --profile dev down` or `docker stop` is still honored (stays down). If you ever see the container *restart-looping* after a reboot, it hit a degraded environment (wiped volume / ABI mismatch) — run the **Container won't start** Troubleshooting recipe. **Both MCP servers require the container to be running** — there is no fallback path; if the container is down, `/mcp` will report `Failed to reconnect` and all MCP tools will be unavailable until `docker compose --profile dev up -d` is run.

**After a fresh clone**: run `docker exec -w /app skillsmith-dev-1 sh -c 'sh scripts/lib/node-modules-mount-gate.sh && npm install && npm run build'` before the `skillsmith` MCP server can connect (after a *volume wipe* use the **Container won't start** Troubleshooting row and the ordered refresh sequence instead — a wiped root volume re-seeds from the image rather than coming back empty, SMI-6674) — exit non-zero with no npm/build output means at least one of the container's node_modules paths (root, or a `packages/*/node_modules`) isn't currently mounted with a volume-shaped root (SMI-6516/SMI-6520/SMI-6614), so the container needs recreating first (`docker compose --profile dev up -d --force-recreate dev`, from the main checkout), not another install attempt. Both MCP servers now have their own preflight launcher: `skillsmith` via `scripts/mcp-skillsmith-launcher.sh` (SMI-5049) and `skillsmith-doc-retrieval` via `scripts/mcp-doc-retrieval-launcher.sh` (SMI-5718) — each prints actionable stderr in the `/mcp` panel's per-server log when `node_modules/` or `dist/` is missing, or when a runtime dependency is corrupt/unresolvable (e.g. an empty nested `node_modules` dir shadowing the hoisted copy after an interrupted npm install, SMI-5451/SMI-5452) — surfaced when you expand the failing entry. `skillsmith-doc-retrieval` additionally checks that the `skillsmith-dev-1` container itself is running before it tries to `docker exec` into it (it is Docker-only — native module `better-sqlite3`).

---

## CI Health Requirements

Zero ESLint warnings/errors. TypeScript strict (no unjustified `any`). All files Prettier-formatted. 100% test pass. No high-severity vulns. **<500 lines/file** — the **blocking** gate is pre-commit, not CI: `lint-staged.config.js` routes every staged `*.{ts,sh}` through `scripts/check-file-length.mjs`, which hard-fails (exit 1) on any file over the limit outside `scripts/check-file-length.ignore`. `audit:standards` Check 3 is the CI-side *observer* of the same policy and only `warn()`s, and only over `packages/` + `apps/` — so a `.mjs`, a `scripts/` file, or anything committed with `--no-verify` can land over-limit with CI green (threshold + test-file exemption are shared via `scripts/file-length-policy.mjs`; the scope/severity divergence is deliberate and tracked in SMI-5994). Split into `foo.helpers.ts`/`foo.types.ts` if approaching, or — for a command whose `withTelemetry`-wrapped action handlers push it over — a `foo.action.ts` sibling holding the impls + wrapped exports while `foo.ts` keeps the commander factory, SMI-5127+). >80% coverage. Source-file changes must include related test updates — for a new SQL migration extending an existing mechanism, check whether the thing it extends already has a dedicated test file (e.g. a sibling claim/heartbeat/GC function set) and add the matching coverage; a SQL-correctness-focused adversarial review round will check the logic without ever flagging that it's untested (SMI-6015 retro).

**When CI fails**: don't merge. Run `docker exec skillsmith-dev-1 npm run preflight` locally. Linear issue if non-trivial.

**Concurrency prevention (SMI-4891/4892/4902)**: shared-state / race-condition gating via the `concurrency-auditor` skill (Mode A plan-audit, Mode B diff-audit) and the `concurrency-audit-pr.yml` PR workflow, which comments on findings but does not block a merge (its gate is still non-blocking; the flip to blocking is tracked in SMI-5591). Lazy-helper convention for browser globals (`window.__SUPABASE_CLIENT__` reads only via `getSupabaseClient()`) enforced by the `no-raw-window-global` ESLint rule. Pattern reference + canonical-fix PRs: [concurrency-patterns.md](.claude/development/concurrency-patterns.md). Opt-out marker: `[concurrency-audit-ack]` in PR body (boolean shape, reason as prose paragraph).

**A fix for a race condition needs its own confirmation review round, not just the round that found it (SMI-6015 retro).** The fix for one race can introduce a second, more subtle one — a round-2 adversarial-review fix for a GC-vs-active-claim abandonment race introduced a round-3 finding (a Postgres READ COMMITTED snapshot-staleness gap) inside that same fix, which a single additional confirmation pass caught. After any race/lock/CAS-shaped fix, run at least one more adversarial-review round scoped narrowly to the fix itself before trusting it — don't stop at the first clean pass.

**A regression test you have not run against the unfixed code is unverified. Revert the fix, confirm the test FAILS, restore (SMI-6598).** **And confirm it fails for the stated reason.** A mutation that fails in unrelated compilation, module loading, setup, mocking or test infrastructure turns the suite red while proving nothing, and in any summary that red is indistinguishable from a real one. **Declare the expected failure before you mutate, then confirm it arrived that way** — a reviewer who mutates first and reads whatever red appears will find a reason it counts. For a runtime mutation, confirm from the full failure and its stack that execution reached the intended observation and failed there because of the mutation; for a type-level mutation, confirm the expected diagnostic lands at the intended site, where execution never reaches a runtime observation and need not. Never classify from the summary string: a correct assertion failure may read generically, and an unrelated one may name the mutated identifier by coincidence. An experiment you cannot repair leaves the assertion unverified, not passed (SMI-6932). A green suite proves the tests pass; it does not prove they constrain anything. Three tests written to pin a fix in SMI-6598 passed identically with that fix removed — each used a value that was syntactically valid but changed no outcome, so it exercised the code without testing it. That is the same invisible-success class as the defects the tests existed to catch, and the only thing that distinguishes a real test from a decorative one is watching it fail. This generalises the race-fix rule above: **a fix for any silent-success path is itself a silent-success risk**, because the new code's failure mode is the same silence it was written to remove. **Watching it fail is necessary, not sufficient (SMI-6497, 2026-09-15).** The author picks the mutation, so the author's blind spot picks it too. SMI-6497 needed four encodings to pin four command-line flags -- `"$*"`, then `|`-delimiters, then whole-line matching, then per-record validation -- and every one of them passed its author's own red-test before a cross-family reviewer named an attack class the author had not imagined. So: red-test your own work, then have someone who did not write it name the mutation you would not have chosen. When a reviewer finds a defect *inside* your fix, expect the next round to find one inside that fix -- three consecutive rounds did on SMI-6497. Don't assume they converge: each round's own fix -- prose included -- gives the next round new material, so set the stopping rule before a round runs, not after seeing what it found (SMI-6651). **Assert the property that makes the code correct, not the value a correct implementation happens to emit (SMI-6732, 2026-09-19).** Those agree until someone changes the implementation, which is the moment a regression test exists for. A cross-family mutation hunt replaced `randomBytes(16).toString('hex')` with a constant and 6,454 tests stayed green, because the assertion matched the 32-hex *shape* rather than requiring two park names to *differ* -- and unpredictability is the whole reason that rename makes a recursive delete un-raceable. Eleven same-family rounds, two of them specifically about races, had not checked it. The tell: a test that would still pass if the code computed its value some other way is pinning an output format, not a behaviour. **And the converse case has its own trap: a passing test can be pinning the defect, so your fix breaking it looks like a regression (SMI-6733 and SMI-6744, independently, 2026-09-28).** A test that reaches the behaviour under test *through* the buggy path passes only while the bug is present — fix the bug and it goes red, which invites you to revert the fix or weaken the assertion. Both instances that day had the same shape: the test's *trigger* depended on a write that should never have happened, while its *assertion* was correct and worth keeping. So when a fix turns a green test red, ask what the test does to reach its assertion before you touch either: if the trigger rides on the defect, re-point the trigger and leave the assertion alone — then prove the new trigger still drives the assertion by disabling it and watching the test fail, because a re-pointed trigger that silently never fires passes forever while asserting nothing.

**Measure, don't reason. A checkable claim about behaviour gets executed before it enters a plan, an issue, a commit message or a PR body (SMI-6505 retro, 2026-09-10).** Plausibility is not evidence, and a confident wrong claim costs more than an open question. SMI-6505 holds the case tables and rejected drafts from the wave that produced this rule.

Three habits carry most of the value:

- **Execute predicate/regex/precedence claims against a case table, in the actual runtime**, before writing the mechanism down — `node -e` for JavaScript, but a shell, SQL, PCRE or RE2 predicate must be tested with its actual engine, since engines can differ on the very edge cases a case table exists to probe. Include the negative cases: a draft that handles the reported input correctly can still fail one nobody has written down yet. That is how each rejected attempt in SMI-6505 got caught.
- **Prefer a direct test of the property over an inference from metadata.** A mount flag is an inference; a successful write at the exact target path demonstrates that that operation succeeds in that context — and no more, since a failure can also mean permissions, path resolution or quota rather than a read-only mount. Container metadata reporting a path writable while a real write to it returned `EROFS` is the case that motivated this — so check the write call's own result and errno, not the mount table. **And a direct test has three blind spots of its own.** It can be satisfied by a **fallback**: a probe that exercises the feature passes when a silent degraded path serves it, so assert the mechanism, not the outcome. It is true only **at the instant it ran** — a state a probe confirmed can change before the work using it finishes, so a clean probe before a long operation is not evidence about that operation (SMI-6516 measured a container healthy, then degraded **within** 42 minutes, with no restart in between -- the window is an upper bound on the interval, not a measured point). And it can be **answering a different question than the one asked**: the same-shaped command returns a different answer about the same tree at the same moment depending on which directory it ran from, what its own matcher syntax actually supports, or which of several inconsistent conventions its output mixes — and a probe that would return a plausible-looking value whichever of the two states it exists to distinguish is true is not a test of either one. Check the instrument hardest when the number looks cleanest, because a wrong instrument returns a plausible answer, never a visibly broken one (SMI-6488 holds the measured instances; SMI-6720 holds the cleanest single one, a grep that returned 0 for the very design it was used to veto). When the **first two** blind spots cannot be ruled out, the reliable postures are continuous observation, after-the-fact attribution at the point of failure, or making the operation itself fail loudly rather than fall through; [ADR-165](docs/internal/adr/165-detecting-unpreventable-environment-degradation.md) chooses among them for the environment-degradation case. None of those three reaches the **third** blind spot, and "fail loudly" is the wrong instinct for it — a wrong instrument does not fail; it answers. Its own remedy is below.
- **Never conclude from truncated output.** `| head`/`| tail` can hide the decisive line, and it changes which exit status you see: a POSIX pipeline's status is its **last** command's, so `git push … | tail -3` can report 0 when the push failed. **Capture the producer's output and status separately** — run it to completion, save the status immediately, truncate only when displaying. `set -o pipefail` is not a general substitute: it was standardised only in POSIX Issue 8 (2024) so support varies by shell, and with an early-closing consumer like `head` it can report failure for a producer that was fine.

- **Measure reachability separately from mechanism.** Verifying every frame of a call chain establishes that a defect's *mechanism* is real and says nothing about whether anything calls the outermost frame — so a severity derived from a correctly-verified mechanism can still be wrong by the whole distance between "this code would destroy every record" and "no user can reach it." Before writing or accepting a severity, check that the entry point has a production caller, and check it like the negative claim it is, with a known-positive control. This is not the same failure as the ones above: every individual measurement can be correct and the conclusion still unfounded, because the gap is in the question set rather than the rigour. SMI-6733 holds the case — an Urgent data-loss issue whose description had been re-derived line by line, whose destructive path sat inside a function that four separate repo locations already recorded as having no caller, one of them a docblock four lines above the signature being read.

**A wrong disproof is worse than the original claim**, because it closes a line of inquiry that was correct: hold retractions to a higher evidentiary bar than the claims they retract. That obligation binds the **reader** too, and the bias has a direction. Truncation can only *manufacture absence* — `head` hides an entry, it never invents one — so a negative claim ("X is not present", "there is no Y") is simultaneously the least likely to be re-checked and the most likely to be corrupt. Re-derive a load-bearing negative from the artifact before writing it into a durable record, whoever stated it, and note that flagging a claim "unverified" does not transfer that obligation to the next reader (SMI-6488 instance 12, 2026-09-18). And when something genuinely cannot be checked, say so and name what was not investigated — an open gap is correct, a plausible story filling it is not. The third blind spot has one remedy, and it is not denominator reporting: **run the probe against a known-positive and a known-negative before trusting its verdict.** An instrument that returns the same value for both is measuring nothing, and no amount of scope reporting reveals that — a perfectly-scoped check against the wrong subject is still wrong, and will happily report the population it examined. Denominator reporting is the remedy for the narrower *scope* problem (a check that ran over the wrong or empty file set), which is a different failure with a different fix; SMI-6488 separates the two deliberately and records that scope reporting does not detect a wrong subject. Both rules are worth having. Do not let one stand in for the other. This rule itself has no enforcement surface and binds by recall, like the rules covered by § Important Instruction Reminders' own enforcement-status note.

**Durable rules instruct; they don't describe.** In an always-loaded file, write the instruction and leave the evidence in the tracking issue. An instruction stays correct as the system changes around it; a factual claim about the system — a version, a line number, a commit SHA, an exit code, a count, a file path — begins rotting the moment it is written, and nothing in the file tells a later reader which claims have gone stale. So when a rule seems to need evidence before it will be believed, cite the issue holding that evidence rather than restating it here. The same applies to an issue's own description: correct it in place when it goes stale, because a correction added below a wrong description leaves two surfaces disagreeing, and the wrong one is read first.

**Post-deploy smoke (SMI-4459)**: `smoke-prod.yml` runs `scripts/smoke-prod.sh` against prod after each merge. Failure → Linear + email. Skip: `[skip-smoke]` in PR body. [smoke-prod-guide.md](.claude/development/smoke-prod-guide.md).

**Build**: Turborepo (`npm run build`); legacy fallback `npm run build:legacy` ([ADR-106](docs/internal/adr/106-turborepo-build-orchestration.md)). **Change tiers**: `docs` ~30s, `config` validation, `code` ~11 min full, `deps` rebuild+audit. **Branch protection**: the declared required-check set is `.github/branch-protection.json` — **read it; never trust a count written anywhere else, including here.** `--fix` applies that FILE to GitHub via a whole-object PUT, so a stale or incomplete file *removes* protection rather than adding it; never run it to "repair" protection without first diffing the file against live. `scripts/validate-branch-protection.sh --dry-run` compares a strict subset — `required_status_checks.contexts` and `.strict`, plus `enforce_admins`, `allow_force_pushes`, `allow_deletions` — and nothing else, so it is not a full arbiter; read the script, not a tally of it. Docs-only PRs require a smaller subset. What the file does and does not guarantee: [ci-reference.md](.claude/development/ci-reference.md); gaps tracked in SMI-6968. **npm overrides, release-PR `Package Validation` (which passes — there is no carve-out and no `--admin` path, SMI-4920), vitest split rationale**: [ci-reference.md](.claude/development/ci-reference.md).

**Dependabot lockfile stability (SMI-5272)**: the root `jose: "5.10.0"` devDependency was added as an anchor for an otherwise optional-peer-only `jose@5.10.0` reachable via `ruflo → @claude-flow/cli → fastmcp`, pinned as a regular root edge so Dependabot's lockfile regen couldn't drop it. **SMI-6744 Wave 4 removes the root `ruflo` devDependency (Checkpoint 4 row 5)**, so the `fastmcp` edge is gone from the lockfile; the host tree follows at the post-merge refresh, after which the root `jose` copy is an ordinary hoisted dependency of `@vercel/oidc` (`^5.9.6`). The anchor is kept anyway — removing it is a separate lockfile change from Wave 4's — and if Dependabot or `npm ci` ever fails with `Missing: jose@5.10.0 from lock file`, the fix is still to restore it. Root cause + manual-consolidated-bump fallback: [ci-reference.md § Dependabot lockfile regen](.claude/development/ci-reference.md).

---

## Project Overview

Skillsmith is a registry for sharing, scanning, and tracking agent skills (discovery, installation, updates, and management), delivered as an MCP server, CLI, and VS Code extension. Packages: `@skillsmith/core` (DB, repositories, services), `@skillsmith/mcp-server` (MCP tools), `@skillsmith/cli`. License: [Elastic License 2.0](https://www.elastic.co/licensing/elastic-license) — all packages, source-available ([ADR-119](docs/internal/adr/119-unified-elastic-license.md)). Quick Start: [README](README.md).

| Tier | Price | API Calls/Month |
|------|-------|-----------------|
| Community | Free | 100 |
| Individual | $9.99/mo | 1,000 |
| Team | $25/user/mo | 10,000 |
| Enterprise | Custom (unpublished, "Contact Sales") | Unlimited |

---

## Git-Crypt (Narrowed Scope)

**Only `supabase/functions/` and `supabase/migrations/` are encrypted via git-crypt.** Strategic IP (`.claude/skills/`, `.claude/plans/`, `.claude/hive-mind/`) lives in the private `smith-horn/skillsmith-strategy` submodule (PAT-based access, parallel to `docs/internal/`). Internal docs are in a private submodule at `docs/internal/`.

```bash
git-crypt status | head -10                           # Check encryption scope
varlock run -- sh -c 'git-crypt unlock "${GIT_CRYPT_KEY_PATH/#\~/$HOME}"'  # Unlock
git submodule update --init                           # Init internal docs (authorized users only)
```

**Not encrypted** (always readable): `.claude/settings.json`, `supabase/config.toml`, `.claude/development/`, `.claude/templates/`.

**Worktrees**: Unlock main repo first, then `./scripts/create-worktree.sh`. Remove with `./scripts/remove-worktree.sh --prune`. Hooks-in-worktrees, Docker bind-mounts (SMI-4689/4738), pre-push (SMI-4767), host native bindings (SMI-4549), and SMI-4698 native-rebuild caveat: see [git-crypt-guide.md § Worktree Setup](.claude/development/git-crypt-guide.md#worktree-setup).

**Strategy submodule init**: each of `.claude/skills`, `.claude/plans`, `.claude/hive-mind` is a submodule of `smith-horn/skillsmith-strategy` pinned to its own branch (`branch = skills/plans/hive-mind` in `.gitmodules`). Plain `git submodule update --init` materializes the right content at each mount-point — no sparse-checkout machinery needed (SMI-4829 cutover, shape b′; the prior shape b sparse-checkout approach was abandoned because cone mode cannot strip upstream path prefixes). External contributors without strategy-submodule access see empty mount-points but no hard error (gate #3, SMI-4829).

**Rebasing**: `./scripts/rebase-worktree.sh <worktree-path> [target-branch]` handles git-crypt filter management, submodule cross-fetching, and branch verification. Handles all submodules in `.gitmodules` (post-SMI-4829: `docs/internal` + 3 strategy mounts). Use `--dry-run` to preview. Pass `--allow-submodule-ahead=<path>` for per-submodule advance permission (or unscoped `--allow-submodule-ahead` for global). Manual fallback: [git-crypt-guide.md](.claude/development/git-crypt-guide.md#rebasing-with-git-crypt).

---

## Branch Management (SMI-2536)

Git-crypt smudge filters can silently switch branches during stash/pop (including lint-staged). Defense-in-depth: run `git branch --show-current` before first edit, and after every `commit` / `stash pop` / `checkout` (stash pop is the most common trigger; checkout can report false success). **Pre-commit auto-restore** (SMI-2747) and **post-commit fallback recovery**: [branch-management.md](.claude/development/branch-management.md).

**Syncing main**: `./scripts/sync-main.sh` (quiet, ~75 tokens vs ~5k git-crypt noise). Then `git checkout -b <branch-name>`.

**Branch naming**: `<type>/<slug>` — `feature/`, `fix/`, `chore/`, `docs/` (matches the commit-type prefixes). Do not prefix branches with a personal username (e.g. `ryansmith108/...`); a growing number of `ryansmith108/`-prefixed branches has drifted from this convention and should be renamed/avoided going forward. **Never embed any person's name anywhere in a branch name** — not as a prefix, and not in the descriptive slug either (e.g. a plan triaging an external reporter's bug report should branch as `docs/cursor-integration-readiness-plan`, not `docs/<their-handle>-cursor-integration-plan`). Describe the *work*, not *who* is attached to it. This persists on GitHub even after the branch is deleted — a merged PR's head-ref name is immutable history, so catch this before the first push, not after (SMI-5893 retro, 2026-07-30: a plan-triage branch briefly carried the reporting tester's GitHub handle; caught and renamed before it reached the remote, but a same-shaped mistake already landed in that PR's commit body and is now permanent). Linear's own auto-suggested `gitBranchName` field prefixes branches with the assignee's username (e.g. `ryansmith108/smi-5893-...`) — don't copy that suggestion verbatim; follow the `<type>/<slug>` convention above instead.

**Risk-first wave ordering (SMI-2596)**: Waves with database migrations or production behavior changes execute first, regardless of implementation readiness. If deviating from risk order, document the rationale explicitly in the wave plan.

**Wave branch stacking (SMI-2597)**: When multiple waves modify overlapping files, branch sequentially (Wave N+1 from Wave N's branch) instead of all from main. This prevents merge conflicts from squash-merges. Tradeoff: earlier waves must merge before later waves can start CI.

**Direct-to-main SQL fixes (SMI-2598)**: see [branch-management.md § Direct-to-Main Commits](.claude/development/branch-management.md#direct-to-main-commits-smi-2598).

---

## Linear Hygiene (per-commit + per-PR)

**Keep Linear in lock-step with the code. Every commit and every merge updates Linear before moving on — no batching, no end-of-session catch-up.**

**After EVERY commit** (immediately, before the next task):

1. **Comment** on the relevant `SMI-xxx` issue with the commit SHA + a one-line summary of what changed.
2. **Advance status** if the commit completes the work (`In Progress` → `In Review`/`Done`). A commit that only partially advances the issue stays `In Progress` with a progress comment.
3. If **no issue exists** for the work, create one under the correct project *before* committing (never commit orphaned work). Project assignment is mandatory (see [Linear hygiene guide](docs/internal/process/linear-hygiene-guide.md)).

**After EVERY PR merges**:

1. Move the issue to `Done` with the squash-merge SHA in a closing comment.
2. Post a **project update** on the Linear project — with every PR, not just when a wave/PR-cluster lands or a blocker changes state. Content: reuse the `pr-description` skill's Business Summary verbatim — keep stakeholders current without them having to read the issue feed.

**Tooling**: MCP Linear tools when connected; fallback `varlock run -- node scripts/linear-api.mjs` (never `npm run linear:done` — broken). Team: **Smith Horn Group**. Always set `project` + a detailed description + labels on issue creation.

**This one rule is already machine-checked at three layers** — don't rely on remembering it, but do know all three are still *warn-only*: (1) `scripts/linear-issue-creation-guard.mjs`, a `PreToolUse` hook on `mcp__linear__save_issue` **create** calls (SMI-5846); (2) `scripts/linear-api.mjs create-issue`'s own gate on the CLI fallback path (SMI-5853, bypass `--force`); (3) `.github/workflows/lint-linear-issues.yml`, a daily CI backstop catching whatever slipped past both (SMI-5841). All three validate against the same contract in `scripts/lib/linear-issue-validation.mjs`, and **all three ship shadow-default (warn, never block) with a shadow-end review target of `2026-08-09` that has already lapsed** — so today a non-compliant issue is *reported*, not *prevented*. Disable vars for each are registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). Everything else in this section (comment-per-commit, status advance, project update per PR) has **no** enforcement surface at all — see the § Important Instruction Reminders note below. Full conventions: [linear-hygiene-guide.md](docs/internal/process/linear-hygiene-guide.md).

---

## Varlock Security

**All secrets via Varlock. Never expose API keys in terminal output.** Commit `.env.schema` (defines `@sensitive`) and `.env.example` (placeholders); **never** `.env`. Run with secrets: `varlock run -- npm test`. Validate: `varlock load` (default pretty format — masked) or `varlock load --quiet` (validation only). **Never** `varlock load --format json` / `json-full` / `json-full-compact` / `env` — those emit **unmasked** values. This ban and the file-read ban below are **enforced, not advisory**: `scripts/env-read-guard.mjs` runs as a `PreToolUse` Bash hook and *denies* the call (it unwraps `sudo` / `docker exec` / `docker compose` / `varlock run --` wrappers first, so neither a wrapper nor argument reordering evades it). See the `[env-read-guard]` Troubleshooting row for the false-positive escape hatch. **Never** read the contents of `.env`, `.env.registry`, or any `.worktrees/**/.env` via *any* command — `cat`, `grep`, `head`, `tail`, `sed`, `awk`, `less`, `strings`, `xxd`, an inline `python3 -c` / `node -e`, or a `docker exec skillsmith-dev-1 …` wrapper around any of them (the repo root is bind-mounted at `/app`, so `/app/.env` is the same file). **Never** `echo $SECRET`. The one sanctioned exception is an output-free presence check — `grep -qE '^KEY=' .env` (exit code only, no content). `.env.schema` and `.env.example` are always safe to read. Never ask users to paste secrets in chat. See [AI Agent Secret Handling](docs/internal/architecture/standards-security.md#411-ai-agent-secret-handling-smi-1956).

**Supabase pooler access**: `SUPABASE_POOLER_URL` has a literal `[YOUR-PASSWORD]` placeholder. Two canonical helpers, both via `varlock run --` (host tool — not inside the container): `./scripts/pooler-psql.sh` (transaction pooler, port 6543) for ad-hoc queries, single-statement DDL, short writes — bypasses PostgREST's 8s `statement_timeout`. `./scripts/pooler-psql-session.sh` (session pooler, port 5432) for long-running maintenance — `VACUUM`, `REINDEX CONCURRENTLY`, stored procedures with `COMMIT` between batches, anything where the transaction pooler returns `ECHECKOUTTIMEOUT` (SMI-4968 retro / SMI-4999). Requires Docker container running. Full rationale: script headers.

---

## Test File Locations (SMI-1780)

Vitest only runs tests matching these patterns. Tests elsewhere are **silently ignored**.

| Pattern | Example |
|---------|---------|
| `packages/*/src/**/*.test.ts` | `packages/core/src/foo.test.ts` |
| `packages/*/src/**/*.spec.ts` | `packages/mcp-server/src/bar.spec.ts` |
| `packages/*/tests/**/*.test.ts` | `packages/core/tests/unit/foo.test.ts` |
| `packages/*/tests/**/*.spec.ts` | `packages/enterprise/tests/integration/bar.spec.ts` |
| `tests/**/*.test.ts` | `tests/unit/utils.test.ts` |
| `supabase/functions/**/*.test.ts` | `supabase/functions/indexer/index.test.ts` |
| `scripts/tests/**/*.test.ts` | `scripts/tests/validate-skills.test.ts` |

**Common mistakes**: `scripts/__tests__/` (use `scripts/tests/`), `packages/core/test/` (use `tests/` plural), `src/foo.test.ts` (must be inside a package). Reference: `vitest.config.ts`. Split rationale (SMI-3502/4557): [ci-reference.md § Vitest Split Rationale](.claude/development/ci-reference.md#vitest-split-rationale).

---

## Skillsmith MCP Tools

| Tool | Description |
|------|-------------|
| `search` | Search skills (query, category, trust_tier, min_score, limit) |
| `get_skill` | Get skill details by `author/name` ID |
| `install_skill` | Install skill to `~/.claude/skills` |
| `uninstall_skill` | Remove installed skill |
| `skill_recommend` | Contextual skill recommendations |
| `skill_validate` | Validate skill structure |
| `skill_compare` | Compare two skills side-by-side |
| `skill_suggest` | Suggest skills based on current project context (counts against monthly quota) |
| `skill_outdated` | Check installed skills for staleness and dependency status |
| `index_local` | Index skills from a local directory |
| `skill_publish` | Prepare a local skill for publishing |
| `skill_rescan` | Re-scan an installed skill's content |
| `skill_recover_source` | Recover the canonical GitHub source of locally-installed skills (read-only) |
| `inventory_push` | Push this machine's installed-skill inventory to your Skillsmith account for the web dashboard |
| `skill_updates` | Check registry for newer skill versions (Individual+) |
| `skill_diff` | Diff two installed skill versions side-by-side (Individual+) |
| `skill_pack_audit` | Audit all skills in a directory (Individual+) |
| `skill_audit` | Audit skill for security advisories (Team+) |
| `skill_inventory_audit` | Audit every installed AI coding client's skill inventory (plus Claude Code's own commands/agents/CLAUDE.md rules) for local namespace collisions; returns rename + edit suggestions (Community; SMI-4590; multi-client SMI-6077) |
| `apply_namespace_rename` | Apply a rename suggestion from a local namespace-collision audit (`apply` / `custom` / `skip` / `revert`) (Community; SMI-4590; `revert` SMI-5671) |
| `apply_recommended_edit` | Apply a recommended prose edit; gated on `APPLY_TEMPLATE_REGISTRY` (Community; SMI-4590) |
| `undo_apply` | Session-scoped undo of the most recent apply_namespace_rename/apply_recommended_edit changeset(s), restored from the apply tool's own backup (Community; SMI-5456/SMI-5470) |
| `apply_manifest_reconcile` | Repair a corrupted or ambiguous `~/.skillsmith/manifest.json` entry through a supported path — `mark_local` / `relink` / `drop_entry` / `verify` / `revert` (Community; SMI-6343 Wave 4, ADR-144 §6 / ADR-145) |
| `team_workspace` | Manage team workspaces: create, list, get, delete (Team+) |
| `share_skill` | Add, remove, or list skills in a team workspace (Team+) |
| `publish_private` | Mark a skill private on this device, hidden from your own search results (Team+) |
| `team_analytics_dashboard` | Per-user tool usage counts, top tools, daily trend (Team+) |
| `team_usage_report` | Weekly/monthly usage summary with period comparison (Team+) |
| `private_registry_publish` | Publish a skill version to your team's private registry as a pending submission (Enterprise) |
| `private_registry_manage` | List/get/install/deprecate/undeprecate/namespace/submissions/approve/reject on your team's private registry via the `action` parameter (Enterprise) |
| `audit_export` | Export audit log events for a time range (Enterprise) |
| `audit_query` | Query audit logs with filters (Enterprise) |
| `siem_export` | Export audit events for SIEM ingestion (Enterprise) |
| `analytics_dashboard` | Recommendation accuracy, adoption curves, team aggregation (Enterprise) |
| `usage_report` | Comprehensive usage report with all metrics (Enterprise) |
| `configure_sso` | Configure SSO/SAML integration: set, test, remove, claim_domain, verify_domain (Enterprise) |
| `sso_settings` | View current SSO/SAML configuration (Enterprise) |
| `rbac_manage` | Inspect and adjust per-role permissions: `list_roles` / `get_role` / `set_role_permission` / `reset_role_permission` (Enterprise) |
| `rbac_assign_role` | Assign, revoke or list role assignments: assign, revoke, list_assignments (Enterprise) |
| `rbac_create_policy` | Bulk-set, clear or list per-role permission overrides: `create` / `list` / `delete` (Enterprise) |
| `webhook_configure` | Configure HMAC-SHA256-signed webhooks for skill lifecycle events: create, list, get, delete, test, rotate_secret (Enterprise; in preview, SMI-4135) |
| `api_key_manage` | Manage API keys for programmatic access: create, list, revoke, get (Enterprise; in preview, SMI-4135) |
| `compliance_report` | Generate SOC2, CycloneDX SBOM, or JSON compliance reports (Team+) |

**Auth**: Personal API Key (`X-API-Key: sk_live_*`, tier-based), Supabase Anon Key (30/min; one bucket per edge endpoint, shared across all anon-key callers, including every keyless MCP client), No Auth (10 trial, plus a 100/min per-IP cap; any direct HTTP request that fails to authenticate). Configure in `~/.skillsmith/config.json` or `SKILLSMITH_API_KEY` env. Shell exports don't reach MCP subprocesses. Team-tool resolution chain (SMI-4312/ADR-116), trust tiers, CLI surface: see [mcp-tools-guide.md](.claude/development/mcp-tools-guide.md).

---

## Supabase Edge Functions

**Project refs — do not confuse (SMI-4252 retro 2026-04-17)**:

| Ref | Role | Used for |
|-----|------|----------|
| `vrcnzpmndtroqxxoqkzy` | **Prod** | `.env` `SUPABASE_URL` / `SUPABASE_PROJECT_REF`; all `supabase functions deploy`; `audit_logs` / `v_indexer_health` / `/functions/v1/stats` when validating prod |
| `ovhcifugwqnzoebwfuku` | Staging | Low-cadence — data lags prod; never curl this when verifying a prod deploy |

When verifying a prod edge function via `curl`, always use `$SUPABASE_URL` (under `varlock run --`) or the literal `https://vrcnzpmndtroqxxoqkzy.supabase.co`. Function-auth matrix (21 rows) and auto-deploy mechanics: see [edge-function-patterns.md § Function Auth Matrix](.claude/development/edge-function-patterns.md#function-auth-matrix).

**Adding anonymous functions** (CI validates): add to `supabase/config.toml` with `verify_jwt = false`, to `NO_VERIFY_JWT_FUNCTIONS` in `scripts/audit-standards.mjs`, and to the deploy block below; `npm run audit:standards` Check 47 (SMI-4963) enforces deploy-script + validate-script + `config.toml` registration coherence. **Deploy commands** (`--no-verify-jwt` required — CI scans CLAUDE.md for these):

```bash
npx supabase functions deploy early-access-signup --no-verify-jwt
npx supabase functions deploy contact-submit --no-verify-jwt
npx supabase functions deploy stats --no-verify-jwt
npx supabase functions deploy skills-search --no-verify-jwt
npx supabase functions deploy registry-sync --no-verify-jwt
npx supabase functions deploy skills-get --no-verify-jwt
npx supabase functions deploy skills-recommend --no-verify-jwt
npx supabase functions deploy stripe-webhook --no-verify-jwt
npx supabase functions deploy checkout --no-verify-jwt
npx supabase functions deploy events --no-verify-jwt
npx supabase functions deploy health --no-verify-jwt
npx supabase functions deploy email-inbound --no-verify-jwt
npx supabase functions deploy generate-license --no-verify-jwt
npx supabase functions deploy license-status --no-verify-jwt
npx supabase functions deploy regenerate-license --no-verify-jwt
npx supabase functions deploy create-portal-session --no-verify-jwt
npx supabase functions deploy list-invoices --no-verify-jwt
npx supabase functions deploy skills-outreach-preferences --no-verify-jwt
npx supabase functions deploy admin-grant-subscription --no-verify-jwt
npx supabase functions deploy admin-incident-manage --no-verify-jwt
npx supabase functions deploy advance-notice-email --no-verify-jwt
npx supabase functions deploy auth-device-code --no-verify-jwt
npx supabase functions deploy auth-device-token --no-verify-jwt
npx supabase functions deploy quota-monitor --no-verify-jwt
npx supabase functions deploy webhook-heartbeat-monitor --no-verify-jwt
npx supabase functions deploy status-check --no-verify-jwt
npx supabase functions deploy status-public --no-verify-jwt
npx supabase functions deploy audit-unsubscribe --no-verify-jwt
npx supabase functions deploy team-compliance-check --no-verify-jwt
npx supabase functions deploy telemetry-consent --no-verify-jwt
npx supabase functions deploy scan-coverage-monitor --no-verify-jwt
npx supabase functions deploy private-registry-get --no-verify-jwt
npx supabase functions deploy release-cadence-heartbeat-monitor --no-verify-jwt
npx supabase functions deploy sso-domain-reverify --no-verify-jwt
npx supabase functions deploy indexer-lock-starvation-monitor --no-verify-jwt
npx supabase functions deploy registry-audit-volume-monitor --no-verify-jwt
```

**Gateway-verified auth** (SMI-4291; deploy without `--no-verify-jwt`): `webhook-dlq`, `auth-device-approve`, `auth-device-preview`, `indexer-dispatch` (SMI-4852), `team-invite-send` (SMI-4294), `sync-stripe-email` (SMI-5168), `sync-oauth-email` (SMI-5173), `inventory-upload` (SMI-5389), `purge-inventory` (SMI-5510), `audit-notify` (SMI-5541), `team-sso-manage` (SMI-6204), `sso-link-notify` (SMI-6205). **CORS, auto-deploy & monitoring**: [deployment-guide.md](.claude/development/deployment-guide.md), [edge-function-patterns.md § Auto-deploy](.claude/development/edge-function-patterns.md#auto-deploy).

---

## Supabase Agent-Skills Plugin

The official `supabase/agent-skills` marketplace ships two separate plugins. Only one is team-standard here:

```bash
claude plugin marketplace add supabase/agent-skills
claude plugin install postgres-best-practices@supabase-agent-skills
claude plugin disable supabase@supabase-agent-skills
```

The generic `supabase` plugin is disabled (not uninstalled — `claude plugin enable supabase@supabase-agent-skills` reverts it) because it collides by skill name with this repo's own `supabase` skill and advises against `SECURITY DEFINER`, which conflicts with this schema's deliberate use of `SECURITY DEFINER` helpers to fix RLS recursion (SMI-4306). `postgres-best-practices` has no such conflicts and registers the same bundled MCP server, so nothing is lost by disabling the other one.

SMI-4590's Team/Enterprise tier-gating for the session-start namespace audit covers this automatically (confirmed via code read, SMI-6228: `session-start-audit-helper.ts` calls `runInventoryAudit()`, which calls `scanLocalInventory()` then `detectCollisions()`; the collision detector matches purely on `identifier` and never branches on `client`/`origin`, so a plugin-vs-project collision surfaces through the existing tier-gated audit with no extra wiring). No manual invocation needed on Team/Enterprise; on Community/Individual, invoke `skill_inventory_audit` directly to check for this class of collision.

**The live MCP server `postgres-best-practices` registers (`mcp.supabase.com`) is disabled, repo-wide guidance (SMI-6308).** Confirmed live: Supabase's OAuth grant for this server is fixed at `.well-known/oauth-protected-resource/mcp` (full Management API read/write — `projects:write`, `database:write`, `secrets:read`, `edge_functions:write`, etc.) and cannot be narrowed via the `?features=docs` URL parameter regardless of what it claims to scope; Supabase's own tracker (`supabase/mcp#239`) confirms feature-based scope narrowing isn't implemented upstream. Since this repo only wants the plugin's static skill content (the reference files), not a live server, disable the connector per-project via `/mcp` → toggle the `supabase` (postgres-best-practices) server off — this writes `"plugin:postgres-best-practices:supabase"` into that project's `disabledMcpServers` in `~/.claude.json`, survives plugin updates, and leaves the skill content fully active. **Do this once per worktree** — the disable is per-project-path, not global, even though the plugin itself is enabled globally. `scripts/lib/mcp-command-guard.mjs` now flags any `mcp.supabase.com` entry that isn't disabled, regardless of `features=` value.

---

## Monitoring & Alerts

High-cadence: Skill Indexer (maintenance 00:00 UTC + recheck 03:00 UTC + discovery in 3 hourly phase-slots per 6h cycle at 06/07/08, 12/13/14, 18/19/20 UTC per SMI-4870, `indexer`) — beyond the existing `if: failure()`/cancelled coverage, `indexer.yml`'s `Evaluate Result Thresholds` step (SMI-6220) alerts on a *successful* run that's silently degraded (rate-limit bucket exhaustion, a discovery failure-ratio breach, secondary rate-limit pressure, or a recheck prevention-outage signal), the exact "success but degraded" gap SMI-6209 exposed; stateless, shadow-default (`SKILLSMITH_INDEXER_DEGRADED_ALERT_SHADOW`), see [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). Separately, the `Alert on upstream upsert block` step in `indexer.yml` and `indexer-backfill.yml` (SMI-7113) sends `indexer_upsert_blocked` when Supabase's Cloudflare firewall refuses a skill row it has not refused in the last 48h, whatever the failure ratio and with no shadow mode; see the `indexer_upsert_blocked` Troubleshooting row. Metadata Refresh (`skills-refresh-metadata`: Supabase pg_cron `32 2-22/4 * * *` triggers it, SMI-7114; GitHub `refresh-metadata.yml` every 4h :30 no longer triggers it but runs its independent observer, and an hourly pg_cron dead-man watches that observer; `workflow_dispatch` still triggers a manual run), Quota Monitor (hourly, Supabase pg_cron — SMI-4798; max quota-warning delay is 60 min), Edge Function Deploy (on merge to main, GHA). Public Status Page (SMI-5752): `status-check` every 5 min (`*/5 * * * *`, writes `status_checks`), `status-daily-rollup` daily 00:15 UTC (aggregates the previous UTC day into `status_daily_rollups`), `status-checks-purge` daily 00:20 UTC (drops `status_checks` rows older than 100 days — `status_daily_rollups` is the durable record). Liveness-alert: weekly retrieval-eval cron also runs a telemetry-feed stale-detection backstop that opens a deduped GitHub issue (`telemetry-liveness` label) when the local `retrieval_events` feed hasn't produced a row in >N days (shadow-default). `status-external-prober` (Wave 7, SMI-5756) runs every 10 min (`*/10 * * * *`, GitHub Actions — outside both Supabase and Vercel) probing `status-public` via both the raw prod Supabase ref and `api.skillsmith.app`; opens a deduped `status-external-outage`-labeled GitHub Issue only on a confirmed dual-URL failure, auto-closes on recovery (shadow-default). `scan-coverage-monitor` (SMI-5866) runs daily 21:45 UTC (Supabase pg_cron) measuring a birth-cohort NULL-rate of `skills.security_score` among recently-created rows (not a whole-table ratio — see the plan doc for why), catching the class of write-path silent-discard bug SMI-5849 was; alerts via the existing Resend/`audit_logs` channel, shadow-default (`SCAN_COVERAGE_ALERT_SHADOW`), self-monitors its own cron cadence. `release-cadence-heartbeat-monitor` (SMI-6052) runs daily 12:50 UTC (Supabase pg_cron) checking GitHub Actions run history for the most recent qualifying successful `release-cadence.yml` run (a `schedule` success, or a `workflow_dispatch` success corroborated by an actual same-day PR — excludes dry-runs); alerts via the existing Resend/`audit_logs` channel when none exists in the last 8 days, shadow-default (`SKILLSMITH_RELEASE_CADENCE_HEARTBEAT_SHADOW`); deliberately Supabase pg_cron rather than another GitHub Actions cron, since checking GitHub Actions' own scheduling reliability from a GitHub Actions cron would share the failure class it exists to catch. `sso-domain-reverify` (SMI-6204) runs daily 04:07 UTC (Supabase pg_cron) re-resolving each claimed team's `_skillsmith-verify.<domain>` DNS TXT record; on 3 consecutive failures it clears `team_sso_domains.verified_at` and disables the corresponding GoTrue SAML provider (clearing our own tracking column alone would leave GoTrue still routing logins to that domain), and separately sweeps domain claims still unverified after 7 days. Kill switch: `SKILLSMITH_SSO_DOMAIN_REVERIFY_DISABLE` (edge-function secret) skips the entire run, not just alerting. `indexer-lock-starvation-monitor` (SMI-6209) runs every 30 min (Supabase pg_cron, `5,35 * * * *` — chosen independently of `scan-coverage-monitor`'s own once-daily cadence, frequent enough to catch a starvation streak well inside Arm B's shortest per-run-type ceiling) evaluating two independent arms against recent `audit_logs` `indexer:run` rows: Arm A (the most recent 3 rows, any `run_type`, all `meta.status='skipped_lock'`) and Arm B (per-run-type staleness of the last row where `meta.status != 'skipped_lock'` — 12h for discovery, ~30h for maintenance/recheck); catches a runner that is alive but perpetually lock-skipped (e.g. `indexer-backfill.yml` holding the singleton `indexer_lock` past its 5-min heartbeat) or silently unscheduled by GitHub, a gap the SMI-6220 threshold-eval step and the SMI-6224 liveness check can't see; alerts via the existing Resend/`audit_logs` channel, shadow-default (`SKILLSMITH_INDEXER_LOCK_STARVATION_ALERT_SHADOW`), self-monitors its own cron cadence. `prod-deploy-cancel-monitor` (SMI-6208) runs every 30 min (`*/30 * * * *`, GitHub Actions) detecting a cancelled `Approve Production Deploy` job in `website-deploy-staging.yml` — the job that actually sits `waiting` for required-reviewer approval under the two-job split confirmed necessary by a live test on 2026-08-27, the silent-cancellation class SMI-6208 was filed for — and filing a deduped GitHub Issue (`prod-deploy-approval-cancelled` label) once past its shadow-default; shadow-default (`SKILLSMITH_PROD_DEPLOY_CANCEL_MONITOR_SHADOW`). Unlike most other monitors in this table, this one does **not** write to `audit_logs` (it's pure `gh` CLI with no Supabase credentials in its GitHub Actions context by design, kept lightweight and dependency-free) — a deliberate, stated exception to the section's closing "All jobs log to `audit_logs` table" line, not an oversight. `pointer-autorepair` (SMI-6260) runs on every `push` to `main` touching `docs/internal`, `.claude/skills`, `.claude/plans`, `.claude/hive-mind`, or `.gitmodules` (GitHub Actions, `.github/workflows/submodule-pointer-check.yml`) — the post-merge half of the docs/internal gitlink ancestry gate, closing the TOCTOU gap the PR-time `pointer-check` required status check alone cannot (bypassable via `--admin`, a direct-to-main push, or the residual PR-CI-to-merge window). On a detected staleness (R3: the registered pointer is behind `docs/internal`'s own upstream tip) it auto-repairs by fast-forwarding and pushing a `[auto-repair SMI-6260]`-marked commit, gated on `SKILLSMITH_MAIN_PUSH_PAT` actually being provisioned (**it is not, as of this writing** — confirmed via `gh secret list --repo smith-horn/skillsmith` — so the job ships alert-only from day one per the plan's explicit degrade-cleanly requirement) and not suppressed by shadow mode (`SKILLSMITH_SUBMODULE_POINTER_AUTOREPAIR_SHADOW`, default on — suppresses only the repair *push*, never the alert). On any other FAIL verdict (R1, R5, R6, R7, R8, R11 — orphaned tip, diverged, backward regression, external-contributor block, unresolvable already-registered pointer) it never auto-repairs — it opens a deduped `submodule-pointer-regression`-labeled GitHub Issue with the exact remediation command instead. Like `prod-deploy-cancel-monitor`, this job does **not** write to `audit_logs` either (pure git/gh CLI, no Supabase credentials in its GitHub Actions context). Full design: [smi-6260-docs-internal-pointer-regression-gate.md](docs/internal/implementation/smi-6260-docs-internal-pointer-regression-gate.md), [ADR-143](docs/internal/adr/143-docs-internal-gitlink-ancestry-invariant.md). Full table: [deployment-guide.md § Scheduled Jobs](.claude/development/deployment-guide.md#scheduled-jobs). Alerts to `support@smithhorn.ca` via Resend on failures. All jobs log to `audit_logs` table.

---

## Ruflo MCP Server + MCP Registry

**Ruflo** (hive mind, agent spawning): auto-configured via `.mcp.json`. Tools `mcp__ruflo__{swarm_init, agent_spawn, coordination_orchestrate, swarm_shutdown}`, plus `mcp__ruflo__memory_retrieve`, `mcp__ruflo__memory_list`, `mcp__ruflo__memory_delete` (SMI-5777 — `task_orchestrate`/`memory_usage`/`swarm_destroy` do not exist in the live registry; `memory_store` and `memory_bridge_status` **do** exist, confirmed live 2026-09-23 against the served `@claude-flow/cli@3.42.4` through the launcher (353 tools), and `memory_store` stays in `.claude/settings.json`'s `permissions.deny` until A6.1 consent — SMI-6744 A1.7). Agent types: architect, coder, tester, reviewer, researcher. Full guide: [claude-flow-guide.md](.claude/development/claude-flow-guide.md). **MCP Registry**: `io.github.smith-horn/skillsmith` on [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io/), auto-published via CI; sync `packages/mcp-server/{package,server}.json`. Auth: GitHub Actions OIDC (SMI-4534). Full guide: [mcp-registry.md](.claude/development/mcp-registry.md).

---

## Default Execution Model — Ruflo Queen + Worktrees + Model Tiering

**Default for any substantive, multi-step task: run a Ruflo queen-coordinator hive on a dedicated worktree** (`./scripts/create-worktree.sh`; the parent session is the queen). Trivial or purely conversational turns may run solo on the current branch.

**Worktrees are the default workspace.** Doing implementation work directly on `main` (or in the main checkout) requires an **explicitly approved exception** — state the rationale and get sign-off before proceeding. This keeps parallel sessions from colliding (SMI-4776) and keeps `main` clean.

**A `fork`-type subagent cannot itself act as the queen for model-tiered delegation.** Forks share the parent's context and always run on the parent's model by design, and are barred from spawning their own Agent-tool subagents — so a fork told to delegate work across the model tiers below will instead do all of it itself, in one long single-model run (observed: a fork handed a 4-PR queen-coordination task did exactly this, needing ~250 tool uses and two turn-limit resumes across ~2 hours). If the queen role itself needs to run in the background, keep the top-level/foreground session as the queen issuing model-tiered `Agent` calls directly, or use a fresh (non-fork) background subagent as the queen instead. A fork is still the right call for a single large task that doesn't need further fan-out.

**Route worker tasks by difficulty across model tiers** (the queen assigns each task to the cheapest tier that can do it well):

| Model | Role | Tasks |
|-------|------|-------|
| **Opus** | Hardest reasoning / adversarial | Detection-rule & algorithm design, FP/FN tuning, security & data-integrity design, adversarial review, plan-review, interpreting ambiguous findings |
| **Sonnet** | Core implementation | Feature code, edge<->core twin ports, tests, refactors, harness & report drafting |
| **Haiku** | Mechanical / high-volume | Fixture scaffolding, regression-baseline bumps, CSV/data wrangling, `index.md` edits, drafting Linear comments, doc formatting |
| **Codex (`gpt-5.6-sol` via NEEDLE)** | Cross-provider second opinion / burst capacity | Cross-provider second opinion on an already-drafted Opus design (different model family, not more reasoning depth — Skillsmith-internal adversarial review stays Opus), OpenAI-specific surfaces, burst work under Claude-quota constraints |

**The queen owns all side effects.** Workers/subagents never commit, push, post to Linear, or touch git — they hand their output (file edits, plus any `index.md` or Linear-comment drafts) back to the queen, who verifies and applies it. The `governance-specialist` subagent in particular must never commit to `main` or delete branches (SMI-5060). Foreground subagents only for interactive prompts; background subagents auto-deny unapproved tools. **This rule outranks this file's own blanket "after every commit"/"after every PR merge" instructions below** (incident, 2026-07-20: a subagent explicitly told not to commit/push/PR/merge instead did all four plus a Linear close-out, reasoning from those blanket lines — never filed as its own Linear issue; a prior version of this line miscited "SMI-5778," which is an unrelated npm-audit CI issue from the same day) — a subagent's own task prompt scoping is authoritative for that subagent; the blanket instructions bind only the top-level coordinating session.

**Codex dispatch (SMI-5668, ADR-128).** Codex-tier tasks are never routed through Ruflo or the Agent/Task tool — dispatch via `scripts/needle/dispatch.sh --workspace <worktree> --title ... --body-file ... [--expect-write]`, which shells out to a separately-authenticated `codex exec` process through NEEDLE. Sandbox is read-only, no override; Codex output is text the queen reads and applies, same as any subagent. **A task requiring actual file writes cannot succeed under the current read-only-only adapter** — pass `--expect-write` for write-intent prompts so `dispatch.sh` can detect and flag the sandbox-rejected-write false-success case (SMI-5700) instead of silently reporting success; omit it for analysis/review-only prompts, where "no diff" is the expected outcome. See `scripts/needle/README.md`'s Troubleshooting section for the full mechanism. Not for tasks depending on this session's accumulated context (no shared memory between harnesses), and not for anything a Sonnet/Haiku worker would finish in under ~2 minutes — a defensive default against Codex's own workspace-file auto-discovery surfacing unexpected priming content in an unusual workspace, not an expected per-dispatch tax (see `scripts/needle/README.md`). `dispatch.sh`'s terminal outcomes: `success` is **final** — consume the result, never re-dispatch, even when the results log also carries an incidental `sandbox_write_rejected=yes` note (expected steady-state under the read-only sandbox on an analysis-only prompt, not a partial result). Every other outcome (`blocked-by-sandbox`, `no-diff-despite-expected-write`, `success-without-agent-message`, or an unclassifiable failure) means the queen re-dispatches the task through normal Claude-tier routing rather than treating a Codex-dispatch failure as final. A pre-flight refusal (exit 2 — the target `--workspace` already holds stale `open`/`in_progress` beads, SMI-5847) is **not** a dispatch failure: clear the stale beads (`bf close <id> --workspace <dir>`) or wait for a legitimately concurrent dispatch to finish, then retry the identical dispatch — do not fall back to Claude-tier for an exit-2 refusal. One-time personal setup: `scripts/needle/README.md`.

Full agent catalog, swarm topologies, and hive-mind examples: [claude-flow-guide.md](.claude/development/claude-flow-guide.md); for SPARC methodology (not a v3 CLI subcommand), see the `sparc-methodology` skill.

---

## Publishing Packages

**Release prep**: `docker exec skillsmith-dev-1 npx tsx scripts/prepare-release.ts --all=patch` (also `--core=minor --cli=patch`, `--dry-run`). **Publish (CI-only)**: push the release-prep branch, open a PR, **merge it to `main`**, then `gh workflow run publish.yml -f dry_run=false` — the workflow always dispatches against `main` (no `--ref` defaults to the repo's default branch), never the release-prep branch itself, so publishing before merge silently republishes whatever was already on `main`. Cadence: weekly (Sun 03:00 UTC) OR `[Unreleased]` ≥ 15 entries ([ADR-114](docs/internal/adr/114-release-cadence-and-gh-release-alignment.md)). Order: core → mcp-server, cli, enterprise. **Local fallback isn't just deprecated — it's blocked** (SMI-4533): `scripts/lib/forbid-local-publish.mjs` is wired as `prepublishOnly` in every publishable package and refuses unless the process is a canonical-repo (`smith-horn/skillsmith`) GitHub Actions runner; break-glass is `SKILLSMITH_PUBLISH_OVERRIDE="SMI-NNNN <≥18-char rationale>"`, which appends an audit row to `~/.skillsmith-publish-overrides.log`. Known bypass: `npm publish --ignore-scripts` skips the hook entirely — npm trusted-publisher OIDC is the actual binding gate. Pre-publish checklist, version-pin rules, break-glass: [publishing-guide.md](.claude/development/publishing-guide.md).

---

## VS Code Extension

Published as `skillsmith-vscode` on [Marketplace](https://marketplace.visualstudio.com/items?itemName=skillsmith.skillsmith-vscode). No Docker (ADR-113). Build: `cd packages/vscode-extension && npm run build && npm run package:check`. CI publish, PAT rotation, changelog rules: [vscode-publishing-guide.md](.claude/development/vscode-publishing-guide.md).

---

## Skill Location Policy

"Skill" means two different things in this repo, sharing only the `SKILL.md` format — which category a new one belongs to decides where it goes:

- **Operational skills** (extend Claude Code itself, for developing Skillsmith) — split by *audience*: `~/.claude/skills/` (global, user-level) for cross-project conventions that apply beyond this repo (e.g. `commit`, `plan-review-skill`); `.claude/skills/` (the `skillsmith-strategy` submodule — see [Git-Crypt](#git-crypt-narrowed-scope)) for Skillsmith-specific dev workflow (`pr-reviewer`, `governance`, `pr-description`), gated because `skillsmith-strategy` is the private competitive-IP repo.
- **Product skill data** (Skillsmith's own domain — it's a skill *lifecycle manager*, so skills are also data it operates on, not just tooling) — lives wherever the consuming package already owns its data, same as any other source/fixture file: registry corpus (`data/external-skills/`), bundled installable assets (`packages/{mcp-server,cli}/**/assets/skills/`), test fixtures (`packages/*/tests/fixtures/**`).

When adding a new skill, ask: does this extend Claude Code, or is it content Skillsmith operates on as a product? That answers which half of this list it belongs to — no separate framework needed beyond this distinction.

---

## Skills & Embedding

Project skills load from the `.claude/skills/` mount-point of the `skillsmith-strategy` submodule. `LocalIndexer.index()` returns `[]` (not throws) when the directory is absent OR present-but-empty (gate #2, SMI-4829). Embedding: real ONNX (~50ms) or mock (`SKILLSMITH_USE_MOCK_EMBEDDINGS=true`); see [ADR-009](docs/internal/adr/009-embedding-service-fallback.md). Disable auto-update: `SKILLSMITH_AUTO_UPDATE_CHECK=false`.

---

## Session Priming (SMI-4451)

`SessionStart` hooks fire on `source=startup` for branches containing an SMI/wave token anywhere in the name (`smi-NNN` or `wave-NNN`; covers `fix/smi-…`, `chore/smi-…`, etc — SMI-4809 broadened the matcher from literal-prefix only). Deny list: `main`, `hotfix-*`, `dependabot/*`, `renovate/*`, `release/*`, `revert/*`. **This branch-token gate applies to the priming hook only** — the audit and MCP-guard hooks below fire on every `startup` regardless of branch. **The priming hook additionally fires on `source=compact` and `source=resume`** (SMI-6423) — a second `SessionStart` matcher entry (`"compact|resume"`) runs `session-start-priming.sh` alone, so the banner survives mid-session context compaction and `--resume` instead of only appearing once at the very first `startup`; the audit and MCP-guard hooks deliberately stay `startup`-only (24h-debounced diagnostics, not per-turn context injection — rerunning them every compaction would be pure overhead). Three hooks: priming (`scripts/session-start-priming.sh`, disable via `SKILLSMITH_DOC_RETRIEVAL_DISABLE_PRIMING=1`) + audit (SMI-4590 — Team/Enterprise namespace audit, 24h debounce, fail-soft, tier-gated; disable via `SKILLSMITH_SESSION_AUDIT_DISABLE=1`) + MCP command guard (SMI-5642 — warns on bare-command MCP server configs vulnerable to nvm-drift ENOENT, plus (SMI-6229) hosted MCP servers not scoped to docs-only across this repo's own configs and enabled plugin-installed ones, 24h-debounced per finding, fail-soft; disable via `SKILLSMITH_MCP_COMMAND_GUARD_DISABLE=1`). Three state-consumers surface diagnostics in the priming banner without being hooks: (a) the **auto-heal consumer** reads `~/.skillsmith/retrieval-autoheal.state` to surface a failing host native-binding repair (SMI-5426; disable: `SKILLSMITH_RETRIEVAL_AUTOHEAL_DISABLE=1`), (b) the **liveness consumer** reads `~/.skillsmith/retrieval-liveness.state` to surface when the telemetry feed is stale (SMI-5432; disable: `SKILLSMITH_RETRIEVAL_LIVENESS_DISABLE=1`), and (c) the **reindex consumer** reads `~/.skillsmith/reindex.state` to surface a failed, anomalous, or hung doc-retrieval reindex (SMI-5786 follow-up; disable: `SKILLSMITH_REINDEX_STALENESS_DISABLE=1`). All three are *consumers of state, not hooks* — the underlying remediation (auto-heal), detection (liveness check), and reindex trigger (`.husky/post-commit`, main-checkout commits only) are driven by their own schedules/triggers (post-merge, weekly eval cron, and main-checkout commits respectively). Full mechanism: [ruvector-dev-tooling.md § Session Priming](.claude/development/ruvector-dev-tooling.md#session-priming-smi-4451).

---

## Troubleshooting

| Problem | Fix |
|---------|-----|
| A `grep`/`head`/`sed`/`cat`/inline-interpreter read of `.env` or `.env.registry` — or a `docker exec … /app/.env` wrapper around one — is denied with an `[env-read-guard]` reason | The `PreToolUse` Bash guard (`scripts/env-read-guard.mjs`) blocks any command that names a secret-bearing env file as a read target. It inspects the whole command string, so argument order and a `docker exec` wrapper do not evade it. Inspect config state with `varlock load` (default pretty format, masked) or `varlock load --quiet` — **not** `--format json`/`json-full`/`json-full-compact`/`env`, which are unmasked and also denied. `.env.schema` and `.env.example` stay readable; an output-free presence check (`grep -qE '^KEY=' .env`) is explicitly allowed. Genuine false positive: `SKILLSMITH_ENV_READ_GUARD_DISABLE=1` for that invocation, and file a Linear issue so the shape gets added to the allow list. Registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). |
| `gh pr create`, `node scripts/*.mjs`, or a relative `--workspace`/path CLI argument (e.g. `scripts/needle/dispatch.sh`) fails after an earlier `cd` into a worktree or its `docs/internal` submodule (SMI-5978) | `git -C <dir>` does NOT protect these — they read the shell's actual process cwd, not a `git -C` target, so a `cd` left over from a prior command (e.g. `cd docs/internal && gh pr create`) silently redirects them. Check `pwd` before any `gh`/`node`/relative-path command that follows a foreground `cd`, or pass an absolute path. `scripts/needle/dispatch.sh`'s own `--workspace` not-found error now names the cwd it resolved against, to make this self-diagnosing. |
| Scan-coverage alert fired (`scan_coverage_degraded`, SMI-5866) | The security-scan write path has stayed abnormally NULL for skills created in the last 24h — confirm with `SELECT * FROM check_scan_coverage();` via `./scripts/pooler-psql.sh`, then inspect the indexer scan write path (`scripts/indexer/skill-processor.ts` around the `security_score` assignment, and the `validationCache` round-trip in `indexer-runners.ts` — this is the exact class of bug SMI-5849 was). Auto-clears on the first tick back under threshold; re-alerts at most once per 24h while degraded. If no alert email ever arrives despite a confirmed degradation, check whether `SCAN_COVERAGE_ALERT_SHADOW` (default on) was ever lifted to `0` — shadow mode still measures/writes state/audits but never sends. Disable sending entirely: `SCAN_COVERAGE_ALERT_DISABLE=true` (Supabase edge-function secret). |
| `indexer_degraded` alert fired (SMI-6220) | A `Skill Indexer` GitHub Actions run concluded `success` but tripped a threshold arm — the email body names which one(s) (rate-limit bucket, discovery failure-ratio, secondary rate-limit hits, or recheck prevention-outage) with the observed values and thresholds in force. Confirm live with `SELECT * FROM v_indexer_health ORDER BY created_at DESC LIMIT 5;` via `./scripts/pooler-psql.sh`. For a rate-limit arm, check `scripts/indexer/_shared/rate-limit.ts` (per-bucket `*_observed`/`*_remaining_min`); for a ratio/recheck arm, check `scripts/indexer/discovery-orchestrator.phase-split.ts` / `scripts/indexer/recheck.ts`. Stateless — no dedup, so a sustained degradation re-alerts every qualifying run. Disable sending: `SKILLSMITH_INDEXER_DEGRADED_ALERT_DISABLE=1` (repo variable); shadow mode (default, no real send) is controlled by `SKILLSMITH_INDEXER_DEGRADED_ALERT_SHADOW` (unset or non-`'0'` = shadow). Full design: [smi-6220-indexer-alert-threshold-sparc.md](docs/internal/implementation/smi-6220-indexer-alert-threshold-sparc.md). |
| `indexer_upsert_blocked` alert fired (SMI-7113) | Supabase's Cloudflare firewall refused one or more skill rows in an indexer upsert. The batch was bisected, so only the refused rows failed; every other row was written. The alert fires on a row not blocked in the last 48h, an exhausted bisect budget, or a truncated block-history lookup, whatever the run's failure ratio. List the refused rows with `SELECT created_at, resource, metadata->>'ray_id' AS ray_id, metadata->>'payload_bytes' AS payload_bytes FROM audit_logs WHERE event_type = 'indexer_upsert_blocked' ORDER BY created_at DESC LIMIT 10;` via `./scripts/pooler-psql.sh`. Use `metadata->'field_lengths'` to find the field that trips the rule. The row's content is never logged, only its identity and sizes. **Scheduled discovery** skips a row blocked in the last 24h, so a repeat offender does not re-alert every slot. **Backfill never skips**: its checkpoint advances past unwritten rows, so a skip would lose them. If the budget was exhausted, the rows past it were not bisected; raise `INDEXER_UPSERT_BLOCK_BISECT_BUDGET` (repo variable, default 64) or wait for the next run. A non-zero exit from the alert step itself means the indexer output was missing, stale or unparseable, which is a different fault from a block. Switches: `SKILLSMITH_INDEXER_UPSERT_BLOCK_ALERT_DISABLE=1` skips only the alert POST; `SKILLSMITH_INDEXER_UPSERT_BISECT_DISABLE=1` (or `true`) restores the old fail-the-whole-batch behaviour and skips the 48h lookup. Registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). [Details](docs/internal/implementation/smi-7112-indexer-ops-reliability-sparc.md) |
| `indexer_lock_starvation` alert fired (SMI-6209) | Arm A (last 3 indexer runs all lock-skipped) or Arm B (a run_type's last non-skip row is stale beyond its threshold — 12h discovery, ~30h maintenance/recheck) tripped — the email body names which arm(s) and, for Arm B, the exact staleness/threshold per run_type. Confirm live with `SELECT id, created_at, metadata->>'run_type' AS run_type, metadata->'meta'->>'status' AS status FROM audit_logs WHERE event_type = 'indexer:run' ORDER BY created_at DESC LIMIT 10;` via `./scripts/pooler-psql.sh`. Remediation: (1) identify the offending `indexer-backfill.yml` run holding the singleton lock — `gh run list --workflow=indexer-backfill.yml --status=in_progress` and note its dispatch/run ID; (2) either cancel it (`gh run cancel <run-id>`) or wait for its SMI-5311 heartbeat to stop and the 20-min stale-lock TTL to lapse; (3) confirm the lock actually released with `SELECT locked_at FROM indexer_lock WHERE id = 1;` — `locked_at` should be NULL or older than 20 minutes. Re-alerts at most once per 24h while degraded; auto-clears on the first tick with neither arm tripped. Disable sending: `SKILLSMITH_INDEXER_LOCK_STARVATION_ALERT_DISABLE=1`; shadow mode (default, no real send) is controlled by `SKILLSMITH_INDEXER_LOCK_STARVATION_ALERT_SHADOW` (unset or non-`'0'` = shadow). Root cause (unbounded `indexer-backfill.yml` lock-hold) fixed by SMI-6246 — see the next row; this alert should now be rare. |
| Backfill auto-chain stopped and emailed an alert (SMI-6246) | `indexer-backfill-autochain.yml` hit its retry cap (5 consecutive lock-skips against the same checkpoint) and stopped self-dispatching rather than retrying forever. Confirm the current lock holder: `SELECT locked_by, locked_at FROM indexer_lock WHERE id = 1;` via `./scripts/pooler-psql.sh`. This is not itself a failure — a campaign that stops auto-chaining is not lost; either wait out the contention or manually intervene (set `BACKFILL_AUTOCHAIN_DISABLE` to `1` first if you need to dispatch while a campaign is actively auto-chaining, per the runbook's Golden rule 4; never delete it, SMI-7112). See [indexer-backfill-lock-yield-plan.md](docs/internal/implementation/indexer-backfill-lock-yield-plan.md) and [ADR-140](docs/internal/adr/140-indexer-backfill-lock-yield-priority-invariant.md). Kill switches: `BACKFILL_AUTOCHAIN_DISABLE=1` stops all self-chaining; `INDEXER_LOCK_RETRY_DISABLE=1` (read by `indexer.yml`) disables the scheduled-run side's bounded retry. Both registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). |
| Backfill dispatch switch `BACKFILL_AUTOCHAIN_DISABLE` (SMI-7112): the autochain went red on a variable-read failure (an `autochain-read-enable` error line, or the `indexer_backfill_autochain_failed` email), or `backfill-rollout-driver.sh` exits 2 with `must read exactly 1` | Never delete `BACKFILL_AUTOCHAIN_DISABLE`; change its value only. `1` means the local driver may run and the autochain does not dispatch; `0` means the autochain dispatches. Set `1` before any manual backfill dispatch. A red autochain run that logs a read failure dispatched nothing: fix the secret `BACKFILL_VARIABLE_READ_PAT` (fine-grained PAT, Variables: read, this repo only, check its expiry first), then resume through the runbook. Run the driver only while the variable reads `1`. Handover, emergency brake, intent reconciliation and PAT rotation: [backfill-autochain-handover.md](docs/internal/runbooks/backfill-autochain-handover.md). The autochain also refuses to dispatch, and goes red with a warning, if any indexer-backfill run was created after the run that triggered it (SMI-7112). |
| `refresh_unhealthy` alert fired (SMI-7114) | The refresh observer (`scripts/ci/refresh-observer.sh`, run by the `refresh-metadata.yml` schedule every 4h) found at least one arm tripped; the email names each: `stale` (no non-failed `refresh:run` in 9h), `dispatch_stale` (no pg_cron `refresh:dispatch` row in 9h: the dispatcher has not completed an enqueue. That is either pg_cron not firing or the dispatcher raising, for example on a missing `refresh_metadata_functions_base_url` / `refresh_metadata_service_role_key` vault secret, which aborts before the row is written. Tell them apart with `SELECT status, return_message, start_time FROM cron.job_run_details WHERE jobid = (SELECT jobid FROM cron.job WHERE jobname = 'skills-refresh-metadata') ORDER BY start_time DESC LIMIT 5;` -- no recent rows means pg_cron is not firing, `failed` rows name the error) or `failures` (`run_failed`, `no_response`, `http_error`, `transport_error`). Confirm live with `SELECT public.refresh_metadata_health();` via `./scripts/pooler-psql.sh`. A `transport_error` (no HTTP status) usually means pg_net stopped waiting, not that the refresh failed; it also covers connection failures, so read the entry's `error_msg` in the RPC output first. For a timeout, check for a `refresh:run` row up to 5 minutes after that dispatch (the request timeout; the measured prod maximum run is under 3 minutes) before acting. A failure stays in the 9h window, so a later observer run may re-report it. Disable sending: `SKILLSMITH_REFRESH_OBSERVER_ALERT_DISABLE=1` (repo variable); while unhealthy with it set, the observer writes no heartbeat, so the dead-man below still pages (timing in the registry row). Registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). |
| `refresh_observer_dead` alert fired (SMI-7114) | The pg_cron dead-man (`check_refresh_observer_heartbeat()`, hourly at :17) saw no `refresh:observer` heartbeat for over 16h: the GitHub observer is not running, is failing, or is unhealthy with alerts disabled. Check `gh run list --workflow=refresh-metadata.yml` first. It re-alerts on the next hourly tick until an alert written after the latest heartbeat is delivered (2xx and `data.sent=true`), then at most daily per outage. It is silent until the first heartbeat exists (owner step M7 in [smi-7112-indexer-ops-reliability-sparc.md](docs/internal/implementation/smi-7112-indexer-ops-reliability-sparc.md)). If it should have paged and did not, look for `SELECT created_at, metadata->>'missing_secret' FROM audit_logs WHERE event_type = 'refresh:observer_deadman_error' ORDER BY created_at DESC LIMIT 5;` -- a missing vault secret leaves it unable to page. Silence it with the Vault secret `SKILLSMITH_REFRESH_OBSERVER_DEADMAN_DISABLE` (`1`/`true`), or stop it with `SELECT cron.unschedule('refresh-observer-heartbeat');`. Registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). |
| Backfill autochain alert fired: `indexer_backfill_autochain_halted`, `_needs_intervention`, `_multiple_deferrals`, `_kill_switch_stop`, `_campaign_complete`, or the pg_cron watcher's `_stalled` / `_unreconciled` / `_resume_dispatch_failed` (SMI-7121, SMI-7142) | Never delete a deferral-marker row to clear an alert: close one with the transition command in section 8.4. `_halted`: the email names the halt; investigate it, then continue with [backfill-autochain-handover.md](docs/internal/runbooks/backfill-autochain-handover.md) section 8.1 (`acknowledge_halts` names only the halts you investigated; `no_progress_record` cannot be acknowledged). `_needs_intervention`: sections 8.2 and 8.3. `_multiple_deferrals`: section 8.6. `_kill_switch_stop`: `BACKFILL_KILL_SWITCH=1` stopped a live chain link, and turning it off does not resume the chain; continue from the named run with section 8.1 (`BACKFILL_AUTOCHAIN_DISABLE=1`, driver mode, stops links quietly by design). `_campaign_complete` is informational: the campaign finished and its marker is closed. `indexer_backfill_autochain_retry_cap` is the SMI-6246 lock-contention stop (row above). `_stalled` and `_unreconciled` come from the Supabase pg_cron watcher, independent of GitHub and of `BACKFILL_AUTOCHAIN_DISABLE`: sections 8.3 and 8.4. `_resume_dispatch_failed`: the pg_cron resume dispatch was refused, could not be sent, or got no pg_net response within 1h, section 8.7. The watcher only alerts and never repairs; silence it with the Vault secret `SKILLSMITH_BACKFILL_AUTOCHAIN_WATCH_DISABLE` (`1`/`true`), and a missing Vault secret leaves it unable to page. Silence only the pg_cron resume dispatcher with the Vault secret `SKILLSMITH_BACKFILL_AUTOCHAIN_RESUME_DISABLE` (`1`/`true`); `SELECT cron.unschedule('backfill-autochain-resume');` stops it. Thresholds `BACKFILL_BUSY_HOURS` and `BACKFILL_NDB_MAX` (repo variables) and the `acknowledge_halts` input are registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). |
| `fatal: <path>: clean filter 'git-crypt' failed` (worktree creation, `git status`, or `git-crypt unlock` itself hangs/fails with this) | `filter.git-crypt.{smudge,clean,required}` is repo-shared state (main checkout + every worktree) — a broken registration can silently write raw ciphertext to disk on `git reset --hard`/checkout. Fix: `./scripts/worktree-crypt.sh fix <worktree-path>` (runs `ensure_git_crypt_filter_registered()`, `scripts/_lib.sh` — never remove the `filter.git-crypt.*` config keys directly, which is what caused this twice repo-wide, SMI-5702/SMI-5861). Disable the self-heal: `SKILLSMITH_GIT_CRYPT_FILTER_HEAL_DISABLE=1`. [Details](docs/internal/implementation/smi-5702-worktree-git-crypt-filter-deadlock.md) |
| `npm install`/`npm ci` fails with `EACCES` on a `rename`/`rmdir` under `node_modules` on the HOST (main checkout), even though `ls -la` shows normal ownership/permissions (SMI-6034) | A macOS ACL, not a stuck lock — Docker Desktop's gRPC-FUSE file-sharing layer stamps a `user:williamsmith deny delete` ACL on **every named-volume mount target** nested inside worktree containers' `:ro` bind of this tree, on **every container start**: the original 7 Tier-A native-module paths (`node_modules`, `node_modules/{esbuild,@esbuild,better-sqlite3,hnswlib-node,onnxruntime-node}`, `packages/core/node_modules`, `packages/core/node_modules/better-sqlite3`, SMI-5650 Wave 2) **plus every Tier-B build-tool-binary path** since SMI-6050 Wave 3 (~120 additional paths, e.g. `node_modules/@turbo/linux-arm64` — confirmed live: `ensure_tier_b_mount_sources()` creates these as real mkdir'd directories, and Docker Desktop stamps the same ACL on them the moment any worktree container using them starts). Confirm with `ls -lde <path>` (not `ls -le`, which doesn't reliably surface this ACL type). Unblock, covering both tiers: `chmod -N node_modules node_modules/{esbuild,@esbuild,better-sqlite3,hnswlib-node,onnxruntime-node} packages/core/node_modules packages/core/node_modules/better-sqlite3 $(node scripts/lib/linux-optional-packages.mjs)` — the Tier-B portion is derived dynamically (never hand-enumerate it; SMI-6050's own package-lock.json-derived list is the single source of truth here too, same script both mechanisms share) — only durable until the next container start anywhere binds this tree, so run it immediately before the `npm install`/`npm ci` you need, not proactively. Durable fix tracked in SMI-6034 (ADR-109 gated — needs SPARC + plan-review before implementation). |
| `npm run build --workspace=packages/website` (or the full `npm run build`) fails in a Docker container with `EACCES, Permission denied '/app/packages/website/.vercel/output/_functions/'`, or `npm run audit:standards` crashes outright with an unhandled `ENOENT` inside `getFilesRecursive` (SMI-6192, shipped) | **Not** the SMI-6034 ACL row above — different symptom, different mechanism: this is Docker Desktop's virtiofs bridge corrupting a freshly-written *file*'s mode inside the plain `.:/app` bind mount (no ACL, not a named-volume mount-target directory), hitting `packages/website/.vercel` build output in both worktree containers and the main checkout's `skillsmith-dev-1` equally. Fixed: `packages/website/.vercel` now mounts as its own named volume (`website-vercel-output` in `docker-compose.yml`), which routes it around the bridge entirely. If you still see this, your container predates the fix — recreate it: `docker compose --profile dev up -d --force-recreate dev` (or `./scripts/worktree-docker.sh start` in a worktree). `audit:standards`' Check 41 also no longer crashes the whole process on a corrupted `.vercel/output` — `getFilesRecursive` now skips `.vercel` outright, matching its existing `node_modules`/`dist`/`.git` skip-list. [Details](docs/internal/implementation/smi-6192-website-vercel-output-eacces.md) |
| `npm run build` fails only for `@skillsmith/website` with `EXDEV: cross-device link not permitted, rename '/app/packages/website/.vercel/output/server/.prerender/_astro/<file>' -> '/app/packages/website/dist/client/_astro/<file>'` (SMI-6426, open; SMI-6476 is a duplicate) | **This is the successor to the SMI-6192 row directly above, not a recurrence of it** — the `.vercel` named-volume mount that routed EACCES around the virtiofs bridge also made `.vercel/output` a *different device* from the `dist/` bind mount, so Astro's "Rearranging server assets" step (`ssrMoveAssets`) can no longer `rename(2)` across the boundary. Reproduced independently at least three times (2026-09-07, 09-08, 09-09) by three sessions, none of them working on the website. **Not caused by your diff** unless your diff touches `packages/website`. Turbo reports `6 successful, 7 total` and Astro itself logs `0 errors` immediately before the rename — the compile is fine, only the asset move fails. **If you were rebuilding to refresh a stale `dist/` (the usual reason), that goal is still met**: the other six packages built. Verify by re-running the specific test that was failing rather than trusting the non-zero exit code. No fix or opt-out exists yet; SMI-6426 is the tracking issue. |
| `npm run lint` exits 1 with ~14 `no-unused-vars` errors, every one of them under `.claude/skills/concurrency-auditor/tests/pattern-6/fixtures/` (SMI-6562 et al., open) | **Pre-existing, not yours, and structurally invisible to CI.** Those files are deliberately-malformed fixtures for a pattern detector, and they live in the `skillsmith-strategy` submodule -- `git ls-files --error-unmatch <path>` fails for them, because this repo does not track them. CI never sees them: `ci.yml`'s `quality-checks` job checks out with `submodules` unset, so `.claude/skills/**` does not exist on the runner and `npm run lint` there lints a smaller tree than yours. So a green CI and a red local lint are both correct, and neither is evidence about the other. Confirm it is not yours in one step: `git show --name-only --format= HEAD` and check the failing paths are absent from it. **Do not file another issue** -- this has been filed at least three times (SMI-6475, SMI-6484, SMI-6562); add a datapoint to SMI-6562, which names the mechanism. Because this row's own neighbours tell you to run local checks before pushing, every session with the strategy submodule initialized rediscovers it; that recurring diagnosis cost is the reason this row exists. |
| `npm run preflight` exits 1 with `❌ Found 3 missing dependencies: @isaacs/keytar, async_hooks, @wdio/globals` (SMI-6474 et al., open) | **Pre-existing on `main`, not your branch.** `scripts/preflight-check.ts`'s undeclared-dependency scan (SMI-760) has failed on a clean `main` since at least 2026-07-14. `async_hooks` is a Node **builtin**, so the scan is at minimum partly false-positive. Confirm it is not yours in one step: `git diff --name-only origin/main...HEAD` — if that touches no `packages/**` TypeScript and no `package.json`, a dependency scan cannot be perturbed by your change. To see it fail identically without your commits, run `npx tsx scripts/preflight-check.ts` in any pristine `main` worktree. Note the failure is in the **first** half of `preflight`'s `&&` chain, so `verify-publish-deps.mjs` never runs — a green `verify-publish-deps` is not evidence either way. **Do not file another issue for this**: it has been filed repeatedly (SMI-5694, SMI-5782, SMI-5920, SMI-6030, SMI-6035, SMI-6048, SMI-6100, SMI-6310, SMI-6474, SMI-6482). Add a datapoint to SMI-6474, the canonical issue, instead. Because CLAUDE.md tells everyone to run `preflight` before pushing, every session rediscovers this; that recurring diagnosis cost is the reason this row exists. |
| `npm run build`/`astro check` fails inside a worktree container with "Turborepo did not find the correct binary for your platform" or "Cannot find native binding"/"Cannot find module '@rolldown/binding-...'" for a build tool (turbo, Rollup/Rolldown, Astro's compiler, Lightning CSS, Tailwind Oxide, ruvector, Wrangler/workerd) (SMI-6050, shipped) | The shared HOST `node_modules` tree is built on macOS, which skips Linux-only `optionalDependencies` — worktree containers (Linux) read that tree read-only and never see these binaries at all, until SMI-6050: worktree containers now get a real writable named-volume seed of every linux-only optional-platform package `package-lock.json`'s `os` field identifies (`scripts/lib/linux-optional-packages.mjs`, self-updating on every dependency bump — no hardcoded list), restored at container boot from the image's own real Linux `npm ci` output (`/opt/native-seed/tier-b`, built into the image at `docker compose build` time). If you still hit this on a worktree created before SMI-6050 landed: pull `main`, then recreate the container (`docker compose --profile dev up -d --force-recreate dev`) or regenerate the override via `./scripts/repair-worktrees.sh` from the main checkout first. **One-time host cleanup** if you manually patched your own host `node_modules` before this fix shipped (tell-tale sign: `ls -la node_modules/@turbo/linux-arm64` shows an `Oct 26 1985` mtime — the artifact of an `npm pack`+`tar` extraction that didn't preserve timestamps, not a real install): remove the stale manually-patched directories so your next rebuild proves the real seed mechanism, not a leftover hand-patch — `chmod -N` those specific paths first per the SMI-6034 entry above, then `rm -rf` them, then rebuild. Disable the whole mechanism (fallback to pre-SMI-6050 behavior): `SKILLSMITH_TIER_B_SEED_DISABLE=1` (registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md)). [Details](docs/internal/implementation/smi-6050-worktree-linux-optional-platform-binaries.md) |
| `dev` container OOM-kills or a build/test run under heavy concurrent-worktree load hits the per-container ceiling (SMI-6064) | `docker-compose.yml`'s `dev` service now caps CPU/memory (`cpus:4`/`mem_limit:6g` default — empirically verified against a real `npm run lint` run, not just arithmetic; an earlier 3g default reliably OOM-crashed type-aware ESLint) so one hot container can't starve every sibling worktree container on the shared Docker Desktop VM pool. If a legitimate workload needs even more, override per-invocation: `SKILLSMITH_DOCKER_CPUS=8 SKILLSMITH_DOCKER_MEM=7g docker compose --profile dev up -d --force-recreate dev` — same `${VAR:-default}` pattern as `DEV_PORT`. To compute your own default (your machine's VM allocation may differ from the 10-CPU/7.75GiB calibration machine), see the Decision Log in [docker-resource-ceiling.md](docs/internal/implementation/docker-resource-ceiling.md). |
| Container won't start | `docker compose --profile dev down && docker volume rm skillsmith_node_modules && docker compose --profile dev up -d`, then finish with the ordered refresh sequence — `( cd <main-checkout-path> && sh scripts/lib/print-deps-refresh-advice.sh <main-checkout-path> )`. Don't stop at `up -d`: the wiped root volume re-seeds from the image, not from your current lockfile, and the eight per-package `*-node-modules` volumes aren't wiped at all (SMI-6674). |
| Native module errors | `docker compose restart dev` (entrypoint self-heals better-sqlite3 / onnxruntime-node / hnswlib-node on restart, SMI-5351; first run may re-download a prebuilt) or per-module, mount-gated (SMI-6614): `docker exec -w /app skillsmith-dev-1 sh -c 'sh scripts/lib/node-modules-mount-gate.sh && npm rebuild <module> --ignore-scripts=false'` — a non-zero exit with no rebuild output means a node_modules mount is detached or not a volume; recreate first (`docker compose --profile dev up -d --force-recreate dev`). esbuild's binary loads lazily, so a corrupt esbuild binary isn't auto-detected — SMI-5352. |
| A pre-push failure prints `✗ Native SQLite binding is broken in the dev container` with a `Mount check: <STATE> -- cause: <CAUSE>` line (SMI-6684/SMI-6687, ADR-165) | **The line carries two independent fields; read the CAUSE for the remedy.** The old "usually follows a bare `npm install`" wording was never measured and is gone. **STATE** answers "did the check produce a usable answer": `FINDINGS` / `FINDINGS + PARTIAL` (it did, and the checker found something — the common case), `CLEAN` / `PARTIAL` (it did, nothing found; `PARTIAL` means some assertions examined nothing), and `TIMEOUT` / `UNEXPECTED` / `MALFORMED` / `NO-REPORT` / `UNAVAILABLE` (**it did not — none of these is a clean result**). A `, REPORT/OUTPUT MISMATCH` suffix means the report and the checker's output disagree, so no cause is named. `OFF` means attribution was disabled. **CAUSE** answers "which mechanism broke the binding": `FALL-THROUGH` = the mount detached and the host's macOS binary is served at a Linux path (SMI-6516) → `docker restart <container>`, escalating to stop/start; `MOUNT-MISSING` = a declared mount at or above `better-sqlite3` is absent → same ladder, and **in a worktree only**, if it survives both, `./scripts/repair-worktrees.sh` then recreate; `MOUNT-SUBSTITUTED` = the wrong backing is mounted → in the main checkout `docker compose --profile dev up -d --force-recreate dev`, in a worktree `./scripts/worktree-docker.sh stop && … start`; `SEED-CONTENT` = mounted but the seeded bytes are wrong → a restart re-seeds the same content, so attach the full check to SMI-6547; `OTHER-NATIVE-FINDING` = a finding at `better-sqlite3` the message does not classify → act on the checker's own line; `NO-NATIVE-FINDING` = nothing wrong there *now*, either never built or a detach already repaired between the failure and the probe, which is **not** distinguishable here, so it never asserts "unbuilt"; `NOT-DETERMINED` = absence could not be concluded. Every state prints one next action; every state **except `UNAVAILABLE`** also prints a `Full check:` command, and `UNAVAILABLE` (the branch predates the checker) replaces that line with rebase advice while still printing its next action. Each run appends one line to `~/.skillsmith/logs/native-attribution.jsonl` (no rotation, by design — the falsifier and retirement test need a lifetime denominator). Opt-outs: `SKILLSMITH_NATIVE_CHECK_ATTRIBUTION_DISABLE=1` (attribution only), `SKILLSMITH_SKIP_NATIVE_CHECK=1` (the whole probe), and `SKILLSMITH_MOUNT_COMPOSITION_DISABLE=1` (the checker itself, which renders `NO-REPORT [checker-disabled]`). **`docker restart` fixes this fault and erases the evidence**, so capture the block (or `docker exec -w /app <c> bash scripts/lib/check-mount-composition.sh --mode auto --live --no-report`) before restarting. |
| `hnswlib-node` fails validation after rebuild (`ignore-scripts=true` in `.npmrc` blocks node-gyp — see SMI-5200) | The entrypoint now self-heals on restart (`docker compose restart dev`, SMI-5351 extended `--ignore-scripts=false` to all native modules). `docker volume rm skillsmith_node_modules` is a last resort only. |
| `skillsmith` MCP CONNECTION_CLOSED, host process dies in <1s with exit 137 and zero stderr | A host-loaded `.node` addon has an invalid or missing code signature (check via `codesign -dv <path>` on `better-sqlite3`'s nested `packages/core/node_modules/better-sqlite3` copy and root `node_modules/hnswlib-node`). Fix: `./scripts/repair-host-native-deps.sh`, then manually rebuild `hnswlib-node` (`npm rebuild hnswlib-node --build-from-source`, falling back to `npm run rebuild` inside its package dir) and re-verify with `codesign -dv`. Distinct from the "Platform mismatch (SIGKILL 137)" row below: same signal (SIGKILL/137) but different root cause (ABI/arch mismatch, container-side only, no `hnswlib-node` involvement) and different remedy — `codesign -dv` is the discriminator: an ABI mismatch fails `require()` with a normal JS error, while an invalid-signature kill shows `SIGKILL (Code Signature Invalid)` in a crash report (`~/Library/Logs/DiagnosticReports/node-*.ips`) and `codesign -dv` reports "not signed" or a stale/mismatched signature. |
| Platform mismatch (SIGKILL 137) | `rm -rf packages/*/node_modules/better-sqlite3 packages/*/node_modules/onnxruntime-node` then rebuild. See also the `skillsmith` MCP CONNECTION_CLOSED row above — same SIGKILL/137 signal, different root cause; check `codesign -dv` to tell them apart. |
| Node ABI mismatch | WASM fallback auto-activates (core ≥0.4.10). Restore native: rebuild in Docker + `./scripts/repair-host-native-deps.sh` (SMI-4549) |
| Host retrieval autoheal failed or in cooldown | Read `~/.skillsmith/logs/retrieval-autoheal-<date>.log` → fix the root cause → reset with `rm ~/.skillsmith/retrieval-autoheal.state` (next `post-merge` retries) → or disable with `SKILLSMITH_RETRIEVAL_AUTOHEAL_DISABLE=1` (SMI-5426) |
| Retrieval-telemetry liveness alert fired (GitHub issue `telemetry-liveness`) | Read `~/.skillsmith/logs/retrieval-liveness-<date>.log` → repair with `./scripts/repair-host-native-deps.sh` → snooze a known away-window with `SKILLSMITH_RETRIEVAL_LIVENESS_SNOOZE_UNTIL=<epoch>`, or disable with `SKILLSMITH_RETRIEVAL_LIVENESS_DISABLE=1` (SMI-5432) |
| `status-external-outage` GitHub Issue fired | Both `status-public` URLs (raw Supabase ref + `api.skillsmith.app`) failed 3/3 attempts from outside Supabase/Vercel — check <https://status.supabase.com> and Vercel's status page first; the issue auto-closes on the next healthy tick (~10 min); snooze a known maintenance window with the `SKILLSMITH_STATUS_EXTERNAL_PROBE_DISABLE=1` repo variable (SMI-5756) |
| `prod-deploy-approval-cancelled` GitHub Issue fired | An `Approve Production Deploy` job in `website-deploy-staging.yml` was cancelled after reaching its approval gate or mid-deploy. Check whether it was an intentional manual cancellation (close the issue if so — this detector cannot distinguish intentional from accidental); otherwise confirm the live site matches the latest tested `main` commit and re-trigger the deploy if stale. If this recurs, verify `approve-gate`'s job-level concurrency group (`website-production-approve-*`, run-ID-keyed, `cancel-in-progress: false`) wasn't reverted — a live test on 2026-08-27 proved the original single-job, ref-keyed design does NOT protect a `waiting` job, hence the split into `approve-gate` + `deploy-production`. Disable: `SKILLSMITH_PROD_DEPLOY_CANCEL_MONITOR_DISABLE=1` (SMI-6208) |
| `submodule-pointer-regression`-labeled GitHub Issue fired (SMI-6260) | `pointer-autorepair` (`.github/workflows/submodule-pointer-check.yml`) detected a `docs/internal` gitlink FAIL on a push to `main` that it could not safely auto-repair. The issue body names the exact rule that fired (R1 orphaned-never-pushed, R5 orphaned-tip, R6 diverged, R7 backward regression relative to what `main` already registered, R8 PAT-unavailable, or R11 already-registered pointer unresolvable) and — for R1/R5/R6/R7 — the exact `./scripts/bump-docs-pointer.sh` invocation that resolves it; for R11, a maintainer must manually repair `main`'s pointer directly (see [ADR-143](docs/internal/adr/143-docs-internal-gitlink-ancestry-invariant.md)'s `docs-internal-pointer-repair` note) before R7 can validate future bumps against it. Confirm the current state live: `git -C docs/internal fetch origin main --prune && git -C docs/internal merge-base --is-ancestor <registered-sha> origin/main && echo behind`. Note: an R3 (behind) detection does NOT open this issue while `SKILLSMITH_MAIN_PUSH_PAT` is provisioned and shadow mode is off — it auto-repairs silently instead; R3 only opens an issue when the auto-repair push was suppressed (shadow mode, unprovisioned PAT, or a push failure) — see [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md) for the exact shadow/kill-switch var names. Dedup key is the exact commit SHA `pointer-autorepair` reacted to, so a fresh regression on a later push always gets its own issue even if an older one is still open. |
| "invalid ELF header" in Docker (SMI-4698) | Try `docker compose restart dev` (self-heals) or, mount-gated (SMI-6614): `docker exec -w /app skillsmith-dev-1 sh -c 'sh scripts/lib/node-modules-mount-gate.sh && npm rebuild <module> --ignore-scripts=false'` first — a non-zero exit with no rebuild output means a node_modules mount is detached or not a volume; recreate first (`docker compose --profile dev up -d --force-recreate dev`); for persistent host-binding leaks, see [git-crypt-guide.md § Host Native Bindings](.claude/development/git-crypt-guide.md#host-native-bindings--sessionstart-instrumentation-smi-4549) |
| A worktree's edits to `.husky/*` or `scripts/*` don't take effect on `git push`/`git commit`, though `bash scripts/<x>.sh` run directly does (SMI-6334) | `core.hooksPath` drifted to an absolute path into the MAIN checkout, so every worktree ran main's hooks and scripts. Verify with `git config --get core.hooksPath` — it must be the relative literal `.husky/_`, not an absolute path. Fix: `./scripts/repair-worktrees.sh`, or directly `git config core.hooksPath .husky/_` (one shared value; fixes main and every worktree at once). Disable the self-heal: `SKILLSMITH_HOOKS_PATH_HEAL_DISABLE=1`; disable the pre-push warning: `SKILLSMITH_SKIP_HOOKS_PATH_CHECK=1`. [Details](docs/internal/implementation/smi-6334-worktree-hookspath-fix.md) |
| Worktree `npm run build` fails (SMI-4689) | SMI-4738 postinstall auto-regenerates override; bounce worktree container. Drift: `./scripts/repair-worktrees.sh` from main repo. macOS only. [Details](.claude/development/git-crypt-guide.md#worktree-docker-bind-mounts-smi-4689) |
| Worktree container hits `EROFS` writing `node_modules/.vite-temp` or `node_modules/.astro` (SMI-5705/SMI-5722) | A Docker bind mount's source is resolved at container-**create** time, so a fresh container can be created before the writable cache-overlay source directory exists on the host. `ensure_build_cache_mount_sources()` (`scripts/_lib.sh`) pre-creates these directories, but only at the trigger points named below — a bare `docker compose --profile dev up -d --force-recreate dev` does NOT re-run it, so it can "fix" some packages (whichever already had the directory) and leave others still broken (SMI-6202 retro). Run `./scripts/repair-worktrees.sh` (safe from the main checkout even with another session's container active) THEN recreate — not a bare force-recreate alone. Misleading secondary symptom: mid-run inside a multi-package script this can surface as `echo: write error: Resource temporarily unavailable` on an unrelated line (a crashed `vitest` process breaking a downstream pipe), which looks like resource exhaustion, not `EROFS` — check the failing package standalone first. The overlay mechanism covers `.vite`/`.vite-temp` (Vite/Vitest) and `.astro` (Astro's own build cache, SMI-5722). [Details](.claude/development/git-crypt-guide.md#worktree-docker-bind-mounts-smi-4689) |
| Worktree container boot log shows `[repair] Could not link ... (non-fatal)` or `... missing/read-only — alias ... not linked` (SMI-5650) | **Not expected — indicates a stale override, not a cosmetic warning.** Post-SMI-5650 the `@skillsmith`/`@smith-horn` scope directories are writable tmpfs overlays that `repair-worktree-container-symlinks.sh` populates at every boot; a persistent warning means this worktree's `docker-compose.override.yml` predates SMI-5650 (no tmpfs mounts yet) or the container hasn't been recreated since the last regen. Fix: `./scripts/repair-worktrees.sh` from the main checkout, then `docker compose --profile dev up -d` in the worktree (recreate, not `restart` — new mounts only apply on recreate). A healthy boot instead logs `[repair] Repaired N ...` or `... already correct`. The same pre-SMI-5650 read-only root mount also blocked native-module self-heal (SMI-5351) for any workspace-local copy under `node_modules` — independently confirmed via SMI-5635 (`docker compose restart dev` not fixing a broken `better-sqlite3` binding) — fixed separately in SMI-5650 Wave 2 (native-module named-volume seeding); see git-crypt-guide.md's `SKILLSMITH_PRE_PUSH_HOST` entry for the pre-push implication while Wave 2 is still landing. **If `./scripts/repair-worktrees.sh` itself warns that this worktree's host-side `node_modules` is empty but could not be removed (SMI-5689/SMI-5685)**: an active container still holds a mount reference into that empty directory — run `docker compose --profile dev down` in that worktree first, then re-run `./scripts/repair-worktrees.sh` and recreate. |
| Worktree edits not reaching the container (stale `wc -l`, prettier "No files matching", stale test runs) | macOS bind-mount freeze — `docker cp <hostfile> <container>:/app/<path>` to sync (pre-commit reads host files, so commits are unaffected). **`docker cp` exit 0 is NOT proof it landed** (SMI-5569: silent no-op on a subset of a batch) — verify each file by content, `diff -q <hostfile> <(docker exec <container> cat /app/<path>)` (not `wc -l`; same-line-count `.snap` edits slip through), re-cp until clean, and run checks in ONE long-lived container, not `docker compose run --rm`. Background/subagent runs must diff too. Never redirect container output back into the host file (`docker exec <container> cat /app/<f> > <f>`): the container's `/app` is a live bind mount of the host tree, so the shell truncates it to 0 bytes before `cat` reads it, and the command still exits 0. Compare with `diff`/`cmp`, and copy one way with `docker cp`. And never `npm install` in a worktree container to "fix" a blip — it wipes native modules (`.npmrc` ignore-scripts); `docker compose --profile dev restart dev` self-heals (SMI-5351). [Details](.claude/development/git-crypt-guide.md#worktree-bind-mount-freeze-and-npm-install-native-wipe-smi-5375) |
| Docker Desktop hung on "Turning off the Docker Engine..." (SMI-5616/SMI-5750) | Host disk full — check `df -h / /System/Volumes/Data`. Force-quit (`pkill -9 -f "com.docker.backend"`), relaunch, then `docker system prune -a -f --volumes` to reclaim. [Details](.claude/development/docker-guide.md#docker-desktop-hung-on-turning-off-the-docker-engine-smi-5616smi-5750) |
| A `verify_jwt = false` admin function rejects a currently-valid `SUPABASE_SERVICE_ROLE_KEY`/`SUPABASE_ANON_KEY` bearer with 401 (SMI-6082) | Supabase's platform can inject the newer `sb_secret_*`/`sb_publishable_*` key format into Edge Functions' reserved env vars while local `varlock`/`.env` still holds the legacy `eyJ...` JWT — both are valid credentials for the same role, but the exact-string compare in `isServiceRoleCallerBySecretCompare()` fails across formats. See `docs/internal/runbooks/admin-complimentary-subscriptions.md`'s Troubleshooting section for the SHA-256 three-way digest-comparison recipe that confirms which side is stale before changing anything. |
| Docker DNS failure | `docker network prune -f` then restart |
| Stale CJS artifacts | `docker exec skillsmith-dev-1 bash -c 'find /app/packages -path "*/src/*.js" -not -path "*/node_modules/*" -not -path "*/dist/*" -type f -delete'` |
| Tool missing in `skillsmith-dev-1` (e.g. `psql: executable file not found`) after a Dockerfile change merged on `main` (SMI-4820) | Stale local image — your container predates the Dockerfile commit. `docker compose --profile dev down && docker compose --profile dev build --no-cache dev && docker compose --profile dev up -d`. `--no-cache` ensures the cached `dev` layer doesn't shadow new `RUN apt-get install` lines. |
| Orphaned agents | `./scripts/cleanup-orphans.sh` (`--dry-run` to preview) |
| `scripts/needle/dispatch.sh` refuses with exit 2, "already holds N open/in_progress bead(s)" (SMI-5847) | The target `--workspace` has leftover beads from an earlier interrupted/orphaned dispatch, or a dispatch that's still legitimately running concurrently into the same workspace (`bf`'s `claim_ttl_minutes` is 30, shorter than `dispatch.sh`'s own 3600s default timeout, so a long-running dispatch's own claim can expire mid-flight and look stale) — the refusal message names both causes and shows up to 5 beads. Close the stale ones (`bf close <id> --workspace <dir>`) or wait for the concurrent dispatch, then retry the identical dispatch; this is a pre-condition refusal, not a dispatch failure, so don't fall back to Claude-tier for it. One-off bypass (drains the stale beads at their own Codex cost): `SKILLSMITH_NEEDLE_STALE_BEAD_GUARD_DISABLE=1`. |
| Pushed a fix to a release-cadence branch but CI never triggers, PR shows a stale `headRefOid`, or `gh api .../pulls/<N>/commits` is empty (SMI-5663, recurring — 6 weeks running as of PR #1959) | The release-cadence PR was auto-closed + branch-deleted by the staleness watcher before your push; the push recreates the branch ref (`git` reports `[new branch]`) but GitHub does not sync commits or fire `pull_request` events against an already-`closed` PR. Run `gh pr reopen <N>` — it resyncs `headRefOid` to your new push and CI begins running normally. Root cause (SMI-5663) is still open/Backlog; this is the known recovery step, not a fix. |
| A PR's CI never triggers — `gh pr checks <N>` reports nothing, `gh api repos/.../commits/<sha>/check-runs` returns `total_count: 0`, across multiple pushes — but the PR is still OPEN (rules out SMI-5663 above, whose symptom is a closed/reopened PR with a stale `headRefOid`) (SMI-6458) | **Confirmed via GitHub's own docs, not just hypothesized**: "Workflows will not run on `pull_request` activity if the pull request has a merge conflict. The merge conflict must be resolved first." `on: pull_request` workflows check out a synthetic `refs/pull/<N>/merge` ref (a test-merge of head into base) — when GitHub can't construct that ref because of a genuine conflict, the workflow dispatch itself silently never happens: no error, no failed check, no check-suite at all. This is **not** the `required_status_checks.strict` branch-protection setting (a separate, unrelated merge-button gate — this repo has `strict: true`, but that only blocks the merge button, it doesn't explain missing check-suites). Diagnose with `gh pr view <N> --json mergeStateStatus,mergeable` — `DIRTY`/`CONFLICTING` (not `UNKNOWN`, which just means GitHub is still computing it) confirms this. In a repo with `docs/internal`-style gitlinks, the "conflict" is very often just the gitlink pointer having diverged from `main` (a normal "my own docs commits haven't merged yet" state, not real content conflict) — `git`'s default gitlink 3-way merge can't auto-resolve it. Fix: rebase onto current `main` (`./scripts/rebase-worktree.sh --allow-submodule-ahead=docs/internal <worktree-path>`) and force-push; checks begin scheduling normally within seconds of the mergeability state clearing. |
| `release-cadence.yml`'s Sunday scheduled trigger looks like it hasn't fired yet (SMI-6052) | GitHub's own best-effort scheduling under this repo's dense Sunday cron cluster has historically delayed this trigger 20min–4.5h past 03:17 UTC — before manually dispatching (`gh workflow run release-cadence.yml -f dry_run=false`) because it "looks skipped," run `gh run list --workflow=release-cadence.yml` first to confirm nothing is already in flight. A branch/PR-collision guard (SMI-6052) now makes a subsequently-late-arriving scheduled run detect an already-opened same-day PR and exit cleanly instead of failing on a non-fast-forward push; the `release-cadence-heartbeat-monitor` Supabase pg_cron backstop (daily 12:50 UTC) separately alerts if no qualifying successful run lands within 8 days, independent of GitHub Actions' own scheduling. [Details](docs/internal/implementation/smi-6052-release-cadence-scheduled-run-collision.md) |
| `publish.yml`'s `Verify X on npm` step failed (SMI-6493) | Since SMI-6493 the budget is 30 x 10s (~300s) plus a final authoritative probe, sized against npm's publish-time malware scan ("typically around five minutes", [GitHub changelog 2026-07-28](https://github.blog/changelog/2026-07-28-npm-publish-time-malware-scanning-and-dual-use-metadata/)). A failure means **this runner did not observe the version within ~5 min** — which is not the same as npmjs not having it. The probe pins `--no-json --offline=false --prefer-offline=false` against `NPM_CONFIG_*` settings that would otherwise guarantee a false red, but a registry outage, proxy, DNS, auth failure or a redirected registry can still stop it observing a package that is genuinely live. So **read the diagnostic before concluding anything**: the final probe runs with stderr un-redirected, so npm's own output appears in the step log immediately after the `Final probe for <pkg>@<version>` line and before the `::error::`. Since SMI-6497 all four verify steps run one shared helper (`scripts/ci/verify-npm-publish.sh`), so that log text is identical across them and the budget policy is written down in exactly one place -- read the helper, not the workflow, when the question is *what the step actually does*. For `skillsmith-cli` only, verifying and smoking are now two separate steps: an `::error::` about npm visibility belongs to `Verify skillsmith-cli on npm`, while an `npx` failure appears under `Smoke the npx wrapper` and is non-fatal by design (it warns and exits 0, because the publish is already verified live and cannot be un-published). If that region is empty, npm produced no diagnostic at all — treat that as a registry/network problem rather than assuming lag. An `E404 No match found for version` **from the workflow** means npmjs does not have that version — which is **two** possibilities, not one: real propagation lag beyond the budget (check <https://status.npmjs.org>), **or a publish that went somewhere else entirely**. The probes pin `--registry=https://registry.npmjs.org` but the four `npm publish` commands do not, so a redirected `NPM_CONFIG_REGISTRY` would publish elsewhere and the pinned probe would correctly report npmjs as empty. Before assuming lag, confirm where the publish actually went — check the publish step's own log for the registry it reported, and `npm view <pkg> version --registry=https://registry.npmjs.org` from a laptop. An E404 from **your own laptop** may instead be a redirected `NPM_CONFIG_REGISTRY` or user `.npmrc`, which is why the laptop command above pins the registry too; an `ENEEDAUTH` / `E403` / DNS / 5xx means an infrastructure or credential problem, not lag — before SMI-6493 all four collapsed into an indistinguishable empty string. **Do not reflexively re-dispatch `publish.yml`**: `npm publish` is irreversible and npm rejects a duplicate version, so confirm the real state first with `npm view <pkg> version --no-json --offline=false --prefer-offline=false --registry=https://registry.npmjs.org` from a laptop (every flag matters on your own machine: a local `NPM_CONFIG_JSON`/`OFFLINE`/`PREFER_OFFLINE` can make a live package look absent, and a `NPM_CONFIG_REGISTRY` or user `.npmrc` aimed at a private or stale proxy returns E404 for a version that is live on npmjs — which this row would otherwise have you read as propagation lag). Finally, check whether the red run suppressed `mirror-mcp-server.yml`'s sync — it gates on the *whole* `publish.yml` run concluding `success` and does **not** self-heal (SMI-6499); repair with `gh workflow run mirror-mcp-server.yml` and confirm the mirror HEAD's `Source-Version` trailer matches npm. [Details](docs/internal/implementation/smi-6493-publish-verify-retry-budget.md) |
| `StuckLockError` on `~/.skillsmith/config.json.lock` or `~/.skillsmith/audits/security-acceptance.json.lock` (SMI-5883) | `acquireOwnedLock`'s two-level lock gave up — the error names `reason` (`held`/`unreclaimable_legacy`/`unreclaimable_unparseable`/`reclaim_unavailable`/`reclaim_disabled`), a remedy specific to that reason, and both file paths. The message never says whether a retry will help unless that is determined, and for all but one reason it is not — the deciding fact is one `reason` does not carry, so the remedy names that fact rather than guessing (SMI-6764). Read the remedy, not the opening verb — the verb is the same for every reason. **`held` does not mean the holder was confirmed alive**: it is also the safe default when the liveness probe never ran (any `timeoutMs` below `RECLAIM_PROBE_AFTER_MS`), and it is what a claim naming another host, or a pid too malformed to probe, classifies as — neither of which is ever auto-reclaimed, so those two do not clear by retrying. Manual unstick: confirm no skillsmith process is running (`ps -ax \| grep -E '[s]killsmith\|[s]klx'`), inspect read-only (`cat <lockPath>` and, only if named, `cat <reclaimPath>`), then remove ONLY the file(s) the error named. A legacy (pre-v1 bare-PID) or unparseable claim is NEVER auto-reclaimed by design (D-5) even when definitely dead — this is expected, not a bug. Disable auto-reclaim entirely (degrades to strict, no-reclaim): `SKILLSMITH_LOCK_NO_AUTO_RECLAIM=1`. |
| The `ruflo` MCP server fails to connect and its `/mcp` log shows a `[ruflo] guard:` line with an exit code (SMI-6744, ADR-170 § 4) | `scripts/mcp-ruflo-launcher.sh` ran `scripts/ruflo-launch-guard.mjs` inside `skillsmith-ruflo-1` before the spawn and it refused; the line names the check and, for codes 3–5, ends with the recovery command. **Read the code, not the verb.** `1` **two causes, named in the message**: a writability probe failed on `/srv/ruflo`, its `.claude-flow/policy` or `.swarm` (permissions or a full or read-only volume) → check the mount with `docker inspect skillsmith-ruflo-1`; **or** `getconf CLK_TCK` measured something other than the assumed `USER_HZ=100` (or could not be measured at all) → fail-closed by design, file a Linear issue rather than changing the constant. `2` the served entrypoint's realpath is not the pinned literal → recreate the service (`./scripts/ruflo-service-up.sh` after `docker rm -f skillsmith-ruflo-1`). `3` another launcher holds the guard's mutex (a SQLite `BEGIN IMMEDIATE` on `state.lock.launcher.db`, a kernel lock the holder's death releases) past its 3 s busy timeout → retry; nothing to delete, ever — the message names the last recorded holder pid. `4` that mutex database is unusable (not a database, a directory) → remove ONLY `/srv/ruflo/.claude-flow/policy/state.lock.launcher.db`, which holds no state; never `state.lock` or `state.json`. `5` the runtime's own `state.lock` is held by a live server → another session is mid-transaction; retry. The guard never deletes the runtime's lock: the runtime writes it as `{pid, acquiredAt}` and clears one older than 30 s on its next acquire, so a stale lock makes the guard wait out the remainder of that window (at most 30 s, printed) and a malformed one only prints a warning (code 6 is retired). `7` is a bug in the guard, not a refusal — file it. Full table and the six launcher checks: [claude-flow-guide.md § Launcher](.claude/development/claude-flow-guide.md#launcher). Kill switch: `SKILLSMITH_RUFLO_LAUNCHER_DISABLE=1` makes the launcher refuse outright — there is no npx fallback by design (SMI-6744 Wave 4), so it silences the server, it does not bypass the checks; registered in [guards-and-opt-outs.md](docs/internal/process/guards-and-opt-outs.md). |
| Symlink outside skills root (SMI-4287) | Set `allowSymlinksOutsideRoot: true` in `LocalFilesystemConfig` to opt in |
| Session-start audit unexpected stderr (SMI-4590) | `export SKILLSMITH_SESSION_AUDIT_DISABLE=1`. Logs: `~/.skillsmith/logs/session-audit-<date>.log` |
| Session-priming banner shows a reindex failure/anomaly/hung warning | Read `~/.skillsmith/logs/skillsmith-doc-retrieval-<date>.jsonl` for the exact error → for a zero-touch anomaly, verify with a manual `docker exec skillsmith-dev-1 node packages/doc-retrieval-mcp/dist/src/cli.js reindex --full` → disable with `SKILLSMITH_REINDEX_STALENESS_DISABLE=1` if a known false positive (SMI-5786 follow-up) |
| Session-priming banner shows a `[ruflo-bridge]` warning (SMI-6744 A5.5.2 delta) | **The banner's first reader is a model — relay it to the user rather than silently acting on it or suppressing it**, the same standing instruction as the MCP-disconnect rule above. Read the line itself first: it always names a next action and a log path (`~/.skillsmith/logs/ruflo-bridge-probe-<date>.log`). `bridge degraded: embeddingBackend '<x>'` → run `npx --no-install tsx scripts/ruflo-bridge-probe.mjs` (**not** bare `node`: the probe imports a sibling by a `.js` specifier that only a TypeScript loader resolves, so `node` exits 1 with `ERR_MODULE_NOT_FOUND` every time, for everyone — SMI-7032), then `/mcp` → `ruflo` → Reconnect — a real status change, not a stale read. `probe could not reach the served server: <reason>` → the writer could not even ask; run `./scripts/ruflo-service-up.sh`, then reconnect — check `docker ps --filter name=skillsmith-ruflo-1` first. `verdict not evaluated: state missing` / `state malformed at <path>` / `state unreadable (<detail>)` → the writer never fired, or its state file is corrupt or unreadable, for this checkout's key — run the probe command by hand and re-check. **This renders immediately and unconditionally.** There is no grace window: SMI-6985 deleted it after its install-date anchor proved renewable three times (file mtime by any checkout, `%cI` by any rebase, `%aI` by a shallow fetch), so a fresh checkout that has never run the probe reports this on its first session and the line names the command to run. `verdict not evaluated: payload malformed at <path>` → the payload itself, not the state file — re-run the verdict script named in the line. `verdict not evaluated: embeddingBackend '<x>' is outside the table pinned at DERIVED_FROM <ver> — treat as FAIL` → upstream returned a value the pinned table does not know; **do not add it to the table to clear the banner** — that is the silent-pass this guard exists to prevent. `bridge UNVERIFIED: <reason>` → identity or freshness could not be corroborated; inspect the store and authority file, then re-run the probe. `verdict not evaluated: the host repo key could not be resolved, so no verdict could be read` → the reader could not identify this checkout's main repo, so it has no key to look up — check you are in a git tree and that `git worktree list` works; a bare or broken checkout is the usual cause. `verdict not evaluated: the bridge-verdict reader failed: <msg>` → the reader itself threw; the message names what. Both of these are fault lines from the consumer rather than verdicts from the writer — they render instead of vanishing BY DESIGN, since a reader that cannot do its job must say so. `verdict stale (Nh)` → the last successful probe is older than `SKILLSMITH_RUFLO_VERDICT_STALE_HOURS` (default 48h). Post-merge is this writer's only trigger (deliberately, owner-decided 2026-10-03), so a stale reading after a quiet week of no merges is expected. **It is not benign when merges HAVE happened in that window** — the writer should have fired and did not, so check whether `.husky/post-merge` ran at all, since a squash-merge pulled in via `sync-main.sh`'s fetch-and-reset never fires it. Refresh with the `tsx` invocation above, never bare `node`. `no learning recorded in N consecutive probes` → something recorded at least once and has stopped. This is the arm's **only** shape: SMI-6985 re-gated it on `everLearned`, the counters' own persisted history, so a connected bridge with a non-empty store no longer arms it on its own. **As of 2026-10-04 nothing in this repository calls the trajectory hooks at all**, so this should not fire until a trajectory writer exists — if it does, find out what started writing and then stopped. `learning counters regressed: … decreased since the last probe` → the store was wiped or reset; check whether that was deliberate. Disable the banner only (writer keeps running): `SKILLSMITH_RUFLO_VERDICT_DISABLE=1`. Disable the writer entirely: `SKILLSMITH_RUFLO_PROBE_DISABLE=1`. Full design: [smi-6744-bridge-verdict-consumer.md](docs/internal/implementation/smi-6744-bridge-verdict-consumer.md). |
| Strategy submodule uninitialized | Empty `.claude/{skills,plans,hive-mind}/` mount-points are expected for external contributors. Skillsmith team members: `git submodule update --init .claude/skills .claude/plans .claude/hive-mind` (each pinned to its own branch in `smith-horn/skillsmith-strategy` per shape b′; no extra setup script). |
| Local typecheck/vitest fails with missing \`marked\`/\`sanitize-html\`/\`@types/sanitize-html\` (TS2688 / "Cannot find package"), or any package a workspace declares but a command can't resolve (SMI-6006 correction) | First, don't diagnose via \`require.resolve('PKG')\` (or checking \`node_modules/PKG\` directly) from the repo root — npm may legitimately place a dependency NESTED under its owning workspace (\`packages/PKG/node_modules/DEP\`) rather than hoisted to root, and Node's resolution only walks UP from the requiring file's own directory. Check from inside the owning package, or trust \`npm ls DEP --workspace=packages/PKG\`. Three separate \`node_modules\` trees exist and can each drift independently: (1) the HOST tree (\`\$REPO_ROOT/node_modules\` on macOS) — guarded by \`scripts/lib/check-node-modules-fresh.sh\` (SMI-5343/5344), warns on commit, blocks on push; fix with a plain \`npm install\` run directly on the HOST in the MAIN checkout (never inside any container). (2) \`skillsmith-dev-1\`'s own container-internal \`node_modules\` (Docker named volumes, disconnected from the host tree) — **self-heals automatically** on \`git push\` from the main checkout, whenever Docker's up and the opt-out isn't set (SMI-6006, \`scripts/lib/check-container-deps-fresh.sh\`) — but **blocks instead of installing** (exit 5) when any of the container's node_modules paths — root or a \`packages/*/node_modules\` (round-2b widened this from a root-only check) — isn't currently mounted with a volume-shaped root (SMI-6516/SMI-6520/SMI-6614, ADR-158; the container has no way to prove a mount is Docker/Podman-managed, only that its root is shaped like one) — installing against a detached or otherwise-suspect mount would write into that path's HOST tree instead; the block names the exact affected path(s), the recreate command, and the verify command. Manual fix if needed (mount confirmed healthy, npm itself failed inside the container): \`( cd /path/to/main-checkout && ./scripts/regen-lockfile.sh )\` — never an ungated container \`npm install\`, which is the exact unattended-install hazard SMI-6614 removed; \`regen-lockfile.sh\` runs the same mount-gated install itself. (3) Every WORKTREE container reads the HOST tree (1) read-only (\`:ro\` mount) — it has no independent state and can NEVER self-heal by running \`npm install\` inside it (\`EROFS\` by design, SMI-5560/5626); the only fix for a worktree-container symptom is step (1)'s host-side install, which then propagates to every worktree automatically. Never run \`npm install\`, \`npm audit fix\`, \`npm update\`, or any other lockfile/dependency-tree-mutating command via \`docker exec\` into a worktree container — this is not merely ineffective but can be actively dangerous: confirmed empirically (SMI-6378, 2026-09-03), running \`npm audit fix\` inside a worktree container produced \`npm warn reify Removing non-directory /app/node_modules\` followed by an ENOENT, breaking the container's own view of \`/app/node_modules\` (\`ls /app/node_modules\` → "No such file or directory") until a \`docker restart\` recovered it — npm's reify step raced against the read-only-by-design bind mount mid-transaction. Only read-only commands (test/lint/typecheck/build) are safe inside a worktree container; all dependency-tree mutations happen on the HOST, in the MAIN checkout (or a scratch worktree via \`create-worktree.sh\`, per the SMI-6034 ACL row's convention below). Bypass a false positive: \`SKILLSMITH_SKIP_DEPS_FRESHNESS=1\` (host guard) or \`SKILLSMITH_SKIP_CONTAINER_DEPS_FRESHNESS=1\` (container self-heal). |
| MCP server logs `Failed to close database on shutdown` | Non-fatal — the process still exits cleanly, but recently-installed skills' dependency metadata may not be persisted. If this recurs, check disk space and file permissions on `~/.skillsmith/skills.db`. The error is logged (not silent) specifically so this is diagnosable. See SMI-5639. |
| The `Edge Functions Typecheck` CI job fails, or `bash scripts/ci/typecheck-edge-functions.sh` fails locally (SMI-6897) | **Read the `RESULT` line before the `VERDICT` line — they are different questions.** `RESULT: EVALUATED` means the check ran and `VERDICT` is about your code: `FAIL` names the new or increased errors, and the fix is to fix them. `RESULT: INCONCLUSIVE` means the check **could not run** and the verdict says nothing about your code — **the gate names its own cause on the `RESULT` line and prints a `next:` action for it — read those two rather than guessing from this row**, because there are around eighteen distinct causes and any list here would go stale (the plan's exit-policy table is the enumeration). They fall into four kinds: the checker could not run (no deno, no `supabase/deno.json`, a git-crypt-locked tree), it ran but its output cannot be trusted (module-graph error, failed remote fetch, unparseable total, an attribution that did not reconcile, a diagnostic whose first location is a remote URL), its **exit status contradicts its output** (deno exits 1 for type errors and 0 for none, so any other pairing means it died after emitting them), or the **file set is wrong** (zero discovered, everything excluded, an unknown TypeScript-like extension, or a `.js`/`.mjs`/`.cjs`/`.jsx` file deno runs but does not type-check). It exits non-zero under `required-ci` and `pre-deploy` on purpose: "not checked" is not "safe". **A fork PR excuses exactly one cause and no others** — a locked tree, which a fork structurally cannot decrypt. Every other cause fails closed there too, and CI picks the fork context from GitHub's own `pull_request.head.repo.fork`, never from whether a key happens to be present. **If you legitimately fixed errors**, lower the baseline with `bash scripts/ci/typecheck-edge-functions.sh --update`. That is a **ratchet** — it refuses to add a row or raise a count, so it cannot be used to accept a regression. **If you renamed a baselined file**, the gate reports a new error in the new path plus a `gone` line for the old one and cannot tell a rename from a regression (SMI-6704 R10): edit `supabase/functions/typecheck-baseline.tsv` by hand and say so in the commit. **Do not run `deno` inside the dev container** — it is not installed there; this is a host-side / CI check. [Details](docs/internal/implementation/smi-6897-supabase-functions-typecheck-gate.md) |
| `skillsmith-doc-retrieval` shows `Failed to reconnect` or `MCP error -32603: schema._def.shape is not a function` in `/mcp` | `scripts/mcp-doc-retrieval-launcher.sh` (SMI-5718) now guards this — expand the failing entry for actionable stderr naming the exact state (container not running, corrupt/missing dependency incl. a stale nested `zod` — the SMI-5452 hazard — or root-hoisted `zod-to-json-schema`) and the exact remediation command. If you still see the raw `-32603` error, the launcher's code-level backstop in `jsonSchemaOf()` (`packages/doc-retrieval-mcp/src/server.ts`) also throws a diagnosable `[doc-retrieval]`-tagged message instead of the opaque native error — check the MCP host's per-server log for it. |

**Detailed diagnostics**: [docker-guide.md](.claude/development/docker-guide.md#troubleshooting).

---

## Key References

- **Architecture**: [API Architecture (as-built)](docs/internal/architecture/system-design/api-architecture.md), [Skill Dependencies](docs/internal/architecture/system-design/skill-dependencies.md), [Index](docs/internal/architecture/index.md). Historical (Dec-2025 design, superseded): [System Overview](docs/internal/architecture/system-design/system-overview.md)
- **Standards**: [Engineering](docs/internal/architecture/standards.md), [DB](docs/internal/architecture/standards-database.md), [Astro](docs/internal/architecture/standards-astro.md), [Security](docs/internal/architecture/standards-security.md)
- **Process**: [Context Compaction](docs/internal/process/context-compaction.md), [Linear Hygiene](docs/internal/process/linear-hygiene-guide.md), [Wave Checklist](docs/internal/process/wave-completion-checklist.md)
- **Testing**: [Stripe](.claude/development/stripe-testing.md), [Neural](.claude/development/neural-testing.md)
- **Billing**: [Admin Grants](docs/internal/runbooks/admin-complimentary-subscriptions.md), [Stripe Ops](docs/internal/runbooks/stripe-operations.md)
- **Website**: [skillsmith.app/docs](https://skillsmith.app/docs); deploy `cd packages/website && vercel --prod`

**Linear**: Skillsmith initiative (SMI-xxx). Authoritative standards: `docs/internal/architecture/standards.md`.

---

## Infrastructure Change Policy (ADR-109)

Changes to Docker, CI, entrypoints, hooks, or dev tooling scripts **require SPARC + plan-review before implementation**. Use `/launchpad --infra` (auto-detected) or run SPARC research manually → `docs/internal/implementation/{slug}.md` → plan-review skill → implement.

Trigger paths: `docker-entrypoint.sh`, `Dockerfile`, `docker-compose.yml`, `.github/workflows/`, `.husky/`, `scripts/` (CI/hook files), `vitest.config.ts`, `turbo.json`, `lint-staged.config.js`.

Application code (`packages/*/src/**`) and docs do not require this. See [ADR-109](docs/internal/adr/109-sparc-plan-review-for-infra-changes.md).

**Architecture decisions get their own ADR (standing rule since 2026-08-24, prospective only).** A plan doc capturing *what* changed isn't the same as an ADR capturing *why one structurally different approach was chosen over another*. When a plan's own architecture decision is genuine (not just an implementation detail), flag it and ask whether it warrants a dedicated `docs/internal/adr/` entry — `plan-review-skill`'s VP Engineering rubric checks for this.

---

## Important Instruction Reminders

**If you are a dispatched subagent** (spawned via the Agent/Task tool) and your own task prompt says not to commit, push, open/merge a PR, or touch Linear: that scoping wins over every "after every commit"/"after every PR merge" instruction below, no exceptions. Those blanket instructions bind the top-level coordinating session only. Hand your work back to the dispatcher instead.

**If a system-reminder reports that the `skillsmith` or `skillsmith-doc-retrieval` MCP server disconnected** (e.g. "N deferred tools are no longer available (MCP server disconnected)"): treat this as always directly relevant, never as suppressible ambient context — surface it to the user in your very next response, name which server dropped, and tell them to run `/mcp` → select that server → Reconnect. This is a best-effort backstop for the case where the tool vanishes from your toolset before any call is attempted (SMI-5941) — the underlying container can be perfectly healthy the entire time, and nothing else in the harness will tell the user unless you do.

Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one.
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
Never save working files, text/mds and tests to the root folder.
NEVER defer fixes to "later" or "a future pass". If a code review or audit surfaces an issue in scope, fix it immediately in the same PR. Do not label findings as "informational" or "non-blocking" if they can be resolved now.
NEVER say "worth a note for next time" or "consider X in future". If something is worth noting, act on it immediately: create the Linear issue, update the doc, fix the config. Observations without immediate action are noise.
After context compaction or session continuation, ALWAYS verify claimed-complete work by reading the actual files before proceeding. Never trust the summary alone — compaction can conflate "planned" with "implemented".
After EVERY commit, run `/governance` to review the changed code. Resolve ALL issues it surfaces before pushing. No exceptions — do not skip, defer, or downgrade findings.
**The one carve-out: a prose-only commit skips `/governance`.** It reviews changed *code*; a commit with none yields a finding-free report that reads as coverage it never gave. **Take the carve-out only when every changed file ends in `.md` and merely describes existing behaviour for a human reader.** If a file prescribes behaviour, serves as a template or specification, or its classification is uncertain, run `/governance`. Two exclusions, and they are different rules: these paths are always excluded whatever they contain — `CLAUDE.md`, `.claude/{skills,agents,commands,templates}/**`, `.github/PULL_REQUEST_TEMPLATE.md`; and separately, exclude any other `.md` that a script, hook, workflow or agent reads as input. A commit is also not prose-only if its prose is generated by, consumed by, or semantically coupled to code elsewhere in the same branch or PR — splitting a change across two commits does not buy the exemption. **Do not settle this by watching the push hook**: its positive allowlist admits files of any type under `docs/`, `.claude/development/`, `.claude/templates/` and `.github/ISSUE_TEMPLATE/` — executable scripts included — except paths vetoed under `tests/` or `fixtures/` (SMI-6620). State the skip in the commit that takes it — an unrecorded exception to a no-exceptions rule is indistinguishable from ignoring it (SMI-6617). For the post-merge retro below, skip only when every file in the whole PR diff meets this definition.
After EVERY commit, update the relevant Linear issue(s) in the Skillsmith initiative (SMI-xxx) to reflect progress. Add a comment with the commit SHA and a brief summary of what changed. Move the issue status forward if the commit completes the work (e.g., "In Progress" → "Done"). If no Linear issue exists for the work, create one under the appropriate project before pushing.
Before every `gh pr create`, use the `pr-description` skill — PR bodies lead with a plain-language Business Summary, not technical detail first.
Before merging any PR, run the `pr-reviewer` skill's full pre-merge check gate, dispatched to a model from a different family than whatever wrote the PR — for Claude-authored work, that means GPT-5.6-Sol via `scripts/needle/dispatch.sh` (ADR-128), never the skill's own default same-family Claude subagent path. Resolve every finding (zero-deferral) before merging; note any check as SKIP only when its trigger condition genuinely isn't present in the diff.
**Carve-out: a pointer-bump or docs-only PR skips this gate.** Those checks read a *code* diff — migrations, RLS, exports, enums, upsert callers, edge-function surfaces — so a PR carrying none presents no trigger for any of them, and running it yields an all-SKIP report that reads as coverage it never gave, the same defect the prose-only carve-out above exists to avoid. **Take it only when every changed path in the PR diff is either a `.md` file or a submodule gitlink whose registered range touches no executable file** — and check the range, not the gitlink: `git -C <submodule> diff --name-only <old>..<new>` must list no `.sh`, `.ts`, `.mjs`, `.sql`, or workflow YAML. A `.claude/skills` bump is never exempt, matching that carve-out's own always-excluded paths; a `docs/internal` bump normally is. Its `.md` test cannot settle this on its own, because a gitlink is not a `.md` file and its contents are opaque at the diff — a real PR hit exactly that gap (SMI-6701, PR #2867). State the skip in the PR body.
After EVERY PR is merged, run `/governance` as a retrospective on the full PR diff. Resolve ALL issues it surfaces immediately — create follow-up commits or Linear issues as needed. Do not close the session until the retro is clean.
After the governance retro, update any `index.md` files in directories where files were added or removed during the PR. Check `docs/internal/`, `.claude/development/`, and `.claude/templates/`. If the root `docs/internal/index.md` folder counts have drifted, update those too. **Partial machine coverage exists for exactly one of those**: `.github/workflows/docs-folder-index.yml` (SMI-4932) fails any PR that touches `docs/internal/**` or `.gitmodules` while the root `docs/internal/index.md` "Folder Reference" table is stale — so the folder-count half of this line is caught for you on the pointer-bump PR. Nothing checks `.claude/development/index.md`, `.claude/templates/`, or per-subdirectory `index.md` files; those stay manual.

**Enforcement status of the reminders above (SMI-6407 audit, 2026-09-05).** Everything from "After EVERY commit, run `/governance`" through this line is **enforced only by prose — no hook, no CI check, nothing observes whether it happened.** So is "Never save working files, text/mds and tests to the root folder," the zero-deferral rule, and the branch-naming rule under § Branch Management. That is a known gap, not an oversight: the migration candidates and their proposed enforcement surfaces are laid out in [smi-6407-claude-md-memory-governance-enforcement-migration.md](docs/internal/implementation/smi-6407-claude-md-memory-governance-enforcement-migration.md), each of which is an ADR-109 infra change requiring SPARC + plan-review before implementation. Until one of those lands, these lines bind by recall alone — treat them accordingly rather than assuming a guard would have caught a miss.
