diff --git a/.agents/skills/develop b/.agents/skills/develop new file mode 120000 index 0000000..6b6a7d0 --- /dev/null +++ b/.agents/skills/develop @@ -0,0 +1 @@ +../../plugins/ndif/skills/develop \ No newline at end of file diff --git a/.agents/skills/ndif-develop b/.agents/skills/ndif-develop deleted file mode 120000 index 2d05536..0000000 --- a/.agents/skills/ndif-develop +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-develop \ No newline at end of file diff --git a/.agents/skills/ndif-operate b/.agents/skills/ndif-operate deleted file mode 120000 index 1eb940f..0000000 --- a/.agents/skills/ndif-operate +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-operate \ No newline at end of file diff --git a/.agents/skills/ndif-selfhost b/.agents/skills/ndif-selfhost deleted file mode 120000 index 13a518d..0000000 --- a/.agents/skills/ndif-selfhost +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-selfhost \ No newline at end of file diff --git a/.agents/skills/ndif-troubleshoot b/.agents/skills/ndif-troubleshoot deleted file mode 120000 index 1177bd3..0000000 --- a/.agents/skills/ndif-troubleshoot +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-troubleshoot \ No newline at end of file diff --git a/.agents/skills/operate b/.agents/skills/operate new file mode 120000 index 0000000..85d3735 --- /dev/null +++ b/.agents/skills/operate @@ -0,0 +1 @@ +../../plugins/ndif/skills/operate \ No newline at end of file diff --git a/.agents/skills/selfhost b/.agents/skills/selfhost new file mode 120000 index 0000000..8f55966 --- /dev/null +++ b/.agents/skills/selfhost @@ -0,0 +1 @@ +../../plugins/ndif/skills/selfhost \ No newline at end of file diff --git a/.agents/skills/troubleshoot b/.agents/skills/troubleshoot new file mode 120000 index 0000000..2754ab0 --- /dev/null +++ b/.agents/skills/troubleshoot @@ -0,0 +1 @@ +../../plugins/ndif/skills/troubleshoot \ No newline at end of file diff --git a/.codex/skills/develop b/.codex/skills/develop new file mode 120000 index 0000000..6b6a7d0 --- /dev/null +++ b/.codex/skills/develop @@ -0,0 +1 @@ +../../plugins/ndif/skills/develop \ No newline at end of file diff --git a/.codex/skills/ndif-develop b/.codex/skills/ndif-develop deleted file mode 120000 index 2d05536..0000000 --- a/.codex/skills/ndif-develop +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-develop \ No newline at end of file diff --git a/.codex/skills/ndif-operate b/.codex/skills/ndif-operate deleted file mode 120000 index 1eb940f..0000000 --- a/.codex/skills/ndif-operate +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-operate \ No newline at end of file diff --git a/.codex/skills/ndif-selfhost b/.codex/skills/ndif-selfhost deleted file mode 120000 index 13a518d..0000000 --- a/.codex/skills/ndif-selfhost +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-selfhost \ No newline at end of file diff --git a/.codex/skills/ndif-troubleshoot b/.codex/skills/ndif-troubleshoot deleted file mode 120000 index 1177bd3..0000000 --- a/.codex/skills/ndif-troubleshoot +++ /dev/null @@ -1 +0,0 @@ -../../plugins/ndif/skills/ndif-troubleshoot \ No newline at end of file diff --git a/.codex/skills/operate b/.codex/skills/operate new file mode 120000 index 0000000..85d3735 --- /dev/null +++ b/.codex/skills/operate @@ -0,0 +1 @@ +../../plugins/ndif/skills/operate \ No newline at end of file diff --git a/.codex/skills/selfhost b/.codex/skills/selfhost new file mode 120000 index 0000000..8f55966 --- /dev/null +++ b/.codex/skills/selfhost @@ -0,0 +1 @@ +../../plugins/ndif/skills/selfhost \ No newline at end of file diff --git a/.codex/skills/troubleshoot b/.codex/skills/troubleshoot new file mode 120000 index 0000000..2754ab0 --- /dev/null +++ b/.codex/skills/troubleshoot @@ -0,0 +1 @@ +../../plugins/ndif/skills/troubleshoot \ No newline at end of file diff --git a/README.md b/README.md index 4702228..470ee08 100644 --- a/README.md +++ b/README.md @@ -113,10 +113,10 @@ you want.) | Skill | Use when... | | --- | --- | -| `ndif-selfhost` | Standing a server up: the published `ndif/ndif` image, the compose dev stack, or a from-source `ndif start`. Prerequisites, tags, ports, volumes, configuration, and the first remote trace. | -| `ndif-operate` | Running models on it: deploy, evict, pin, scale, `models.yaml`, sizing and padding, HOT/WARM/COLD, the dashboard, telemetry, turning on auth. | -| `ndif-troubleshoot` | It won't start, requests hang, a deploy OOMs, a result won't download, versions disagree — symptom to cause to fix, and where the logs actually are. | -| `ndif-develop` | Changing the server itself: the request lifecycle, the process map, trusted vs untrusted execution, the model-actor hooks, the test suite, release mechanics. | +| `selfhost` | Standing a server up: the published `ndif/ndif` image, the compose dev stack, or a from-source `ndif start`. Prerequisites, tags, ports, volumes, configuration, and the first remote trace. | +| `operate` | Running models on it: deploy, evict, pin, scale, `models.yaml`, sizing and padding, HOT/WARM/COLD, the dashboard, telemetry, turning on auth. | +| `troubleshoot` | It won't start, requests hang, a deploy OOMs, a result won't download, versions disagree — symptom to cause to fix, and where the logs actually are. | +| `develop` | Changing the server itself: the request lifecycle, the process map, trusted vs untrusted execution, the model-actor hooks, the test suite, release mechanics. | These skills document a server, so — unlike the nnsight ones — their code blocks are reference material rather than executed examples. @@ -212,10 +212,10 @@ skills/ │ └── ndif/ │ ├── .claude-plugin/plugin.json │ └── skills/ -│ ├── ndif-selfhost/ -│ ├── ndif-operate/ -│ ├── ndif-troubleshoot/ -│ └── ndif-develop/ +│ ├── selfhost/ +│ ├── operate/ +│ ├── troubleshoot/ +│ └── develop/ ├── tests/ # executes every code block └── Makefile ``` diff --git a/plugins/ndif/skills/ndif-develop/SKILL.md b/plugins/ndif/skills/develop/SKILL.md similarity index 98% rename from plugins/ndif/skills/ndif-develop/SKILL.md rename to plugins/ndif/skills/develop/SKILL.md index cb9bcf0..d579738 100644 --- a/plugins/ndif/skills/ndif-develop/SKILL.md +++ b/plugins/ndif/skills/develop/SKILL.md @@ -1,6 +1,6 @@ --- -name: ndif-develop -description: Change NDIF's own server code. Use when working inside the ndif repo — "how does an NDIF request actually run", "where do I add an endpoint / a model actor / a provider / a CLI command", "what is the dispatcher", "trusted vs untrusted execution", "the sandbox runner", "how do I test NDIF", "how do I force the untrusted path", "how is nnsight pinned", "how do I cut an NDIF release" — or when reading a traceback from NDIF's internals rather than from a user's block. Covers the repo layout, the process map, the request lifecycle, the five model-actor hooks, the live-server test suite (CI runs none of it), and the tag-driven publish workflows. For running a server rather than changing one, use `ndif-selfhost`, `ndif-operate` and `ndif-troubleshoot`. +name: develop +description: Change NDIF's own server code. Use when working inside the ndif repo — "how does an NDIF request actually run", "where do I add an endpoint / a model actor / a provider / a CLI command", "what is the dispatcher", "trusted vs untrusted execution", "the sandbox runner", "how do I test NDIF", "how do I force the untrusted path", "how is nnsight pinned", "how do I cut an NDIF release" — or when reading a traceback from NDIF's internals rather than from a user's block. Covers the repo layout, the process map, the request lifecycle, the five model-actor hooks, the live-server test suite (CI runs none of it), and the tag-driven publish workflows. For running a server rather than changing one, use `selfhost`, `operate` and `troubleshoot`. --- # Developing NDIF @@ -329,6 +329,6 @@ that is the most common drift in the repo. ## Related skills -- `ndif-selfhost` — getting a dev stack up. -- `ndif-troubleshoot` — reading the logs a change produces. -- `ndif-operate` — the operator surface your change has to keep working. +- `selfhost` — getting a dev stack up. +- `troubleshoot` — reading the logs a change produces. +- `operate` — the operator surface your change has to keep working. diff --git a/plugins/ndif/skills/ndif-develop/references/extending.md b/plugins/ndif/skills/develop/references/extending.md similarity index 100% rename from plugins/ndif/skills/ndif-develop/references/extending.md rename to plugins/ndif/skills/develop/references/extending.md diff --git a/plugins/ndif/skills/ndif-develop/references/request-lifecycle.md b/plugins/ndif/skills/develop/references/request-lifecycle.md similarity index 100% rename from plugins/ndif/skills/ndif-develop/references/request-lifecycle.md rename to plugins/ndif/skills/develop/references/request-lifecycle.md diff --git a/plugins/ndif/skills/ndif-operate/SKILL.md b/plugins/ndif/skills/operate/SKILL.md similarity index 98% rename from plugins/ndif/skills/ndif-operate/SKILL.md rename to plugins/ndif/skills/operate/SKILL.md index 38b53e3..56cf934 100644 --- a/plugins/ndif/skills/ndif-operate/SKILL.md +++ b/plugins/ndif/skills/operate/SKILL.md @@ -1,6 +1,6 @@ --- -name: ndif-operate -description: Run models on an NDIF you host — deploy, evict, pin, scale, restart, inspect the queue, kill a request, export a models.yaml, and size a model so it actually fits. Use for "deploy a model on NDIF", "ndif deploy/evict/scale/status/queue/kill", "pin a model", "models.yaml", "NDIF_DEPLOYMENTS", "how much GPU will this model take", "padding factor", "HOT/WARM/COLD", "NDIF dashboard", "schedule a model", "add a GPU node", "turn on NDIF auth", "API keys", "NDIF in production", or NDIF Grafana/Loki/InfluxDB telemetry. Assumes a server is already up — standing one up is `ndif-selfhost`. +name: operate +description: Run models on an NDIF you host — deploy, evict, pin, scale, restart, inspect the queue, kill a request, export a models.yaml, and size a model so it actually fits. Use for "deploy a model on NDIF", "ndif deploy/evict/scale/status/queue/kill", "pin a model", "models.yaml", "NDIF_DEPLOYMENTS", "how much GPU will this model take", "padding factor", "HOT/WARM/COLD", "NDIF dashboard", "schedule a model", "add a GPU node", "turn on NDIF auth", "API keys", "NDIF in production", or NDIF Grafana/Loki/InfluxDB telemetry. Assumes a server is already up — standing one up is `selfhost`. --- # Operating an NDIF @@ -317,7 +317,7 @@ hardened jail. Terminate TLS yourself and never expose 8001 directly. ## Related skills -- `ndif-selfhost` — getting a server up in the first place. -- `ndif-troubleshoot` — when a deploy fails or a request hangs. -- `ndif-develop` — changing placement, the queue, or the actor. +- `selfhost` — getting a server up in the first place. +- `troubleshoot` — when a deploy fails or a request hangs. +- `develop` — changing placement, the queue, or the actor. - nnsight plugin `remote` — the client side of what these deployments serve. diff --git a/plugins/ndif/skills/ndif-operate/references/cli-commands.md b/plugins/ndif/skills/operate/references/cli-commands.md similarity index 100% rename from plugins/ndif/skills/ndif-operate/references/cli-commands.md rename to plugins/ndif/skills/operate/references/cli-commands.md diff --git a/plugins/ndif/skills/ndif-operate/references/production-and-auth.md b/plugins/ndif/skills/operate/references/production-and-auth.md similarity index 99% rename from plugins/ndif/skills/ndif-operate/references/production-and-auth.md rename to plugins/ndif/skills/operate/references/production-and-auth.md index 2702d5f..62ea4cb 100644 --- a/plugins/ndif/skills/ndif-operate/references/production-and-auth.md +++ b/plugins/ndif/skills/operate/references/production-and-auth.md @@ -35,7 +35,7 @@ itself defaults to `trusted=True`. Follow that flag: An explicit `trusted` in the envelope is honored either way, which is the one escape hatch: a client can send `trusted: false` to force the sandbox path with no Postgres at all. That is how you exercise the untrusted path in dev — see the -`ndif-develop` skill. +`develop` skill. ## Turning it on diff --git a/plugins/ndif/skills/ndif-operate/references/sizing-and-placement.md b/plugins/ndif/skills/operate/references/sizing-and-placement.md similarity index 100% rename from plugins/ndif/skills/ndif-operate/references/sizing-and-placement.md rename to plugins/ndif/skills/operate/references/sizing-and-placement.md diff --git a/plugins/ndif/skills/ndif-selfhost/SKILL.md b/plugins/ndif/skills/selfhost/SKILL.md similarity index 98% rename from plugins/ndif/skills/ndif-selfhost/SKILL.md rename to plugins/ndif/skills/selfhost/SKILL.md index 1cbe6fb..41fbd90 100644 --- a/plugins/ndif/skills/ndif-selfhost/SKILL.md +++ b/plugins/ndif/skills/selfhost/SKILL.md @@ -1,5 +1,5 @@ --- -name: ndif-selfhost +name: selfhost description: Stand up your own NDIF server — the backend that nnsight's remote=True talks to — on your own GPUs. Use for "run my own NDIF", "self-host NDIF", "docker run ndif/ndif", "local NDIF server", "point nnsight at my server", "ndif start", "install NDIF from source", "NDIF docker compose", or any question about NDIF prerequisites, ports, volumes, tags, or configuration. Covers all three routes (the published ndif/ndif image, the compose dev stack from a checkout, and a from-source install with the ndif CLI), how to prove it works, and the one default that matters — an NDIF with no Postgres runs every caller's Python inside the model process. NOT for using the public ndif.us service; that is the nnsight plugin's `remote` skill. --- @@ -28,7 +28,7 @@ that. A trusted request: That is the right default for an NDIF you run for yourself, and the wrong one the moment a second person can reach port 8001. Before anyone else can: `docs/runbooks/enable-auth.md`, then `docs/operating/production.md`. The -`ndif-operate` skill covers what turning it on changes. +`operate` skill covers what turning it on changes. ## Pick a route @@ -80,7 +80,7 @@ change: other people hold.** `ndif status` will say a 80 GB card is 80 GB free while a colleague's job holds 25 GB of it, and the placer will put a model there. Subtract other tenants' `nvidia-smi` usage yourself and force the placement - with `ndif deploy --gpus N` (or `--size-bytes`); see the `ndif-operate` skill's + with `ndif deploy --gpus N` (or `--size-bytes`); see the `operate` skill's sizing section for the arithmetic. - **The container runs as root**, so anything it writes into a bind mount — the Ray temp dir, new files in the HF cache — is root-owned afterwards. Clean up @@ -340,7 +340,7 @@ and exhaustively in `docs/reference/env-vars.md` and `docs/reference/ports.md`. ## Related skills -- `ndif-operate` — deploying and sizing models, the dashboard, auth, production. -- `ndif-troubleshoot` — when it does not come up, or a request hangs. -- `ndif-develop` — changing the server's code. +- `operate` — deploying and sizing models, the dashboard, auth, production. +- `troubleshoot` — when it does not come up, or a request hangs. +- `develop` — changing the server's code. - nnsight plugin `remote` — writing the client-side code that talks to it. diff --git a/plugins/ndif/skills/ndif-selfhost/references/configuration.md b/plugins/ndif/skills/selfhost/references/configuration.md similarity index 100% rename from plugins/ndif/skills/ndif-selfhost/references/configuration.md rename to plugins/ndif/skills/selfhost/references/configuration.md diff --git a/plugins/ndif/skills/ndif-selfhost/references/routes.md b/plugins/ndif/skills/selfhost/references/routes.md similarity index 100% rename from plugins/ndif/skills/ndif-selfhost/references/routes.md rename to plugins/ndif/skills/selfhost/references/routes.md diff --git a/plugins/ndif/skills/ndif-troubleshoot/SKILL.md b/plugins/ndif/skills/troubleshoot/SKILL.md similarity index 98% rename from plugins/ndif/skills/ndif-troubleshoot/SKILL.md rename to plugins/ndif/skills/troubleshoot/SKILL.md index e614d3c..c254947 100644 --- a/plugins/ndif/skills/ndif-troubleshoot/SKILL.md +++ b/plugins/ndif/skills/troubleshoot/SKILL.md @@ -1,5 +1,5 @@ --- -name: ndif-troubleshoot +name: troubleshoot description: Diagnose a self-hosted NDIF that is broken — container won't start, no GPU, 503 "compute backend is reconnecting", requests that hang in QUEUED or DEPLOYING, CANT_ACCOMMODATE, CUDA out of memory on deploy or inside a block, "COMPLETED but the client can't download", "The model architecture on this server doesn't match", "Your request payload could not be read", "Starting Ray client server failed", code changes with no effect, a blank dashboard, empty Grafana panels. Use when someone pastes an NDIF server log, an nnsight RemoteError from their own deployment, or asks where NDIF's logs are, how to read `ndif queue`, or how to kill or reconstruct a request. For the public ndif.us service use the nnsight plugin's `remote` skill instead. --- @@ -187,7 +187,7 @@ to serve a half-CPU model. Read it as "the estimate was too low". RAM for the WARM cache. For GPU pressure: evict explicitly, deploy the incoming model `--pinned` (which waives the age check), lower `NDIF_MINIMUM_DEPLOYMENT_TIME_SECONDS`, or tune the padding knobs. The whole -procedure, including what the ledger cannot see, is in the `ndif-operate` skill +procedure, including what the ledger cannot see, is in the `operate` skill and `docs/runbooks/model-oom-on-deploy.md`. ## COMPLETED but no result @@ -314,7 +314,7 @@ And the label that hides model-actor logs: they are **`service="model"`**, not ## Related skills -- `ndif-selfhost` — prerequisites, tags, ports, and the configuration model. -- `ndif-operate` — deploys, sizing, eviction, auth. -- `ndif-develop` — when the fix is a code change. +- `selfhost` — prerequisites, tags, ports, and the configuration model. +- `operate` — deploys, sizing, eviction, auth. +- `develop` — when the fix is a code change. - nnsight plugin `debugging` — when the failure is in the user's own trace. diff --git a/plugins/ndif/skills/ndif-troubleshoot/references/error-messages.md b/plugins/ndif/skills/troubleshoot/references/error-messages.md similarity index 100% rename from plugins/ndif/skills/ndif-troubleshoot/references/error-messages.md rename to plugins/ndif/skills/troubleshoot/references/error-messages.md diff --git a/plugins/ndif/skills/ndif-troubleshoot/references/reading-logs.md b/plugins/ndif/skills/troubleshoot/references/reading-logs.md similarity index 100% rename from plugins/ndif/skills/ndif-troubleshoot/references/reading-logs.md rename to plugins/ndif/skills/troubleshoot/references/reading-logs.md diff --git a/smithery/build.py b/smithery/build.py new file mode 100644 index 0000000..00f52a9 --- /dev/null +++ b/smithery/build.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +"""Assemble the single-skill Smithery bundle from the nnsight plugin. + + python smithery/build.py # writes smithery/nnsight/ + python smithery/build.py --check # build, then fail if the output is stale + +Smithery (smithery.ai) hosts one skill per bundle: a SKILL.md plus optional +scripts/, references/ and assets/ directories, with no other skill to cross-load. +The nnsight plugin is 18 skills that lean on each other by name, so this script +folds them into one: + + plugins/nnsight/skills/nnsight/SKILL.md -> SKILL.md + plugins/nnsight/skills/nnsight/references/X.md -> references/X.md + plugins/nnsight/skills/nnsight/scripts/*.py -> scripts/*.py + plugins/nnsight/skills//SKILL.md -> references/.md + plugins/nnsight/skills//references/R.md -> references/-R.md + +and rewrites what only makes sense across skills: relative links between the +trees, "the `` skill" prose, the "Related skills" lists, and the test-harness +directives (``) that mean nothing outside this repo. + +The source skills stay the single source of truth — every code block in them is +executed by tests/test_skills.py. Re-run this after editing a skill. +""" + +from __future__ import annotations + +import argparse +import filecmp +import re +import shutil +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SKILLS_ROOT = REPO_ROOT / "plugins" / "nnsight" / "skills" +OUT = REPO_ROOT / "smithery" / "nnsight" +CORE = "nnsight" + +DESCRIPTION = ( + "Read, modify, and analyze the internals of neural networks with nnsight 0.8 — " + "tracing activations, intervening on modules, batching interventions, gradients, " + "caching, generation, and remote execution on NDIF for models too large to run " + "locally. Use for any task that touches model internals rather than just outputs: " + "activation extraction, logit lens, activation and attribution patching, causal " + "tracing, ablation, attention-head analysis, circuit discovery, probing, sparse " + "autoencoders, steering vectors, model editing and LoRA, vLLM and tensor-parallel " + "runs, quantized and multimodal models, and debugging or porting existing nnsight " + "code. Load this before writing any nnsight code: idioms from older versions " + "(.value, nnsight.list(), LanguageModel, proxies) are widespread and silently wrong " + "on 0.8. Bundles one guide per technique in references/ and scripts that print a " + "model's module paths without downloading weights." +) + +FRONTMATTER = f"""--- +name: nnsight +description: {DESCRIPTION} +license: MIT +metadata: + author: ndif-team + source: https://github.com/ndif-team/skills + nnsight: "0.8" + transformers: ">=5" +--- +""" + +# One row per bundled skill, in the order the guide table lists them. +GUIDES: list[tuple[str, str, str]] = [ + # (skill, group, one-line "use when") + ("debugging", "Foundation", "an error, a hang, an empty result, or pre-0.8 code to port"), + ("remote", "Foundation", "running on NDIF: sessions, request batching, download size, non-blocking jobs"), + ("interp-experiment-design", "Techniques", "choosing a metric, controls, and sanity checks before running anything"), + ("logit-lens", "Techniques", "decoding what each layer predicts; tracking where an answer emerges"), + ("activation-patching", "Techniques", "locating the layers, positions, or heads that carry a behavior; DAS"), + ("attribution-patching", "Techniques", "scaling patching to whole models with a gradient approximation"), + ("causal-tracing", "Techniques", "corrupt-and-restore factual localization (ROME-style)"), + ("ablation", "Techniques", "testing necessity: zero, mean, resample, and noise ablation"), + ("attention-analysis", "Techniques", "attention patterns, per-head metrics, induction/copy head detection"), + ("circuit-discovery", "Techniques", "finding and validating the subgraph behind a task (IOI-style)"), + ("probing", "Techniques", "training classifiers on activations; what is linearly decodable, and whether it is used"), + ("sae-and-dictionary-learning", "Techniques", "feature-level analysis; attaching, training, and evaluating SAEs"), + ("model-steering", "Techniques", "steering vectors, function vectors, persistent behavioral edits"), + ("model-editing-and-lora", "Techniques", "weight edits, ROME-style updates, adapters trained through a frozen model"), + ("nnterp", "Runtimes and tooling", "one script that runs unchanged across GPT-2, Llama, Qwen, Gemma"), + ("vllm", "Runtimes and tooling", "throughput, continuous batching, CUDA-graph taps, `model.edit()` sweeps, nnsight-serve"), + ("tensor-parallel", "Runtimes and tooling", "a model too big for one GPU, sharded with transformers TP under `torchrun`"), + ("quantization", "Runtimes and tooling", "a model too big for one GPU, held in 4 or 8 bits (`dtype=\"nf4\"`, `\"int8\"`)"), + ("diffusion-and-multimodal", "Runtimes and tooling", "VLMs, diffusion pipelines, the diffusion lens, non-text tasks"), +] + +DIRECTIVE = re.compile(r"^[ \t]*[ \t]*\n", re.MULTILINE) +SKILL_PHRASE = re.compile(r"((?:[Tt]he\s+)?)`([a-z0-9-]+)` skill\b") +LINK = re.compile(r"\]\(([^)\s]+)\)") + + +def skill_dirs() -> list[Path]: + return sorted(p for p in SKILLS_ROOT.iterdir() if (p / "SKILL.md").exists()) + + +def split_frontmatter(text: str) -> tuple[str, str]: + assert text.startswith("---\n"), "no frontmatter" + end = text.index("\n---\n", 4) + return text[4:end], text[end + 5 :] + + +def rewrite_links(text: str, skill: str, own_refs: set[str], in_reference: bool) -> str: + """Point every relative link at where the file now lives in the bundle.""" + + def sub(m: re.Match) -> str: + target = m.group(1) + if "://" in target or target.startswith("#"): + return m.group(0) + path, _, anchor = target.partition("#") + anchor = f"#{anchor}" if anchor else "" + # Links into the core skill's references from another skill. + core = re.fullmatch(r"(?:\.\./)+nnsight/references/([^/]+\.md)", path) + if core: + return f"]({core.group(1)}{anchor})" + if skill == CORE: + return m.group(0) + # A non-core skill's own references, from its SKILL.md or from a sibling. + own = re.fullmatch(r"(?:references/)?([^/]+\.md)", path) + if own and own.group(1) in own_refs: + return f"]({skill}-{own.group(1)}{anchor})" + raise ValueError(f"{skill}: unhandled link {target!r}") + + return LINK.sub(sub, text) + + +def rewrite_prose(text: str, in_reference: bool, skill: str, known: set[str]) -> str: + """Cross-skill mentions become pointers into the bundle.""" + prefix = "" if in_reference else "references/" + top = "the top-level `SKILL.md`" + + def link(name: str) -> str: + if name == CORE: + return "../SKILL.md" if in_reference else "SKILL.md" + return f"{prefix}{name}.md" + + # "the `nnsight` skill → batching" names one of the core references. + def core_ref(m: re.Match) -> str: + ref = re.sub(r"\s+", "-", m.group(2).strip()) + assert (SKILLS_ROOT / CORE / "references" / f"{ref}.md").exists(), f"{skill}: no core reference {ref!r}" + return f"{m.group(1)}[{ref}.md]({prefix}{ref}.md)" + + text = re.sub(r"((?:[Tt]he\s+)?)`nnsight` skill → ([a-z][a-z\s]+?)(?=[,.;)]|\s(?:has|for|is|section)\b)", core_ref, text) + + # "`scripts/x.py` in the `nnsight` skill": the script now sits in this bundle. + def script_home(m: re.Match) -> str: + if "scripts/" in text[max(0, m.start() - 160) : m.start()]: + return "in this skill" + return m.group(0) + + text = re.sub(r"(?:in|from) the\s+`nnsight` skill", script_home, text) + + # "`a` and `b` skills" + def plural(m: re.Match) -> str: + a, b = m.group(1), m.group(2) + return f"`{a}` and `{b}` guides (`{link(a)}`, `{link(b)}`)" + + text = re.sub(r"`([a-z0-9-]+)` and `([a-z0-9-]+)` skills\b", plural, text) + + def sub(m: re.Match) -> str: + article = "The " if m.group(1) and m.group(1).startswith("T") else "the " + name = m.group(2) + if name == CORE: + return article + top[4:] + return f"{article}`{name}` guide (`{link(name)}`)" + + text = SKILL_PHRASE.sub(sub, text) + + if in_reference: + text = text.replace("this skill", "this guide").replace("the other skills", "the other guides") + if skill != CORE: + text = text.replace("the main SKILL", f"`{skill}.md`") + + # "## Related skills" bullet lists: every backticked skill name becomes a link. + def related(section: re.Match) -> str: + body = section.group(2) + + def name(m: re.Match) -> str: + n = m.group(1) + return f"[{n}]({link(n)})" if n in known else m.group(0) + + body = re.sub(r"(?m)^(- .*)$", lambda m: re.sub(r"`([a-z0-9-]+)`", name, m.group(1)), body) + return f"## Related guides{section.group(1)}{body}" + + text = re.sub(r"## Related skills(\n)((?:(?!^## ).*\n?)*)", related, text, flags=re.MULTILINE) + return text + + +def guide_table() -> str: + lines = [ + "## Technique and runtime guides", + "", + "Everything the NDIF team ships as separate skills is bundled here as one guide", + "each. Read the one that matches the task before writing code; each is", + "self-contained and its examples run against real models in CI.", + "", + ] + group = None + for skill, g, when in GUIDES: + if g != group: + group = g + lines += [f"**{group}**", "", "| Guide | Use when... |", "|---|---|"] + lines.append(f"| [{skill}](references/{skill}.md) | {when} |") + nxt = GUIDES.index((skill, g, when)) + 1 + if nxt == len(GUIDES) or GUIDES[nxt][1] != g: + lines.append("") + return "\n".join(lines) + + +def build_core(text: str, known: set[str]) -> str: + _, body = split_frontmatter(text) + body = DIRECTIVE.sub("", body) + + # The intro sentence that points at other skills. + old = ( + "This skill is the general-purpose reference. Techniques built on it (logit lens,\n" + "activation patching, steering, …) have their own skills; see the bottom of this\n" + "file." + ) + new = ( + "This file is the general-purpose reference. Techniques built on it (logit lens,\n" + "activation patching, steering, …) each have a guide under `references/`; see\n" + "[Technique and runtime guides](#technique-and-runtime-guides) at the bottom." + ) + assert old in body, "core intro changed; update build_core" + body = body.replace(old, new) + + # The core's own "Related skills" list becomes the full guide table. + head, sep, _ = body.partition("## Related skills") + assert sep, "core has no Related skills section; update build_core" + body = head + guide_table() + + body = rewrite_links(body, CORE, set(), in_reference=False) + body = rewrite_prose(body, False, CORE, known) + return FRONTMATTER + body + + +def build_reference(text: str, skill: str, own_refs: set[str], known: set[str]) -> str: + """A non-core SKILL.md becomes a reference; frontmatter goes, the H1 stays.""" + _, body = split_frontmatter(text) + body = DIRECTIVE.sub("", body) + body = rewrite_links(body, skill, own_refs, in_reference=True) + body = rewrite_prose(body, True, skill, known) + return body.lstrip("\n") + + +def build_sub_reference(text: str, skill: str, own_refs: set[str], known: set[str]) -> str: + body = DIRECTIVE.sub("", text) + body = rewrite_links(body, skill, own_refs, in_reference=True) + body = rewrite_prose(body, True, skill, known) + return body + + +def build(out: Path) -> None: + if out.exists(): + shutil.rmtree(out) + (out / "references").mkdir(parents=True) + (out / "scripts").mkdir() + + bundled = {s for s, _, _ in GUIDES} + found = {d.name for d in skill_dirs()} - {CORE} + known = found | {CORE} + assert bundled == found, f"GUIDES out of sync with plugins/nnsight/skills: {bundled ^ found}" + + for d in skill_dirs(): + skill = d.name + own_refs = {p.name for p in (d / "references").glob("*.md")} + skill_md = (d / "SKILL.md").read_text() + if skill == CORE: + (out / "SKILL.md").write_text(build_core(skill_md, known)) + for ref in sorted((d / "references").glob("*.md")): + (out / "references" / ref.name).write_text(build_sub_reference(ref.read_text(), skill, own_refs, known)) + for script in sorted((d / "scripts").glob("*.py")): + shutil.copy(script, out / "scripts" / script.name) + else: + (out / "references" / f"{skill}.md").write_text(build_reference(skill_md, skill, own_refs, known)) + for ref in sorted((d / "references").glob("*.md")): + (out / "references" / f"{skill}-{ref.name}").write_text( + build_sub_reference(ref.read_text(), skill, own_refs, known) + ) + + shutil.copy(REPO_ROOT / "LICENSE", out / "LICENSE") + + +def validate(out: Path) -> list[str]: + problems: list[str] = [] + skill_md = (out / "SKILL.md").read_text() + fm, body = split_frontmatter(skill_md) + + name = re.search(r"^name: (.+)$", fm, re.MULTILINE).group(1) + if not re.fullmatch(r"[a-z0-9-]{1,64}", name): + problems.append(f"name {name!r} violates ^[a-z0-9-]{{1,64}}$") + desc = re.search(r"^description: (.+)$", fm, re.MULTILINE).group(1) + if len(desc) > 1024: + problems.append(f"description is {len(desc)} chars (max 1024)") + if (n := body.count("\n")) > 500: + problems.append(f"SKILL.md body is {n} lines (keep under 500)") + + for md in [out / "SKILL.md", *sorted((out / "references").glob("*.md"))]: + text = md.read_text() + rel = md.relative_to(out) + if "