epistemic-mapper
Writes down what your project knows, what it guesses, and what could go wrong before handing it off.
Installation
Paste this into Claude Code, Cursor, or any agent that can run commands.
SKILL.mdShow the author's original SKILL.md
---
name: epistemic-mapper
description: "Build or update a canonical EPISTEMIC-MAP.md sorting a project's knowledge into four states: validated Facts, open Questions, undocumented Assumptions, and inferred Risks. Purpose-built for prototype-to-production handoff, where a signed contract means a new engineering team must take over before the original builder's tacit knowledge walks out the door. Spawns subagents across docs and the codebase to find what's proven, what's asked, what's silently assumed, and what nobody has noticed, then runs a dedicated synthesis pass for systemic risks no single source states. Use whenever a user mentions \"epistemic map\", \"knowledge map\", \"EPISTEMIC-MAP.md\", \"known unknowns\", \"unknown unknowns\", \"what do we actually know vs assume\", \"map our assumptions\", \"handoff risk map\", \"prototype handoff\", or \"onboarding a team onto a prototype\". Always prefer this skill over ad-hoc knowledge-gap documentation."
license: Apache-2.0
metadata:
author: accelint
version: "1.0.1"
---
# Epistemic Mapper
Extract a project's knowledge state from its documentation and its code, then
sort every observation into one of four quadrants and write the result into
a canonical `EPISTEMIC-MAP.md`. This is a handoff artifact: it exists because
a prototype builder holds a mass of tacit knowledge that never got written
down, and once a contract is signed and a team takes over, that knowledge
either gets captured now or gets rediscovered the hard way, in production.
```
HIGH CERTAINTY (validated)
|
[ ASSUMPTIONS ] | [ FACTS ]
Unknown Known | Known Known
nobody wrote it | proven, cited
down, but the | evidence
code/docs rely |
on it |
------------------------ + ------------------------
|
[ RISKS ] | [ QUESTIONS ]
Unknown Unknown | Known Unknown
blind spots, found | explicit open
only by looking | gaps, already
across everything | flagged somewhere
|
LOW CERTAINTY (unvalidated)
```
## Relationship to Other Living Documents
`CONSTRAINTS.md` holds externally-imposed hard limits. `JARGON.md` holds term
definitions. `ARCHITECTURE.md` holds structural decisions. `AGENTS.md` holds
agent behavior and orchestration. `EPISTEMIC-MAP.md` is different in kind
from all four: it isn't a ledger of what's true, it's a map of *how sure we
are* about what's true. Read the other living docs (if present) before
extraction starts, for two reasons:
1. **Don't duplicate.** If a Known Known is already stated in `CONSTRAINTS.md`
or defined in `JARGON.md`, reference it by file and section instead of
restating it as a new Fact. `EPISTEMIC-MAP.md` should point at the other
living docs, not compete with them.
2. **Seed confidence.** A claim backed by an entry in `CONSTRAINTS.md` is
about as validated as a claim gets — treat it as a Known Known with
`CONFIRMED` confidence by default, not something to re-litigate.
Unlike the other living documents, which accumulate, **this one is supposed
to shrink.** Every Assumption that gets validated becomes a Fact. Every
Question that gets answered gets marked resolved. Every Risk that gets
investigated either gets fixed (closes) or gets formally accepted (moves to
Known Unknown, now tracked instead of hidden). A re-run with a flat or
growing unresolved count is a signal that the handoff isn't actually
progressing — say so plainly in the executive summary rather than treating
a big file as a sign of thoroughness.
## NEVER Do When Building an Epistemic Map
- **NEVER invent a new category slug.** Pick the closest of the five
canonical slugs by where the consequence lands, and put the specific
pattern in the statement/reasoning text instead — `merge_epistemic_map.py`
refuses anything else and explains the fallback rule in the error itself.
Worked example: `references/schema.md`.
- **NEVER paraphrase or reconstruct the findings schema from memory when
writing subagent prompts.** Quote `references/schema.md`'s field names
and quadrant values verbatim. A renamed field (`observation` for
`statement`) or a shortened quadrant (`fact` for `known-known`) causes
every entry from that subagent to fail validation — or worse, be
silently dropped if the mismatch is at the top-level key instead of a
field inside it.
- **NEVER create a separate state or cache file** (`.epistemic-map-state.json`
or similar). All state lives in `EPISTEMIC-MAP.md`'s own rendered text,
parsed back in by `merge_epistemic_map.py` on every run — see
`references/schema.md` if a run seems to need somewhere else to persist
state.
- **NEVER truncate, summarize, or write a placeholder in
`EPISTEMIC-MAP.md`** in place of full content — no "N total, see X for
the complete list," no "showing top K of N," no pointer to a
`consolidated.json`, a `backlog.md`, or any other file that isn't
`EPISTEMIC-MAP.md` itself. The file is self-contained; every entry the
merge script produced belongs in it, however long that makes the file. A
large entry count is never a reason to summarize — see "Correlating and
Deduplicating" above for why volume doesn't require the agent to hold or
author entries by hand in the first place.
- **NEVER create an intermediate "consolidated" or "merged" findings
file.** Pass every subagent's scratch file directly to a single
`merge_epistemic_map.py` invocation via multiple `--findings` arguments.
The script does the correlation; there is nothing to pre-merge.
- **NEVER hand-draft or restructure `EPISTEMIC-MAP.md`.** Every write goes
through `merge_epistemic_map.py` — a hand-edit won't just look
inconsistent, it will fail to parse on the next run.
- **NEVER add a "Related Documentation" section to `EPISTEMIC-MAP.md`
itself.** Linking is one-directional: `AGENTS.md`/`CLAUDE.md` point to
this file, not the reverse.
- **NEVER write scratch or findings JSON with a relative path, or inside
the repository being scanned.** Absolute `/tmp/...` paths only —
`merge_epistemic_map.py` refuses relative paths outright.
- **NEVER invent an entry not supported by evidence** (Facts, Questions,
Assumptions) **or a reasoning trail** (Risks). Mark it `INFERRED` rather
than asserting certainty, and never silently upgrade confidence to make a
finding look more solid than it is.
- **NEVER crawl `openspec/changes/archive/` automatically** — only read
specific archive files if the user names them explicitly.
- **NEVER run subagent extraction serially when subagents are available** —
dispatch one per source simultaneously, capped at 5 per wave.
## The Quadrant Test
Every candidate observation goes through this triage, in order. Stop at the
first `YES`.
```
CANDIDATE OBSERVATION
|
v
Q1: Backed by evidence (data, contract, test result,
executed code path, explicit recorded decision)?
|
YES -+----------------------------> KNOWN KNOWN (Fact)
| NO
v
Q2: Is the gap explicitly raised somewhere (a TODO,
an open question in a doc, a ticket, an unresolved
thread)?
|
YES -+----------------------------> KNOWN UNKNOWN (Question)
| NO
v
Q3: Does the code or docs silently rely on this without
anyone stating it outright?
|
YES -+----------------------------> UNKNOWN KNOWN (Assumption)
| NO
v
(handled separately, see Synthesizing Risks below)
-> UNKNOWN UNKNOWN (Risk)
```
`references/quadrant-test.md` has the full version with worked examples per
quadrant and a redirect table for things that look epistemic but aren't
(team preferences, decided facts that already live in `CONSTRAINTS.md`,
glossary terms). Read it before the first extraction wave.
Note what this test structurally implies: Facts, Questions, and Assumptions
can all be found by a subagent reading one source closely. Risks cannot — if
a single source stated the risk, it would already be a Question or an
Assumption. Risks only emerge from looking at the whole picture at once,
which is why they get their own pass below instead of coming out of the
same extraction wave.
## Discovering Sources
Two source types, both in scope:
- **Documentation.** Auto-scan conventional folders — `docs/`, `documents/`,
`README.md`, and similar — the same discovery behavior as
`constraints-extractor`. Accept explicit paths the user adds. Exclude
`openspec/changes/archive/` by default; it's historical record, not
current knowledge. **Also exclude `CONSTRAINTS.md`, `JARGON.md`,
`ARCHITECTURE.md`, `AGENTS.md`/`CLAUDE.md`, and `EPISTEMIC-MAP.md` itself
from this scan** — they're read separately below for cross-referencing,
not re-extracted as sources. A broad doc glob that sweeps these up too
produces circular or duplicate entries (re-"discovering" a fact that's
already a validated `CONSTRAINTS.md` entry) and wastes a subagent's pass
on content that's already known.
- **The codebase.** Prototypes carry more tacit knowledge in code than in
prose — a hardcoded region, a `// TODO: handle retries`, a try/catch that
silently swallows an error, a config value nobody explains. Identify the
main source directories (`src/`, `lib/`, `app/`, or whatever the repo
actually uses) and treat each top-level module or package as its own
source for extraction purposes.
Read any existing `CONSTRAINTS.md`, `JARGON.md`, `ARCHITECTURE.md`, and
`AGENTS.md` up front, per the section above — these seed Known Knowns and
tell you what not to restate. **This read and the source-discovery glob
above are independent of each other — issue them as parallel/batched tool
calls in the same turn, not one after another.** Neither depends on the
other's result, and serializing two cheap, independent operations only adds
latency for no benefit.
**Optional interview pass.** Pass `--interview` to enable a short, targeted
question phase after correlation (below), for when the original builder is
still reachable. Off by default — don't require an interview session to
produce a useful map, since the builder may already be gone by the time this
runs.
## Extracting Facts, Questions, and Assumptions
**Read `references/schema.md` in full before writing a single subagent
prompt.** Every subagent's task instructions must include the exact JSON
shape from that file, quoted directly — the `entries` top-level key, field
names (`statement`, `evidence`, `impact`, `confidence` — never `observation`
or another synonym), and quadrant values written exactly as the hyphenated
`known-known` / `known-unknown` / `unknown-known` strings, never a short
form like `fact` or `question`. Do not reconstruct the schema from memory or
from this file's summary of it — paraphrasing here is exactly how a field
gets renamed or a quadrant gets shortened in a subagent's output, and
`merge_epistemic_map.py` validates strictly against the schema as written,
not against a close approximation of it.
Spawn one subagent per source — one per doc, one per top-level code module —
capped at 5 concurrent per wave, matching the rest of this skill family.
Each subagent applies the Q1/Q2/Q3 test to everything in its source and
reports back a flat list of candidates in the Q1-through-Q3 quadrants only.
Risk is out of scope for this wave; a subagent looking at one file has no
way to tell a real systemic risk from an isolated oddity.
Every Fact, Question, and Assumption needs at least one evidence pointer —
a file (relative path from the project root, not an absolute path), a line
range or section, and a short quote or paraphrase of what's there. No
evidence, no entry. This mirrors `constraints-extractor`: ambiguity is fine,
an unsupported claim is not.
Subagents write scratch findings to **absolute paths only** (e.g.
`/tmp/epistemic-mapper/<source-id>.json`) — never a relative path, which
resolves against the repo's working directory and leaves stray JSON files
in the root. Refuse and re-request if a relative path is about to be used.
## Correlating and Deduplicating
Merge the wave's findings with `scripts/merge_epistemic_map.py`, which
handles the mechanical parts: ID assignment, category slug canonicalization
against the alias table in `references/schema.md`, duplicate collapsing
across sources, and ordering. This is not judgment work — don't hand-roll
it, and don't pre-merge it either.
**One invocation, every scratch file passed directly.** Every subagent's
scratch file (from every wave) plus the risk-synthesis file goes to a
*single* run of the script, as multiple `--findings` arguments:
```bash
python3 scripts/merge_epistemic_map.py \
--findings /tmp/epistemic-mapper/doc-1.json /tmp/epistemic-mapper/doc-2.json ... /tmp/epistemic-mapper/risks.json \
--output-dir /absolute/path/to/repo/root
```
There is no manual pre-merge step, no intermediate "consolidated" file, and
no reason for one — the script reads every findings file straight off disk
and does the dedup itself. This means entry volume is never something the
orchestrating agent needs to hold in its own context or author by hand: 13
source documents producing 106 total candidates is the script reading 14
small files and writing one output file, the same amount of orchestration
work as 2 sources producing 4 candidates. If a run feels like it's
generating "too much" to write out, that's a sign the script isn't being
invoked directly on the raw scratch files — stop and call it that way
instead of summarizing.
While merging, assign a confidence tier to each entry:
- **CONFIRMED** — two or more independent sources agree.
- **INFERRED** — a single source, reasonably read.
- **CONFLICTING** — sources disagree. Don't average them into a false
middle; keep both readings visible and flag it. A CONFLICTING entry in
the Facts quadrant is a contradiction worth escalating on its own — two
docs stating different rate limits for the same API is itself a finding.
## Synthesizing Risks
After correlation, run one dedicated pass over the full merged picture —
not per-source, across everything at once. Look for the patterns that only
show up in aggregate:
- A dependency that three separate modules assume works a certain way, that
nothing in the codebase or docs ever confirms.
- A single vendor, region, or credential with no fallback mentioned
anywhere, especially where multiple Assumptions already cluster around it.
- A structural pattern common to failed handoffs — no error handling on an
external call, no documented rollback path, a "temporary" decision with
no owner or expiry — that this specific project also exhibits, whether or
not anyone here has said so.
Because no source states a Risk outright, evidence citations don't apply.
Instead, write a **reasoning trail**: which observations, taken together,
point at the risk, and why the combination matters more than any one of
them alone. This is the one quadrant where the burden is on synthesis and
judgment rather than citation — say so plainly rather than manufacturing a
citation that doesn't exist.
**Risks still need one of the five canonical category slugs — never a new
one.** A synthesized risk is cross-cutting by nature (that's what makes it
a Risk rather than a Fact filed under one source), so it will often feel
like it deserves a more precise label than any of the five categories in
`references/schema.md` offers. Resist that. The category field answers
"where does the consequence land," not "what kind of risk pattern is
this" — a cascading schedule dependency across three deliverables isn't a
new `schedule-cascade-risk` category, it's `business-scope` if the missed
commitment is externally facing, or `team-process` if it's an internal
bottleneck. Pick the closest existing slug based on where the impact
actually lands, and put the specific mechanism in the `statement` and
`reasoning` text, where the precision belongs. `merge_epistemic_map.py`
will refuse to guess at an invented category — see the worked example in
`references/schema.md` for exactly this situation.
## Assembling the Backlog
`merge_epistemic_map.py` generates the Epistemic Backlog automatically —
3-5 action items, ranked by quadrant and severity (Risks and Assumptions
generally outrank Questions, since an unexamined blind spot is more
dangerous to a new team than a known open question). It always lands as
the `## Epistemic Backlog` section inside `EPISTEMIC-MAP.md` itself.
**There is no separate `backlog.md` file, and there never should be.** Do
not create one, do not reference one, do not write "see backlog.md" instead
of the actual checklist. If the backlog needs turning into real tickets
rather than a checklist, offer to hand the top items off to a ticket-creator
skill if one is available — but that produces tickets, not a markdown file,
and it's an offer, never automatic. The checklist inside `EPISTEMIC-MAP.md`
always stands on its own even if the offer is declined.
## Previewing and Writing EPISTEMIC-MAP.md
`EPISTEMIC-MAP.md` is one self-contained file, and it's plain markdown —
no frontmatter, no hidden block, no companion state file. Every field the
merge needs to round-trip (id, category, confidence, evidence, impact,
history, severity, first-seen date) is folded directly into each entry's
own rendered lines — see `references/schema.md` for the exact per-entry
format. `merge_epistemic_map.py` parses that same text back in as state on
the next run, the same approach `constraints-extractor` already uses. Two
files (or a visible block plus a hidden one) is how a handoff artifact
gets copied, moved, or reviewed with half of it left behind, or ends up
carrying the same content twice for no reason — earlier drafts of this
skill did exactly that with a YAML frontmatter block, and it roughly
doubled the file's size for zero added information. Don't reintroduce it.
**`merge_epistemic_map.py` is the only legitimate way this file gets
written or changed.** Never hand-draft or restructure `EPISTEMIC-MAP.md`
directly — not a heading, not a single line inside an entry. Write
findings to scratch JSON per `references/schema.md` and run the script.
The file is regenerated in full on every run from what the script parses
back in; a hand-edit doesn't just fail to survive a re-run, it will make
the next run fail to parse the file at all, on purpose — see "NEVER Do"
above. The file's structure is exactly what the script produces — Facts,
Questions, Assumptions, Risks, Epistemic Backlog, nothing else. If a task
seems to call for a different section (a cross-link note, a summary file,
anything not in that list), that's a signal to say so in the conversation
rather than add it to the file.
Always preview before writing, including small updates to an existing
file — same convention as the rest of this skill family. On a re-run, the
script reads prior state directly out of the existing file's own rendered
entries (there is nothing else to read it from) and marks:
- **Promotions** — an Assumption or Question that now has evidence moves
quadrants, and the entry gains a `History (DATE): was -> became — reason`
line. All of an entry's history lines are kept, not just the latest.
- **Resolutions** — a Question that's been answered or a Risk that's been
fixed gets marked `resolved` with a short note, not silently deleted.
Institutional memory of what used to be uncertain has value.
- **Dismissals** — a Risk investigated and found not to apply gets marked
`dismissed` with the reasoning kept, for the same reason.
Never overwrite state silently. If a run finds zero new or changed entries,
say so plainly in the executive summary rather than padding the file to
look productive. If `EPISTEMIC-MAP.md` exists but doesn't match what
`merge_epistemic_map.py` itself would render — a hand-edit, a heading
missing a field, an old frontmatter-based file from a previous version of
this skill — the script refuses to run rather than guessing at prior state
and silently losing promotion history. Surface that error to the user
instead of working around it.
## Cross-Linking Into AGENTS.md / CLAUDE.md
This is the one exception to "the script owns the file" — a separate,
one-time prose edit to a *different* file, done after `EPISTEMIC-MAP.md`
has already been written and previewed. It never touches
`EPISTEMIC-MAP.md`'s own generation.
If `AGENTS.md` or `CLAUDE.md` exists at the target location, check whether
it already has a "Related Documentation" section (or equivalent)
referencing `EPISTEMIC-MAP.md`. If not, offer to append a one-line
reference in the same style the file already uses — matching how
`jargon-extractor` links `JARGON.md` and `constraints-extractor` links
`CONSTRAINTS.md`. This is a plain prose edit, not a companion script: a
single presence check, not high-volume sorting or dedup, so there's no
ongoing mechanical work here that would justify one.
`AGENTS.md`/`CLAUDE.md` is read on every agent interaction, so a pointer
placed there is not a passive footnote — it surfaces every time the file
loads, regardless of whether the agent goes on to open `EPISTEMIC-MAP.md`
itself. Word it to push toward "actually consulted," not just
discoverable:
```markdown
- **EPISTEMIC-MAP.md**: What's proven, what's assumed, and what's still unknown about this project; most relevant during handoff.
_(Check this before making changes in an area flagged as an open Question or Assumption — treat an entry there as something to verify, not something to rely on)_
```
**The linking is one-directional** — see the NEVER-list rule above.
`AGENTS.md`/`CLAUDE.md` points to `EPISTEMIC-MAP.md` via this step, never
the reverse.
## Reference Files
- `references/quadrant-test.md` — worked examples per quadrant and a
redirect table for near-misses (the triage test itself is the flowchart
above). Read before extraction.
- `references/template.md` — the full `EPISTEMIC-MAP.md` skeleton used for
new-file creation.
- `references/schema.md` — the JSON shape subagents write to scratch files,
the category alias table, and the shape `merge_epistemic_map.py` expects.
**Mandatory reading before dispatching subagents** — quote its exact
field names and quadrant values into subagent prompts rather than
paraphrasing; see "Extracting Facts, Questions, and Assumptions" above.
Ships with 7 supporting files:
- CHANGELOG.md
- evals/evals.json
- README.md
- references/quadrant-test.md
- references/schema.md
- references/template.md
- scripts/merge_epistemic_map.py
Mirrored from the author's public source. Install counts from the open skills registry.