Skip to content

AGENTS.md

This file provides guidance to Claude Code (claude.ai/code) and other coding agents when working in this repository.

What this repo is

Chemical Safety Assistant. Nothing is built yet. The only source material is chemical-safety-assisant-docs.pdf (211 KB, scanned images — no text layer; pdftotext-style extraction returns garbage). To read it, use the Read tool with pages: after installing poppler (brew install poppler), or a vision pass. Every requirement extracted from it must land in docs/knowledge/research/ with a page citation before it is used downstream.

The primary asset of this repo right now is the pipeline below, not code.


The pipeline

Five stages. Each consumes the previous stage's artifact and emits a new one with a Coverage table proving nothing was dropped.

#CommandReadsWritesID prefix
0/researchthe PDF, primary sourcesdocs/knowledge/research/<topic>.mdR-nn
1/grill-with-docsstage 0 findingsCONTEXT.md glossary + docs/adr/NNNN-*.mdD-nn
2/to-specstages 0–1.scratch/<feature>/PRD.mdUS-nn
3/to-ticketsstage 2.scratch/<feature>/issues/NN-<slug>.mdT-nn
4/wayfindera fog-bound effort.scratch/<effort>/map.md + child issuesW-nn

The agent cannot drive this chain

/grill-with-docs, /to-spec, /to-tickets, /wayfinder and /handoff all carry disable-model-invocation: true. A human types them. Only /research and /domain-modeling are model-invocable.

So when a stage's exit criteria are met, do not attempt the next stage. Print:

Stage N complete → <artifact path>
Exit criteria met: <one line each>
Next: type /<command>

Then stop. An agent that "continues the pipeline" on its own has silently skipped a human gate.

Where wayfinder actually sits

/wayfinder is listed as stage 4 but it is not "the step after tickets". It is the alternate entry point for an effort too big to hold in one session: it charts a map of investigation tickets, and resolving those tickets is what produces the inputs to stages 1–3. Use it when the destination is fogged; use stages 0→3 straight through when it is not. Either way the Coverage discipline below is unchanged.


Traceability: the load-bearing rule

The user's stated goal is "content was not missing from documents". That is enforced mechanically, not by care.

  1. Assign a stable id at creation. R-01, D-01, US-01, T-01, W-01. Ids are append-only. Never renumber, never reuse, never close a gap. A deleted item becomes a tombstone row, not a missing number.

  2. Every downstream artifact ends with a ## Coverage table mapping every upstream id to where it landed:

    markdown
    ## Coverage
    
    | Upstream | Landed in | Note |
    |----------|-----------|------|
    | R-01 | US-03, US-04 | |
    | R-02 | D-02 (ADR-0002) | |
    | R-03 | — | Dropped: out of scope, see Out of Scope §2 |
  3. A blank cell is a bug. is only legal with an explicit Dropped: reason.

  4. Verification is a set diff, runnable by any agent or human:

    bash
    ./init.sh    # the gate: runs every check, reports them together, one exit code

    Run it before declaring any stage complete. If it fails, the stage is not complete. Run the second after editing a template or the checker — it walks a synthetic pipeline through all four boundaries and asserts a dropped id still fails.

  5. Coverage proves an id landed. It does not prove the downstream claim is true. Each row carries an Evidence cell — the concrete thing a reader can check ("acceptance criteria name the p.12 limit", "ADR-0002 linked"). A row with a landing and no evidence is a claim, not a check.

  6. The check is one-way, in three named cases — collapsing them into one "extra downstream is fine" rule silently swallows the typo class:

    CaseVerdict
    Declared upstream, absent from every downstream Coverage tableerror — research may not evaporate into a spec
    Declared downstream with no upstream idlegal — a spec may exceed its research
    A Coverage row citing an id declared nowhereerror — phantom reference: a typo, or a half-landed rename

    The checker is stage-aware: an id needs downstream coverage only once a later stage exists in the repo. So finishing stage 1 does not fail merely because no PRD has been written yet — but the moment the first US-nn appears, every D-nn must be mapped.


Ping-pong review (two models)

Every stage artifact is reviewed by a second model before the human is told the stage is done. Roles, not personalities:

  • Proposer — writes the artifact. Default claude-opus-5.
  • Griller — attacks it. Default claude-opus-4-8 (a genuinely different model, so it does not inherit the Proposer's blind spots).

How to actually get a second model

Claude Code's Agent tool takes only the aliases sonnet | opus | haiku | fable — it cannot pin claude-opus-4-8. Two real options:

  1. Cross-session (preferred, true two-model). The human opens a second terminal:
    bash
    claude --model claude-opus-4-8
    Then this session finds it with ListAgents and drives it with SendMessage, passing the artifact path plus the grill prompt. The Griller replies in-thread.
  2. Same-model second pass (fallback when no second session exists).Agent(subagent_type: "fork") — inherits full context, so it grills with everything this session knows, but shares its priors. Say in the round log that it was a fork, not a cross-model grill; the two are not equivalent evidence.

Grill prompt (send verbatim, then the artifact path)

You are the Griller. Do not improve the document. Find what is WRONG or MISSING.
1. Set-diff every upstream id against the Coverage table. Name every id that is
   absent, or mapped with no reason.
2. Name every claim with no source citation.
3. Name every term used that is not in CONTEXT.md.
4. Name every decision stated as fact that was never decided in an ADR.
Output only a numbered defect list. No praise. No rewrites. "No defects" is a
legal answer only after you have run the set diff and can say so.

Rounds

Ping-pong until the Griller returns zero defects or two consecutive rounds surface only defects the Proposer has already accepted as won't-fix. Cap at 3 rounds; if defects are still landing at round 3, the stage input is too big — split it and restart.

Log every round to docs/knowledge/journal/YYYY-MM-DD.md (see below). A stage marked complete with no round log did not happen.


The journal — tracking every change and prompt

One file per day: docs/knowledge/journal/YYYY-MM-DD.md. Append, never rewrite. Write an entry at every stage transition, every ping-pong round, and every human decision that changed direction.

markdown
## 14:20 — Stage 2 /to-spec, round 2

**Actor:** Proposer (claude-opus-5, session chem-assistant-de)
**Prompt:** <the instruction that produced this, verbatim or a path to it>
**Artifact:** .scratch/ghs-labelling/PRD.md @ a3f1c9d
**Defects from Griller (claude-opus-4-8):** R-07 unmapped; "hazard class" not in glossary
**Resolution:** US-11 added for R-07; glossary term added (D-05)

@ a3f1c9d is the git commit that holds that exact version. Commit each artifact before logging it so the reference resolves. That commit hash is the version reference the user asked for — journal entries without one point at nothing.


Knowledge layout

AGENTS.md                       ← this file: the pipeline
CONTEXT.md                      ← central knowledge: ubiquitous language, glossary
docs/adr/NNNN-<slug>.md         ← decisions, one per file, never edited after Accepted
docs/agents/                    ← skill configuration (tracker, labels, domain rules)
docs/knowledge/research/        ← R-nn findings, each claim cited to a primary source
docs/knowledge/journal/         ← append-only log of every prompt, round and change
.scratch/<feature>/PRD.md       ← US-nn specs
.scratch/<feature>/issues/      ← T-nn tickets
.scratch/<effort>/map.md        ← wayfinder maps

Central vs module knowledge. CONTEXT.md + docs/adr/ are central — anything a second module would need to agree with. Module-local knowledge lives beside the module once code exists (src/<module>/CONTEXT.md, src/<module>/docs/adr/); at that point add a root CONTEXT-MAP.md pointing at each, and docs/agents/domain.md switches to multi-context. Until then, single-context.

A term used in any artifact must exist in CONTEXT.md. That is a grill check, not a style note.


Agent skills

Issue tracker

Local markdown under .scratch/. No remote; gh/glab are unavailable. See docs/agents/issue-tracker.md.

Override: /to-tickets says to write a single tickets.md at the repo root for local trackers. This repo does not do that — tickets go to .scratch/<feature>/issues/NN-<slug>.md, one file each, matching the wayfinder layout so Status: / Blocked by: lines mean the same thing everywhere.

Triage labels

open / in progress — <why> / resolved, written as a Status: line in each issue file. These are what the tickets use, not a default scheme imported from elsewhere — the imported one was never adopted and was removed on 2026-09-20. See docs/agents/triage-labels.md.

Domain docs

Single-context: CONTEXT.md + docs/adr/ at the root. See docs/agents/domain.md.

Templates

Start every artifact from docs/agents/templates/research.md, adr.md, prd.md, ticket.md, wayfinder-map.md, wayfinder-ticket.md, journal-entry.md. They already carry the id and Coverage shapes the checker enforces, so an artifact written from scratch usually fails it.


Session start

  1. ./init.sh first, before reading anything else. Red before you start is not yours — find out whose before you add to it. A session that inherits a red gate and says nothing has adopted someone else's bug.
  2. Read session-handoff.md — state-of-now: active stage, uncommitted tree, blockers, next step, and the dead ends already walked. This is not the journal.
  3. Read CONTEXT.md for vocabulary.
  4. Read today's and yesterday's journal file — history, and the commit shas.
  5. Scan .scratch/*/issues/ for the frontier: open, unblocked, not in progress.

Do not re-read the whole docs/knowledge/ tree. The journal is the index.

Handoff vs journal — they are different files and both are required. The journal is append-only history: what happened, with a commit sha. session-handoff.md is a single overwritten file: where things stand right now. History cannot answer "is the tree dirty"; state-of-now cannot answer "why was this decided". Keeping only one loses the other.

Session end

  1. Run ./init.sh; paste the summary block into session-handoff.md.
  2. Commit the artifacts, then append a journal entry citing the sha.
  3. Overwrite session-handoff.md from docs/agents/templates/session-handoff.md. The uncommitted-tree line is mandatory — an agent that assumes a clean tree and runs a checkout destroys the previous session's work.
  4. /handoff additionally, if context is running low. It writes to the OS temp dir, not the repo — so record its path in the journal, or the next session cannot find it.

Traps that bite in this repo

Each one costs a debugging session for an agent that does not know it.

  • Never gate on grepped output. ./init.sh | grep -q ok && commit matches a summary line whether the run passed or failed. Gate on the exit code.
  • Never chain gates with &&. An abort-on-first-error chain means a long-red gate hides every gate behind it. init.sh runs all of them unconditionally and reports them together; do not "tidy" it.
  • A gate that fails on its own documentation gets deleted by the next person.coverage_check.py scans only docs/knowledge/research/, docs/adr/, .scratch/ and CONTEXT.mdnot docs/agents/templates/, whose files are full of id-shaped placeholders. It matches ids structurally: leading a heading or list item to declare, first cell of a Coverage-table row to cover. Prose saying "per D-07" does not declare anything. Keep both properties if you touch it.
  • .scratch/ is NOT disposable here, despite the name. It holds the middle three stages of the traceability chain (PRD, tickets, maps). It is committed and deliberately not in .gitignore. Do not sweep it, and do not add it to an ignore file.
  • Do not hand-maintain a state registry. The frontier is derived — scan .scratch/*/issues/ for open, unblocked, not in progress. A feature_list.json beside it would be a second source of truth for the same fact, and it drifts. If scanning ever becomes too slow or too lossy, add an index the gate regenerates and diffs, never one a human or agent edits by hand.
  • Ids are machine identity, not vocabulary. coverage-check needs D-07; a human reading prose needs the decision's name. Write "the single-tenant ruling (D-07)", never a bare "per D-07". Once prose is made of ids, a renumber silently rewrites history — which is also why ids are never renumbered.

Rules that override defaults

  • Never renumber an id. Everything above depends on ids being permanent.
  • Never claim a stage is complete without the Coverage set-diff and a logged grill round. "Looks complete" is not the bar.
  • Never inline file paths or code snippets into a PRD or ticket — they go stale. Exception: a schema/state-machine snippet from a prototype that encodes a decision more precisely than prose.
  • The rejected option is the operative part of an ADR. Record the ruling in the decider's own words including what lost and why. If a change contradicts an Accepted ADR: stop and raise it, do not implement over it. Without the rejected option written down, the next agent sees a shape it would not have chosen and "improves" it back.
  • Cite the PDF by page whenever a requirement traces to it. It is scanned, so a future agent cannot grep for the sentence — the page number is the only handle.