This is about decluttering. Comments interrupt code; reading a file should be reading the code.
A gloss is a margin: you read the source, and only if you are confused do you read the note beside
it. @inixiative/gloss keeps source files comment-free except for the three forms that have to be
inline, and moves everything else into a mirrored sidecar — src/foo.ts → .gloss/src/foo.ts.md.
Clean source is the primary win; gloss content is upside for whoever reads the file next. An empty
gloss is acceptable, not failure.
A mirrored tree at the repo root: every source file's margin is the same path under .gloss/, with
.md appended.
src/lens/applyLens.ts → .gloss/src/lens/applyLens.ts.md
src/fieldMap/types.ts → .gloss/src/fieldMap/types.ts.md
packages/api/src/hooks.ts → .gloss/packages/api/src/hooks.ts.md
Enumeration respects .gitignore: a gitignored source file (generated SDK clients, build
output) is never linted, harvested, or checked, tracked or not. Outside a git repo the full
tree is walked.
.gloss/ is committed and reviewed like any docs tree — the one exception is
.gloss/.events.jsonl (machine-local harvester state), which setup gitignores. A gloss file exists
only when a source file has something in its margin; comment-free files get no mirror.
Everything else in a source file is harvestable, including JSDoc.
1. // why: — a load-bearing constraint the code can't show. Must-read-now, stays inline,
interrupts on purpose.
// why: Stripe replays webhooks for up to 3 days; the ledger insert must stay idempotent on eventId.
await ledger.upsert({ where: { eventId }, create: entry, update: {} });2. Daggers — content-free existence markers. Fixed strings; any trailing content is a lint
error. // gloss:file at the top of a file says a file-level preamble exists; // gloss on its own
line immediately above a declaration says that symbol has a gloss section.
// gloss:file
// gloss
export const resolveMarkerTarget = (source: ParsedSource, line: number) => { ... };3. Machine directives — recognized by pattern, extensible per repo: eslint-disable*,
biome-ignore, @ts-expect-error / @ts-ignore / @ts-nocheck, prettier-ignore,
/// <reference, shebangs, license headers, #__PURE__, webpack/vite magic comments, istanbul/c8,
@vitest-environment, sourceMappingURL.
// biome-ignore lint/suspicious/noExplicitAny: the resolver hands back an untyped AST nodeRepos with their own machine-read comment forms extend the recognized set in package.json —
regex sources tested against the full comment text:
{ "gloss": { "directives": ["@atlas\\b"] } }A config error (non-array, non-string entry, invalid regex, unparseable package.json) throws
rather than degrading to no patterns — silently dropping a directive pattern would reclassify
protected machine comments as harvestable and the next harvest would sweep them.
Vendored trees are excluded by path. Enumeration otherwise skips only what git check-ignore
skips, so a tracked-but-not-ours directory — a pinned upstream mirror, a generated client — gets
harvested, editing files the repo forbids editing and filing upstream's commentary as our margin:
{ "gloss": { "exclude": [".claude/skills"] } }Entries are path prefixes, never regexes or globs, matched at the segment boundary (vendor/skill
does not exclude vendor/skills). A leading ./ and trailing slashes normalize away; an empty
entry throws rather than excluding the whole repo. Excludes apply to every surface — harvest,
lint, check, fix.
Plain markdown, no frontmatter, no schema. An # <source path> h1, an optional file preamble, then
one ## <symbol> section per symbol. Class members key as Class.method; export default keys as
default. Linear and PR references are ordinary markdown links.
An entry that fits on one line is one bullet; anything longer is a paragraph. The ## <symbol>
heading is the entry's only binding — it is the one link the checker can verify (dagger ↔ section).
A quoted code line or a line number would answer "where does this apply" with a syntactic proxy
that rots silently; prose that needs finer-than-symbol placement is load-bearing by definition and
belongs inline as // why:.
# src/resolver.ts
The single marker→symbol resolver. The ESLint rule and the CI check both consume this — two
implementations of "which declaration does this marker belong to" would diverge and produce
lint-passes/CI-fails split-brain.
## resolveMarkerTarget
- a decorator is part of the declaration it decorates
Skips blank lines and `// why:` lines between the dagger and the declaration. Rejects
multi-declarator `const a = 1, b = 2` rather than guessing which half was meant. See
[ZLT-1204](https://linear.app/...).
## ParsedSource
`markerLine` is absent for symbols reached without a dagger; the checker uses that to find sections
whose dagger was deleted.The write path is mechanical, not instructional. Comment the way you are trained to; the harvester
sweeps every non-directive, non-why:, non-dagger comment out of the source into the enclosing
symbol's gloss section and plants the dagger. Nothing depends on an agent remembering a CLAUDE.md line through a long session, and nothing
is deleted to satisfy lint: the spillway fills by machine.
The calm default is harvest-at-commit (gloss harvest --staged in pre-commit). gloss watch is
opt-in per dev process and heavily debounced — it must never rewrite a file mid-edit, which is the
one real race here: an agent whose Read predates the harvest is holding drifted content.
gloss read prints each section under a header it derives at read time:
## resolveMarkerTarget written 2026-06-14, symbol changed 3× since
- a decorator is part of the declaration it decorates
The staleness note is derived from git at read time — a stored date or line number would rot, a derived one cannot.
Both timestamps come from git — the mirror's history for the note, a numeric git log -L <start>,<end>:<file> over the AST-resolved symbol span for the code. Nothing is written into any
file, so nothing can be rubber-stamped fresh. On a shallow clone the header refuses rather than
reporting the clone boundary as "last touched"; after a paired git mv it walks back a hop rather
than letting a rename-only commit launder a stale note into a fresh one.
Run as bunx gloss <cmd>, or bun src/cli.ts <cmd> in this repo.
| Command | |
|---|---|
gloss lint [paths] |
Flag source comments that are not // why:, a dagger, or a machine directive. |
gloss check |
Bidirectional dagger ⇔ section audit, both directions. Blocking in CI; also the rename/move/orphan detector. |
gloss fix |
Repair what check found: re-pair renamed symbols, move mirrors after a git mv, rewrite header paths. |
gloss harvest [paths] |
Sweep harvestable comments into the gloss and plant daggers. --staged for the pre-commit path. |
gloss watch |
Harvest on save, debounced. Opt-in per dev process. |
gloss read <file> [symbol] |
Print the gloss with the derived staleness header per section. |
gloss history <file> [symbol] |
Per-section changelog — git log -p scoped to one section. |
gloss setup [root] |
Create .gloss/, install the CLAUDE.md block. Idempotent; upgrades an existing block in place. |
bun add -d @inixiative/gloss
bunx gloss setupsetup creates .gloss/ with a header README and appends the agent instructions from
snippets/CLAUDE.gloss.md to your CLAUDE.md, between <!-- gloss:begin --> / <!-- gloss:end -->
markers. Re-running it replaces the block between those markers with the current snippet, so an
upgrade is bunx gloss setup.
It deliberately does not touch git config, lefthook.yml, or your package.json. Wire the hooks
yourself from snippets/lefthook.gloss.yml:
pre-commit:
commands:
gloss-harvest:
glob: '*.{ts,tsx}'
run: bunx gloss harvest --staged
stage_fixed: true
pre-push:
commands:
gloss-check:
run: bunx gloss checkIf the repo publishes to npm, add .gloss to package.json files — our own agents read published
source in node_modules, and daggers must not dangle downstream.
The gloss is advisory. It is past-session commentary. It may be stale, it may have been wrong
when written. Trust the code and its // why: lines over it. This is not a new cost: truth was never
enforceable for comments either, and comments carried no staleness signal at all. The gloss is held
to the standard comments always actually had, plus a derived freshness header they never had.
Reading it is a second action, like git log or git blame — triggered by a visible dagger and
felt confusion, not performed on every read. A gloss consulted 10% of the time still beats a comment
taxing 100% of reads.
The filing rule. If deleting a gloss entry could change what correct behavior looks like, it was
misfiled — it belongs in // why:. A misfile is recoverable (the content is in the gloss rather than
nowhere, and review can promote it); a // why: that was never written is not.
Never delete a gloss section or a dagger to green a failing check. Run gloss fix. A check that
trains deletion gives the corpus a negative half-life — on every red rename, deleting the pair is the
cheapest green. Repair is built to be strictly cheaper than deletion, and the failure message says so.
inixiative/template#88 carries the ratified
design and the full two-round adversarial review — including the mandated changes (no funcname-based
git log -L, fix ships with the blocking check, one shared marker→symbol resolver) and the risks
accepted with eyes open.