# Joining Labs — instructions for whoever prepares a repository
You are in a repository. It should become an **ABC Legacy Labs** project, **take**
Labs projects into itself, or both. This page is everything you need to do that
without asking anyone: decide from what the repository itself shows, write one
file, validate it, hand two lines back to the owner. The reference behind every
rule here is [CONTRACT.md](CONTRACT.md); read it only if something below is
unclear.
## 0. What you will produce
One folder at the repository root, with one required file and a few optional
ones — **all under fixed names**:
```
abc-labs/
labs.json required — what you are and what you rent
README.md optional — rendered on your project page: your own story, in your words
CHANGELOG.md optional — its newest section is shown as "What's new"
icon.svg optional — your card's icon (or icon.png; ≤ 64 KB; shown as an
)
```
Labs reads **only this folder**, and only these names. It never follows a path
into the rest of your repository, so you can reorganise everything else without
telling anyone. Put what you want shown here; how it gets here — copied by your
build, written by your release script — is your business.
Nothing else changes in the code. Never put a secret in it. Never make the code
import anything from `abc-labs/` — the folder is metadata and vendored files, not
a dependency.
## 1. Decide the direction
| If the repository… | then `labs.json` has… |
|---|---|
| is a thing people use — a server, a tool, a skill pack, an asset set, a starter | an **`export`** half |
| wants to take a Labs project into itself (skills, a template, a component) | an **`import`** half |
| both | both |
Most product repositories only `import`. Most Labs projects only `export`.
## 2. The template — copy, then replace every `<…>`
Every `<…>` lists its allowed values or says what to write. Remove the fields
marked *optional* if they do not apply. `kind` and `category` are two fields but
one decision: write both, and they must agree — the validator refuses a pair
that does not (§3).
```json
{
"labs": 1,
"export": {
"slug": "",
"name": "",
"tagline": "",
"kind": "",
"category": "",
"surfaces": {
"": {}
},
"tags": [""],
"license": "",
"version": "",
"requires": [""],
"status": "",
"started": "",
"uses": {
"": {}
},
"install": {
"include": [""],
"link": { "": "abc-labs//{name}" }
},
"links": {
"docs": "",
"feedback": "/issues>"
},
"note": ""
},
"import": {
"": "<* for its default branch | a tag | a branch | a commit>"
}
}
```
## 3. Choose `kind`, then `category`
Both go in the file, and they must agree — one is the shelf, the other the role
on it. Decide in this order.
**Step A — which `kind`?** One question: *does it do work, or is it material?*
| `kind` | if… | examples |
|---|---|---|
| `service` | it **does work**. Running, called, operated, embedded — something happens because of it | a server, an inspector, a worker, a 3D engine |
| `resource` | it **is material**. It does nothing by itself; someone or something else uses it | skills, icons, datasets, a starter repo |
The test that settles most cases: *if nobody and nothing runs it, does anything
happen?* No → `resource`.
**Step B — which `category` inside that kind?** Ask the questions in order and
take the first "yes". The category says what a project that *takes* this one
**gets**.
| `kind` | `category` | Say **yes** if… | Then the surfaces may be |
|---|---|---|---|
| `service` | `api` | you send it a request and get a result back, without owning how it is done | `mcp` `app` `cli` `package` `bot` |
| `service` | `tool` | you operate it yourself to do your own work faster | `userscript` `extension` `app` `cli` `package` `mcp` |
| `service` | `agent` | you hand it a goal and it works out the steps on its own | `mcp` `app` `cli` `package` `bot` |
| `service` | `component` | it becomes a part of your product (a UI component, a library, a 3D engine) | `package` `import` |
| `resource` | `knowledge` | it makes you — or your agent — *know* something: how, what, why (skills, playbooks, specs, courses) | `ai` `import` `download` |
| `resource` | `asset` | you take material and use it as it is; it does nothing by itself (icons, fonts, tokens, datasets, models) | `download` `import` `package` |
| `resource` | `template` | you start your own thing from it | `import` `app` `download` |
Three rules. **The pair must match** — `"kind": "resource"` with
`"category": "tool"` is refused, and the error names which half to change.
**One category**, the one most people would name; a bundle (tokens + components
+ docs) picks a primary and tags the rest, or becomes several projects. **The
channel does not change either field**: a UI inspector is a `service`/`tool`
whether it arrives as a userscript or, one day, over MCP.
A worked pair, because it is the one people get wrong: a pack of AI skills is
`resource`/`knowledge`, not a service — the skills do nothing on their own; the
AI that reads them is the thing that works. It is *met* through the `ai`
surface (§4), which is where the AI comes in.
## 4. Choose `surfaces` — where a stranger meets it, with what they need
List every surface that **works today**; a planned one is a sentence in the
README, not an entry. Surface and link URLs must be absolute HTTPS URLs (no
`javascript:`, `data:`, local files, credentials or control characters). Each
surface is a key with the fields shown:
| surface | where | write |
|---|---|---|
| `mcp` | an AI client connects over MCP | `{}` — and rent `mcp` in `uses` (§6) |
| `ai` | an AI reads it — skills, rules, prompt packs | `{ "format": "" }` — and fill `install` (§7) |
| `import` | `abc-labs//` in the taking repository | `{}` — and fill `install` (§7) |
| `userscript` | the browser, via Tampermonkey | `{ "url": "" }` |
| `extension` | inside another application | `{ "url": "", "host": "" }` |
| `app` | an application you open | `{ "url": "", "platform": "" }` |
| `cli` | a terminal | `{ "command": "" }` |
| `package` | a package registry | `{ "command": "", "registry": "" }` |
| `bot` | a chat | `{ "url": "" }` |
| `download` | a file you fetch | `{ "url": "" }` |
No other fields exist on a surface; the validator refuses unknown ones. Where
people meet you is always a surface — there is no `links.url`.
**The `ai` surface declares a format, never a host.** Which assistants read a
format is a fact about the ecosystem, and Labs keeps it — so your manifest does
not go stale when a new client adds support:
| `format` | what it is | Labs shows it as running in |
|---|---|---|
| `agent-skill` | Agent Skills — a folder with `SKILL.md` | Claude Code, claude.ai and the Claude API |
| `agents-md` | an `AGENTS.md` file | any agent that reads AGENTS.md |
| `cursor-rules` | Cursor rule files | Cursor |
| `prompt` | a pack of prompts | any assistant — pasted or referenced |
Your format missing? Tell the Labs owner — it is one line in Labs, not a field
in your file.
## 5. Choose `status`
| | |
|---|---|
| `building` | not usable by an outsider yet. The clock runs: 14 days from `started` to `alpha`, or archived |
| `alpha` | an outsider can use it; may break, change, vanish |
| `beta` | shape is stable; breaking changes get a deprecation window |
| `graduated` | left Labs for a home of its own; endpoints keep working |
| `archived` | retired; `mcp` answers `410 Gone` and points at `note` |
A repository that already ships to real users is `beta`, not `building`.
## 6. `uses` — rent a platform capability only if you need it
| rent | when | you receive |
|---|---|---|
| `host` | Labs should **run your process** on its server | `PORT`, `HOST` (`127.0.0.1`), `LABS_ID`, `LABS_SLUG`, `LABS_DATA`, `NODE_ENV` — listen on `$HOST:$PORT`, keep what must survive a deploy in `$LABS_DATA`. It runs in a sandbox of its own: the internet yes, the rest of the machine no (CONTRACT.md) |
| `web` | people should be able to **try it in a browser** | `WEB_PUBLIC_URL` — your own origin, `https://.labs.abclegacyllc.com`. Add `"dist": ""` and Labs serves it with no process at all; also list an `app` surface (its `url` is filled in by Labs) |
| `mcp` | you have an `mcp` surface | `MCP_PATH` (`/` — serve the MCP endpoint **at this path**), `MCP_PUBLIC_URL` |
```json
"uses": {
"host": { "install": "", "start": "" },
"mcp": {},
"web": { "dist": "", "spa": "" }
}
```
Served somewhere else already? `"mcp": { "upstream": "https://" }` and
no `host`. The upstream must be an external HTTPS host; private/loopback hosts
are rejected. A userscript, a skill pack, a dataset rent nothing — omit `uses`.
## 7. `install` — only with an `ai` or `import` surface
```json
"install": {
"include": ["", "<'*' within a segment, '**' across; a matched directory is copied whole>"],
"link": { "": "abc-labs//{name}" }
}
```
**Match folders, not the files inside them.** A matched folder travels whole —
`SKILL.md` *and* whatever sits next to it (`references/`, `tools/`, data). A glob
like `ux-*/SKILL.md` looks right and silently strips all of that. The validator
(§8) prints what your globs match and how many files travel; read that list and
make sure every nested resource is in it.
For an Agent Skills pack (`"ai": { "format": "agent-skill" }`) the link is almost
always `".claude/skills/{name}": "abc-labs//{name}"` — that is where Claude
Code looks; a claude.ai user uploads the same folder by hand. Sources must point inside
`abc-labs//`; an existing real file at a destination is never overwritten.
## 7b. The rest of `abc-labs/` — say more, safely
| file | what happens to it | rules |
|---|---|---|
| `README.md` | rendered on your project page under **About** | Markdown, a safe subset: headings, lists, code, links, emphasis. Raw HTML is shown as text, not run. No images. Write it for a stranger — the repository README can stay technical |
| `CHANGELOG.md` | its **first release section** appears as *What's new* — the heading and what is under it | any heading style works; a `# Changelog` title on top is skipped. Newest release first |
| `icon.svg` or `icon.png` | your card's icon and your page's | ≤ 64 KB; square; served as an image, never inlined, so it can contain no script that runs on the catalog |
| `version` in `labs.json` | shown on the card next to the category, and on the page | make your release script write it — `jq`, `sed`, a build step — so it moves when the real version moves |
All optional. A project that ships only `labs.json` looks exactly as it does today.
Labs re-reads the folder every five minutes, so pushing any of these **is** how
you update your page — there is nothing to tell anyone and nothing to run.
## 8. Validate — do not skip
From the repository root, with node ≥ 22 — nothing to clone, no account:
```bash
npx -y https://labs.abclegacyllc.com/cli/bd6e9d7f01a0.tgz export
```
Read every line it prints and fix the file until it says `valid`. Read the
summary line too: it names the kind, category and surfaces it understood, and
that is your last chance to notice it understood something you did not mean.
With an `install` block it also lists what your globs would hand an importer —
entries and file counts — and warns when the globs match files instead of their
folders.
**Where this works.** Anywhere with node ≥ 22 and the network. This page lives
at `https://labs.abclegacyllc.com/JOIN.md`, and the command above is the
validator Labs itself runs — the Labs repository is private, and you need
nothing from it. Do not guess the tables from memory instead: the validator
knows things this page cannot repeat for every case (category ⇄ surface fit,
`mcp` ⇄ `uses.mcp`, `install` ⇄ `ai`/`import`, field types).
## 9. Hand back
Commit `abc-labs/labs.json`. Then give the owner exactly this:
1. **For an `export`**: the one command the Labs owner runs, *after* the
repository is public — `labs register />`.
It assigns this project's id and adds it to Labs's registry; the card, the
page at `labs.abclegacyllc.com/` and the entry in `index.json` then
appear by themselves. If `host` is rented, the owner also runs
`labs deploy `.
2. **For an `import`**: nothing to hand back — run
`npx -y https://labs.abclegacyllc.com/cli/bd6e9d7f01a0.tgz import ` here, commit what
lands in `abc-labs/` (the files, `labs.json`, `labs.lock.json`) and any links
it made.
## Self-check before you finish
- [ ] `abc-labs/labs.json` is the only Labs-related change; no secrets in it
- [ ] `slug` is this project's own — lowercase, stable, and names what the project *is*; there is no `id` in the file
- [ ] `kind` and `category` agree (§3); exactly one category; every listed surface is allowed for it (§3, last column)
- [ ] every surface works today and carries its fields
- [ ] `mcp` surface ⇄ `uses.mcp`; `ai`/`import` surface ⇄ `install` — both present or both absent
- [ ] no `links.url`, no `run` (both were removed from the contract)
- [ ] `status` reflects reality; `started` is a real date
- [ ] `npx -y https://labs.abclegacyllc.com/cli/bd6e9d7f01a0.tgz export` says `valid`
## Never
Never leave `kind` and `category` disagreeing — fix the file, not the error.
Never list a surface that is not live. Never name the project after a capability
(`-mcp`, `-skill`). Never make the code depend on
Labs being up — capabilities arrive as environment variables, imports as files
you own. Never try to register the repository yourself or invent its id; the
owner registers it, after the repo exists.