| name | glossary |
| description | Create or audit GLOSSARY.md. Use before naming product concepts, writing user-visible terms, renaming concepts, or checking vocabulary drift and banned terms. |
| user_invocable | true |
| argument-hint | [init | audit | add <term>] |
Glossary
GLOSSARY.md at the repo root is the canonical name for every product concept. One concept, one word, everywhere: UI strings, public API names, doc headings, route segments, error messages, commit subjects.
Worktree isolation
Before any edit, follow the worktree isolation contract. It provides the atomic live-agent claim used below.
An existing worktree alone does not prove another agent is active.
wt is the only worktree tool. Never run git worktree add, and never use a harness worktree option such as EnterWorktree or isolation: "worktree". Those write to .claude/worktrees/, which is banned. wt places every worktree at <parent>/<repo>.<branch-slug>.
Keep the primary checkout read only. Before mutation, run wt list --format=json. Reuse the task's worktree with wt switch <branch>, or create one with wt switch --create <branch> --base <base>. Read its absolute path from the JSON, then pass that path as workdir to every later command. Never share a mutation worktree between tasks.
The failure mode this exists to stop
An agent given a concept with no established name invents one, then propagates it. A single feature ends up shipping as Sprint in the dashboard, runBatch() in the SDK, "campaign" in the docs, and /jobs in the URL. Nobody decided that. It accretes one plausible-in-isolation naming choice at a time, and by the time a human notices, the term is in a published API and a customer's bookmarks.
Vocabulary is a product surface. Treat inventing a word with the same caution as adding a public export.
Rules
- Read
GLOSSARY.md before naming anything user-visible. If the repo has one, its terms win over anything that reads better in the moment.
- Never introduce a synonym for a term that exists. If the glossary says Sprint, do not write "run", "batch", or "job", not even in a tooltip, a variable name, or a log line.
- Never use a term on the ban list. The ban list carries a replacement; use it.
- A concept with no term does not get named silently. Propose an addition, state the candidate term and the synonyms it displaces, and get confirmation. Inventing quietly is the whole failure mode.
- Take the platform's word before inventing one. GitHub, Nuxt, Vue, and HTTP have already named most things.
auto merge beats a coined merge tier, because the reader knows it and nobody has to confirm it. Propose at most one new term per change; a set of new terms is a redesign, not a name.
- Match the recorded casing exactly.
Nuxt SEO and NuxtSEO are different brands to a reader.
- A term list without a relationship map is half a glossary. See below. Terms are only ambiguous in relation to each other, so the map is what makes the list decidable.
Rule 4 is the one that matters. Rules 1 to 3 only work on concepts someone already thought about.
The relationship map is mandatory
A flat list of terms records what each word means on its own. It cannot record what the reader actually needs: how the terms relate, and therefore which distinctions are load-bearing and which are drift.
Every GLOSSARY.md carries a ## Map section. Produce it during init, refresh it during audit, extend it on add. Never present a term list as finished without one.
The map answers four questions a list cannot:
- Production — what makes this thing, and what does it make? A term that is produced by another term is not a synonym for it, however similar the words look.
- Storage and ownership — which table, which module or layer. Two terms in two tables owned by two teams are two concepts, whatever the UI calls them.
- Cardinality — 1—1, 1—N, N—N, and whether the constraint is enforced (a unique index) or merely conventional. An enforced 1—1 kills entire classes of naming question outright.
- Surface crossing — which customer-facing word each internal term surfaces under. This is where drift lives, and it is invisible in a list because the list has one row per term, not one row per crossing.
The table is the mandatory part of the map; the diagram is optional. Filling one Customer word cell per row is what exposes a collision, because the column forces you to answer per term and two identical cells are visible at a glance. Draw the diagram on top only when 3 or more terms converge on one word, or a pipeline branches. Field evidence: on a real init run the table decided every finding and the diagram decided none.
Pick the column set from the repo shape. Only Term and Customer word are fixed; the middle columns answer "where does it live, who owns it, how does it relate" in whatever terms the codebase actually has. Forcing DB columns onto a library produces a table with 11 of 16 cells empty.
Application backed by a database:
| Term | Table | Owner | Cardinality | Customer word |
| --- | --- | --- | --- | --- |
| Finding | `findings` | `pro/audit` | Audit 1—N Finding | "issue" |
| Page Issue | `site_page_issues` | `pro/sites` | Site 1—N, by path | "issue" |
| Ticket | `sprint_tickets` | `pro/sprint` | Sprint 1—N Ticket | "issue" |
Mark an enforced constraint as enforced: Site 1—1 Sprint (uniqueIndex) ends a whole line of questioning that Site 1—1 Sprint leaves open.
Library, CLI, or SDK: a term's switching cost is who imports it, not what stores it. Cardinality is usually a type parameter rather than a constraint, so it earns no column.
| Term | Export path | Stability | Consumers | Customer word |
| --- | --- | --- | --- | --- |
| Analyzer | `@gscdump/analysis/registry` | published subpath | CLI, MCP | "tool" |
| Report | `@gscdump/analysis` | published | CLI | "report" |
For a published package, an exported type name is a customer surface, not an internal one. It is as hard to change as a route.
For any shape, add a second table when published identifiers outnumber concepts: one row per frozen identifier (CLI command and flag, MCP tool name, export subpath, error code, route) mapped to the term it names. In a library that crossing table is where the drift actually lives.
When presenting the map to a human for a naming decision, a rendered diagram can carry the argument: sources on the left, arrows labelled with what actually moves, paths shown splitting and whether they rejoin. Tag each internal box with the customer word it surfaces under. Several differently-shaped boxes carrying an identical tag is the drift argument made visible. Write it as Mermaid per Map syntax below.
Do not draw one by default. Two init runs produced a diagram; in both the table found every collision and the diagram found none, restating what the Customer word column already said. Draw one only when a human has to be persuaded of a branching pipeline, and skip it whenever the relationships are type-flow rather than data-flow.
Never ask for an ASCII containment tree. A tree cannot render a node with two parents, and a term with two parents is the normal case, not the exception: one real repo had Site owned by both Team and GSC Property, which made the tree impossible to draw at all.
Read the decision records before drawing
A glossary harvested only from schema and UI will misread deliberate decisions as drift. This is the single most expensive mistake in this skill. Before writing the map, read docs/adr/, docs/decisions/, or whatever the project's decision log is called, and grep it for each candidate term.
An umbrella word that an accepted ADR chose on purpose is a ratified umbrella, not accretion, and proposing to "fix" it wastes the user's time and burns credibility. The evidence trail runs: schema says what is stored, UI says what is shown, the decision log says which of those was chosen and why. Only the third distinguishes a deliberate collapse from a silent one.
Check every ban against the schema
Run this against the bans already in the file, not only the ones you are proposing. An inherited ban list rots as the schema grows, and one real repo banned "reports" and "insights" while both were a live route, a table, and a published MCP tool.
Before a word goes on the ban list, or stays on it, grep for it across every frozen surface: stored enum values, column names, status literals, published export paths and subpaths, CLI command and flag names, MCP tool names, error-code literals, route segments, and ADR titles. A word is frozen when changing it breaks someone outside the repo, whether that is a database or an importer. Schema is only the database-shaped half; one library run rejected 10 proposed bans and not one of them was blocked by a stored value. A word the database persists cannot be banned in favour of another term. It is not a synonym; it is a value with behaviour attached, and banning it puts the glossary in conflict with the code rather than with a careless string.
When this fires, the ban list is what is wrong, not the schema. Narrow the ban to prose, or drop it, and record the enum's values with the axis they belong to.
Watch for the same word on two axes
The subtlest finding a map produces: one word used as a value on two independent classification axes of the same entity, meaning something different on each. A list cannot show this, because the word gets one entry and looks consistent. The map shows it as two boxes with one label.
When it appears, do not rename either axis reflexively. Record both, state the axis each belongs to, and only then ask whether the collision is worth the cost of renaming.
Commit scopes
A commit scope is vocabulary, and it drifts the same way every other surface does. Measured across Harlan's seven repositories, 176 of 240 scoped commits in one repo named a single service under two words, agent and github-agent.
Record scopes as a ## Scopes table, placed after Banned. It uses the Banned column shape.
## Scopes
| Never | Use instead | Why |
| --- | --- | --- |
| `agent` | `github-agent` | The package, unit and skill all spell it `github-agent` |
List only retired spellings. Never list every allowed scope. An allowlist looks tidier and fails in practice: 40 to 59 percent of scopes in these repositories appear exactly once, so the list churns while the real drift is a handful of synonym pairs. A scope absent from the table is allowed.
Do not derive scopes from the directory tree. A structural list matched between 5 and 70 percent of real scopes, and under 25 percent in every application repository. Roughly half of real scopes name a process lane such as ci or deps, or a subsystem that crosses directories such as crawl or overlay.
The commit-msg git hook reads this table. It refuses a retired scope and names the replacement. A repository with no GLOSSARY.md keeps every scope, so this is opt in.
Add a row when audit finds two scopes naming one concept. Pick the winner by the same rule as any other term: weight by surface, never by count. In the example above agent had 111 uses against 65, and it still lost, because the package, the systemd unit and the skill directory all spell it github-agent. A frozen surface outranks a tally.
Format
GLOSSARY.md has four sections in this order: Map, Terms, Banned, Open questions. A repository that enforces commit scopes adds Scopes after Banned.
Read references/format.md for the Mermaid map syntax, the term entry shape, and a worked example before writing or auditing the file.
Workflows
Pick by the argument given, defaulting to audit when the user points at a codebase and to init when no GLOSSARY.md exists.
init — bootstrap from an existing codebase
Do not invent the vocabulary. Recover the one already in use, then pick winners.
Harvest three surfaces separately, and do the customer one first. A schema is engineering vocabulary. It is evidence of what the team calls things, not of what the product calls them, and it may have drifted from the business names years ago. Starting from tables produces a tidy glossary that quietly contradicts the UI.
-
Find the glossary that already exists. Before harvesting anything, grep the repo for an informal one: a Vocabulary, Terminology, Naming, Say/Don't say, or Copy section in COPY.md, CONTEXT.md, STYLE.md, CONTRIBUTING.md, README.md, or the docs tree. Projects that care about wording usually wrote one down without calling it GLOSSARY.md. Missing this ships a third competing list and is the worst outcome this skill can produce. Never silently override a wording decision someone already made, and never write a second list beside an existing one without saying which wins.
0.5. Decide how you relate to what you found, and record the decision. Three outcomes, and the skill will not choose for you:
- Point to it — the existing list is complete and better established than anything you would write. Cite it as authoritative, cover only what it omits, and say so in the intro.
- Fold it in — the existing list is partial or scattered. Move it in verbatim, credit where it came from, and leave a pointer behind in the old file so the two cannot drift.
- Supersede it — the existing list is stale or contradicted by shipped surfaces. Say which entries you are overriding and why, one line each.
Put the choice in ## Open questions unless the evidence makes it obvious, because it decides who owns wording from now on.
-
Customer surface: UI strings in templates, route and page filenames, headings, marketing and docs copy. Plus everything published, which counts here even with no UI in the repo: CLI command names, flag names and positional args (analyze <tool> names a concept), package names and export subpaths (@scope/pkg/registry), MCP tool names and protocol identifiers, stable error-code literals, and exported type names. Watch for a registry id disagreeing with its public path (id: 'decay' behind /tools/content-decay is two names for one thing). This surface is what a term means to the person paying, and for a library it is nearly the whole repo.
-
Internal surface: table names, stored enum values, protocol contracts, layer and module directory names, unexported helpers. Exported types are not internal in a published package; file them under the customer surface, since an importer feels a rename exactly like a customer feels a changed route.
audit — find drift
- Read
GLOSSARY.md. Build the search set from every Never: entry and every Banned row.
- Validate the ban list before searching against it. For each banned word, grep for it as a stored enum value, column name, or status literal. A word the schema persists is not a synonym to be replaced; the ban is the defect. Report those first, as glossary bugs rather than code bugs, because every hit they generate downstream is noise.
- Search the codebase for each surviving term. Prioritise user-visible surfaces: templates, markdown, route names, public exports, error strings. Internal-only variable names are a lower tier; report separately.
- Re-walk the map against reality. Confirm each term still has the table, owner, and cardinality recorded, and that no new term has appeared in the tree's territory. A map that has silently gone stale makes every other answer in the file untrustworthy. Redraw it as part of the audit output, not as a follow-up.
- Report as
file:line, the offending term, and the canonical replacement:
User-visible (fix now):
app/pages/runs.vue:14 "Run history" -> Sprint history (route also needs /runs -> /sprints)
docs/guide/setup.md:31 "campaign" -> Sprint
server/api/sprint.ts:88 throw new Error('batch failed') -> 'Sprint failed'
Internal identifiers (lower tier, ripast can rename):
lib/queue.ts:12 runBatch() -> runSprint()
Needs a human read (may be ordinary English):
README.md:6 "run the CLI" -- likely fine, not the Sprint noun
- For code identifiers, hand the renames to the
ripast skill; it is AST-aware and updates import sites. Do not sed a rename across a repo.
- Prose and template strings need reading in context: a hit can be a legitimate everyday use of the word rather than the product concept ("run the tests" is not the Sprint noun). Never bulk-replace those.
add <term>
Append a term block. Fill the Never: line with the synonyms it displaces, including whatever the code currently calls it. A new term with an empty Never: line is half-recorded, and audit will not catch drift against it.
Then place the term in the map, and treat that as part of adding it rather than as bookkeeping. Give it a parent, a table, an owner, a cardinality, and its customer-facing word. A term that cannot be placed is the useful failure: either it duplicates something already on the tree, or it belongs to a concept nobody has named yet, and both need resolving before the term is written.
Scope
Glossary governs nouns for product concepts: what a thing is called. It does not govern voice, tone, or sentence style. If the project also has .claude/context/writing-style.md from the site-setup skill, that owns prose style and this owns terminology. When they disagree on a product noun, the glossary wins.