---
name: complex-md
description: Generate COMPLEX.md for the current repository. Computes hotspot signals from the dependency graph and git history locally, writes the file per the COMPLEX.md spec, and wires it into the repo's agent context files. Use when asked to generate, update, or check COMPLEX.md.
---

# complex-md skill (spec 0.3, prompt 0.3.0)

You are inside a git repository. Produce a `COMPLEX.md` at the repository root
that conforms to spec 0.3 (https://complex.md/spec). Work in
three stages: compute the signals with the shell, write the file with the
generation prompt at the end of this document, then wire the file into the
repository's agent context files. All three stages are one job; do not stop
after the file is written.

## Stage 1: compute the signals

The CLI is the reference implementation of the analysis: it builds a
dependency graph of the working tree (imports, script and link tags,
stylesheet imports, shell and Caddy includes, path literals in scripts and
configs), reads the last 2,000 commits, classifies every file by kind, and
writes the front matter itself so no number passes through a model. Prefer
it whenever Node 18 or newer is available:

```sh
npx -y complex-md generate --agent --no-wire
```

This runs locally, calls no model, and writes two files: `.complex-md/prompt.md`,
the generation prompt with every input filled in (front matter, signals
table, load-bearing files, co-change pairs and seams, the profile and blind
spots, the text of the top hotspot files with their dependents and covering
tests), and `.complex-md/front-matter.yaml`. Read `.complex-md/prompt.md`
and go to Stage 2. Set `tool` to `complex-md-skill/0.3.0`.

### Fallback without Node

When `npx` is not available, compute the history axis with git and awk and
approximate the structural axis with grep. Say so: set `tool` to
`complex-md-skill/0.3.0 (shell)` and add the blind spot
`dependency graph approximated by text search; covering tests by name` to
the front matter. Scratch files go in `/tmp`.

1. Kinds and scope. Rankable files are source, config, markup and style;
   tests, docs, data, manifests, CI, generated, vendored and binary files are
   not ranked. As one extended regex over the repo relative path, the files
   to drop:

```sh
EX='(^|/)(docs?|tests?|__tests__|specs?|e2e|examples?|benchmarks?|fixtures?|testdata|vendor|node_modules|third[_-]?party|dist|build|out|coverage|generated|\.github|\.circleci)/|\.(md|mdx|rst|txt|adoc|lock|snap|map|min\.js|min\.css|json|csv|ndjson|xml|png|jpe?g|gif|svg|ico|woff2?|ttf|pdf|zip)$|\.(test|spec|tst|e2e)\.[a-z]+$|_test\.[a-z]+$|(^|/)test[_-]?[a-z0-9]*\.[a-z]+$|(^|/)(package|composer|tsconfig[^/]*|jsconfig)\.json$|(^|/)(Cargo\.toml|go\.mod|go\.sum|pyproject\.toml|setup\.py|Gemfile|pom\.xml|CHANGELOG[^/]*|LICENSE[^/]*)$'
```

2. Lines of code per rankable file; the row count is `files_analyzed`:

```sh
git ls-files | grep -vE "$EX" | xargs -d '\n' wc -l 2>/dev/null | grep -v ' total$' \
  | awk '{ print $1 "\t" $2 }' > /tmp/cx-loc.tsv
wc -l < /tmp/cx-loc.tsv
```

3. History over the last 2,000 non-merge commits (or all of them), skipping
   bulk commits over 30 files. Weighted churn gives each commit
   `0.5 ^ (age_in_commits / half_life)` with `half_life = max(50, N / 4)`.
   Fixes are commits whose message matches the fix pattern; do not match
   issue numbers. Output columns: `churn`, `churn_w`, `fixes`, `fixes_w`,
   `authors`, `owner_share`, `path`.

```sh
N=$(git rev-list --count --no-merges HEAD); [ "$N" -gt 2000 ] && N=2000
HL=$(( N / 4 )); [ "$HL" -lt 50 ] && HL=50
FIXRE='(^|[^a-z])(fix(es|ed|ing)?|bug|bugfix|hotfix|regression)([^a-z]|$)'
git log -n "$N" --no-merges --name-only --pretty=format:'@%ae%x09%s' \
  | awk -F'\t' -v hl="$HL" -v fixre="$FIXRE" '
    function flush(  i, w) { if (n == 0 || n > 30) return; w = 0.5 ^ (idx / hl)
      for (i = 1; i <= n; i++) { f = fl[i]; c[f]++; cw[f] += w; au[f, a]++
        if (isfix) { fx[f]++; fw[f] += w }
        if (au[f, a] == 1) na[f]++; if (au[f, a] > mx[f]) mx[f] = au[f, a] } }
    /^@/ { flush(); idx++; a = substr($1, 2); isfix = (tolower($2) ~ fixre); n = 0; delete fl; next }
    /./  { fl[++n] = $0 }
    END  { flush(); for (f in c) printf "%d\t%.2f\t%d\t%.2f\t%d\t%.2f\t%s\n", c[f], cw[f], fx[f] + 0, fw[f] + 0, na[f], mx[f] / c[f], f }' \
  > /tmp/cx-churn.tsv
```

   Record `window_commits` (`$N`), `commits_total`, the first and last commit
   dates in the window, and the committer count
   (`git log -n "$N" --no-merges --format=%ae | sort -u | wc -l`).

4. Co-change pairs among rankable files and docs, excluding convention files
   (any file in more than a quarter of the commits, and changelogs), with
   `coupling` = shared commits over the quieter file's commits, in percent.
   Keep pairs with at least `max(3, min(10, N / 150))` shared commits and
   coupling of 34 or more, top 10 by count. Output: `count`, `coupling`,
   `path a`, `path b`.

```sh
PM=$(( N / 150 )); [ "$PM" -lt 3 ] && PM=3; [ "$PM" -gt 10 ] && PM=10
git log -n "$N" --no-merges --name-only --pretty=format:'@' \
  | grep -vE '(^|/)(CHANGELOG|CHANGES|HISTORY|NEWS)[^/]*$|\.(test|spec|tst)\.[a-z]+$|_test\.[a-z]+$|(^|/)(tests?|__tests__|\.github|node_modules|vendor|dist|build)/|\.(lock|json|snap|map|png|jpe?g|svg)$' \
  | awk -v pm="$PM" '
    function emit(  i, j, x, y, s) { if (n == 0 || n > 30) return; commits++
      for (i = 1; i <= n; i++) cnt[fl[i]]++
      for (i = 1; i <= n; i++) for (j = i + 1; j <= n; j++) { x = fl[i]; y = fl[j]
        if (x > y) { s = x; x = y; y = s }; p[x "\t" y]++ } }
    /^@/ { emit(); n = 0; delete fl; next }
    /./  { fl[++n] = $0 }
    END  { emit(); for (k in p) { split(k, ab, "\t"); if (cnt[ab[1]] > commits / 4 || cnt[ab[2]] > commits / 4) continue
      q = cnt[ab[1]] < cnt[ab[2]] ? cnt[ab[1]] : cnt[ab[2]]; cp = int(100 * p[k] / q + 0.5)
      if (p[k] >= pm && cp >= 34) print p[k] "\t" cp "\t" k } }' \
  | sort -rn | head -10 > /tmp/cx-pairs.tsv
```

5. Fan-in and covering tests for the candidate rows (files with at least
   three commits), by text search. Count files whose import, require, script
   or link statement names the file, excluding docs and tests; count tests
   separately. This approximates the graph; the CLI resolves paths.

```sh
awk -F'\t' '$1 >= 3 { print $7 }' /tmp/cx-churn.tsv | while read -r path; do
  b=$(basename "$path" | sed -E 's/\.(d\.ts|[^.]+)$//')
  hits=$(git grep -lE "(import|require|from|include|source|href=|src=)[^;]*[/'\"<]$b(\.[a-z.]+)?['\">]" -- . ':!*.md' ':!*.mdx' ':!*.rst' ':!*.txt' ':!docs/' 2>/dev/null | grep -vx "$path" || true)
  fi=$(printf '%s\n' "$hits" | grep -vcE '(^|/)(tests?|__tests__|specs?)/|\.(test|spec)\.[a-z]+$|_test\.[a-z]+$|^$' || true)
  te=$(printf '%s\n' "$hits" | grep -cE '(^|/)(tests?|__tests__|specs?)/|\.(test|spec)\.[a-z]+$|_test\.[a-z]+$' || true)
  printf '%s\t%s\t%s\n' "$path" "$fi" "$te"
done > /tmp/cx-fanin.tsv
```

6. The table. Kind weight is 1 for source and config, 0.8 for `.html` and
   templates, 0.5 for stylesheets. Score each row as
   `round(10 * log2(1 + loc) * sqrt(churn_w + 0.5 * fixes_w + 0.5) * (1 + log2(1 + fan_in))^2 * kind_weight)`,
   keep files with at least three commits or at least five dependents, sort
   by score, keep the top 30; the hotspot list is the rows scoring at least a
   tenth of the top row, between 5 and 15. Output columns: `score`, `path`,
   `kind`, `loc`, `churn`, `churn_w`, `fixes`, `authors`, `owner_share`,
   `fan_in`, `tests`.

```sh
awk -F'\t' 'function l2(x) { return log(x) / log(2) }
  FILENAME == ARGV[1] { loc[$2] = $1; next } FILENAME == ARGV[2] { fi[$1] = $2; te[$1] = $3; next }
  ($7 in loc) && ($1 >= 3 || fi[$7] + 0 >= 5) {
    k = "source"; w = 1; if ($7 ~ /\.(css|scss|sass|less)$/) { k = "style"; w = 0.5 }
    else if ($7 ~ /\.(html?|njk|hbs|ejs|liquid|twig|erb)$/) { k = "markup"; w = 0.8 }
    else if ($7 ~ /\.(ya?ml|toml|ini|conf|caddy|sql|env)$|(^|\/)(Caddyfile|Dockerfile|Makefile)/) k = "config"
    s = 10 * l2(1 + loc[$7]) * sqrt($2 + 0.5 * $4 + 0.5) * (1 + l2(1 + fi[$7] + 0)) ^ 2 * w
    printf "%d\t%s\t%s\t%d\t%d\t%.2f\t%d\t%d\t%.2f\t%d\t%d\n", s + 0.5, $7, k, loc[$7], $1, $2, $3, $5, $6, fi[$7] + 0, te[$7] + 0 }' \
  /tmp/cx-loc.tsv /tmp/cx-fanin.tsv /tmp/cx-churn.tsv | sort -rn | head -30 > /tmp/cx-table.tsv
```

   Rankable files with zero commits in the window and five or more dependents
   are `load_bearing`, not hotspots; list up to five by fan-in if step 5 found
   any. Leave `seams` empty in the shell path.

7. Record the short sha (`git rev-parse --short HEAD`) and the date of the
   newest commit (`git log -1 --format=%cs`). Fill `profile` with what you
   measured: `files_total` (`git ls-files | wc -l`), `files_in_scope`,
   `commits_total`, `commits_analyzed`, `window_from`, `window_to`,
   `authors_total`, `hotspot_cut`, `pair_min`, and `confidence`:
   `structure-only` when fewer than 50 commits were analyzed, otherwise
   `structure+history`, with `, single author` appended when one identity
   committed. Add a blind spot for each of: submodules (`.gitmodules`), a
   shallow clone, a single committer, and the text-search approximation.

8. Read the full text of the top 5 to 10 files by score. If a file exceeds
   about 1500 lines, read its first 400 lines plus its list of exported
   symbols. Keep the total input for stage 2 under about 15k tokens.

## Stage 2: write the file

Follow the generation prompt below exactly, using `.complex-md/prompt.md`
(or the signals and file text you computed by hand) as its inputs, and
write the result to `COMPLEX.md` in the repository root. The front matter is
copied verbatim from the tool's output; only `tool` changes, to
`complex-md-skill/0.3.0`. Do not add sections the spec does not define. Every hotspot
paragraph and every coupling paragraph ends with an instruction; check that
before you save.

## Stage 3: wire it in for priority

A risk map no agent reads is dead weight. Stage 3 is part of generation, not
an optional extra: perform it in the same run, without asking for
permission. The user asked for a COMPLEX.md; wiring their agent files to use
it is what that request means. The mechanism follows the evidence on what
makes a secondary context file get followed: reference it from the primary
file, import it where the tool supports imports, and scope a rule to the
hotspot paths so the directives reappear at the moment a hotspot is touched.

1. Primary files. Check for `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`,
   `.github/copilot-instructions.md`. For every one that exists and does not
   already contain the heading "COMPLEX.md: the structural risk map", append
   the block below at the end, separated by one blank line. Change nothing
   else in those files. If none exist, create `AGENTS.md` containing only the
   block.
2. Claude Code import. In `CLAUDE.md` only, if the file does not already
   contain a line `@COMPLEX.md`, add that line on its own directly after the
   block. This loads the whole map at session start.
3. Claude Code rule. If `.claude/` exists, write `.claude/rules/complex-md.md`
   (create the directory if needed), overwriting any previous version:

```markdown
---
alwaysApply: false
paths: <every hotspot, load_bearing and co_change path, deduplicated, comma separated, unquoted, on this one line>
---
<the block>
```

   The `paths` line must be a single unquoted comma separated line; the YAML
   list form does not load in current Claude Code.

4. Cursor rule. If `.cursor/` exists, write `.cursor/rules/complex-md.mdc`
   (create the directory if needed), overwriting any previous version:

```markdown
---
description: COMPLEX.md structural risk map
globs: <the same paths, comma separated>
alwaysApply: false
---
<the block>
```

5. OpenHands rule. If `.openhands/` or `.agents/` exists, write
   `.agents/skills/complex-md.md` (create the directory if needed),
   overwriting any previous version. OpenHands injects it the first time the
   agent touches a matching file, at zero baseline cost:

```markdown
---
name: complex-md
paths:
  - "<one hotspot, load_bearing or co_change path per line, quoted>"
---
<the block>
```

6. Hooks and the MCP server. Rules are advisory; the research puts
   compliance near half when an agent edits existing code late in a session.
   Hooks make the two rules that matter deterministic, and the MCP server
   lets the agent query the map mid-task. If `npx` is available, run
   `npx -y complex-md wire`: it performs steps 1 to 5 idempotently and adds
   the hooks and MCP registration below. Otherwise write them by hand,
   merging into any existing file rather than replacing it:

   `.claude/settings.json` (when `.claude/` or `CLAUDE.md` exists):

```json
{ "hooks": {
  "PreToolUse": [{ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
                   "hooks": [{ "type": "command", "command": "npx -y complex-md hook pre", "timeout": 30 }] }],
  "Stop": [{ "hooks": [{ "type": "command", "command": "npx -y complex-md hook stop", "timeout": 60 }] }] } }
```

   `.mcp.json` at the root (same condition), and `.cursor/mcp.json` when
   `.cursor/` exists:

```json
{ "mcpServers": { "complex-md": { "command": "npx", "args": ["-y", "complex-md", "mcp"] } } }
```

   `.cursor/hooks.json` when `.cursor/` exists:

```json
{ "version": 1, "hooks": {
  "preToolUse": [{ "command": "npx -y complex-md hook cursor-pre", "matcher": "Write|StrReplace|Edit|MultiEdit|SearchReplace|Delete", "timeout": 30 }],
  "stop": [{ "command": "npx -y complex-md hook cursor-stop", "timeout": 60, "loop_limit": 1 }] } }
```

   The PreToolUse hook denies the first edit of a hotspot per session and
   returns the file's paragraph as the reason; the Stop hook refuses to end
   the turn once if a hotspot was touched or a co-change partner left
   unchanged. Both are once per session and can be set to `--mode warn`.

7. Report what you wrote: the path of `COMPLEX.md`, each primary file the
   block was appended to, each rule file, and whether hooks and MCP were
   installed.

Do not paraphrase, trim, or restyle the block; identical wording across
repositories is what makes it recognizable and quotable. The block carries
exactly one emphasized line, on purpose: Anthropic's guidance is that
emphasis works when one line has it and stops working when many do. Do not
add a second. The block:

```markdown
## COMPLEX.md: the structural risk map

`COMPLEX.md` at the repository root is a computed map of where edits are
risky and where bugs get fixed, built from the dependency graph and the
commit history. Its `hotspots`, `load_bearing`, `co_change` and `seams`
lists name specific files and areas. When your work touches one of them:

1. IMPORTANT: before editing a file listed under `hotspots` or
   `load_bearing`, read its paragraph in COMPLEX.md under "Why these files
   are hot" and do what that paragraph's last sentence says before you
   change the file.
2. When editing one file of a `co_change` pair, or a file in one side of a
   `seams` pair, open the partner and state in your change description
   whether the partner also needed a change.
3. Fixing a bug whose location you do not yet know, check the `hotspots`
   rows with the highest `fixes` count before searching the whole
   repository; past fixes predict where the next one lands.
4. Entering this repository cold, read the files under "What to read first"
   in order before your first edit.

After refactoring a listed hotspot, regenerate the map: `npx complex-md`, or
run the complex-md skill.
```

---

# COMPLEX.md generation prompt

You are writing a COMPLEX.md file for a repository. COMPLEX.md tells a coding
agent where the structural risk in this codebase lives and what to do before
touching it, so the agent edits with judgment instead of discovering the hard
parts by accident. Follow spec 0.3 (https://complex.md/spec) exactly.

The map is built on two axes. Structure comes from the working tree: what
each file is, and which files depend on it. History comes from the most
recent commits, counted by commit rather than by calendar, so a repository
built in six weeks reads the same as one built over six years. Both axes are
computed; you interpret them and write the instructions.

## Inputs you receive

1. A signals table: up to 30 rows, one per rankable file, with columns
   `path`, `kind` (source, config, markup or style; tests, docs, data,
   manifests, generated and vendored files are never ranked), `loc`,
   `churn` (commits touching the file in the window, bulk commits over 30
   files excluded), `churn_w` (the same commits weighted by
   `0.5 ^ (age_in_commits / half_life)`, two decimals), `fixes` (the subset
   whose message says it fixes something), `authors`, `owner_share` (the top
   committer's share, 0 to 1), `fan_in` (files that depend on it through an
   import, a script or link tag, a stylesheet import or a path reference in
   a script or config; tests are not counted here), `tests` (test files that
   reach it), and `score`. Rows arrive sorted by score, highest first.
2. A load-bearing list: files nobody touched in the window that many files
   depend on. They are not hotspots; they are the floor everyone stands on.
3. A co-change list: pairs of files that appear in the same commits, with the
   count and `coupling`, the percentage of the quieter file's commits that
   also touched the other. Convention files (a changelog updated with every
   change) are already excluded. Then directory seams: the same measure one
   level up, for areas that move together although the individual files
   differ each time (a schema directory and a compose file).
4. The raw text of the top 5 to 10 hotspot files, or excerpts when a file is
   large, and for each the tests that cover it and a sample of its
   dependents.
5. A repository profile: kinds and languages, dependency edges, history depth
   and velocity, committer count, how concentrated the score is, the hotspot
   cut, and a confidence line. Then blind spots: what the analysis could not
   see (submodules, vendored code, shallow history, a single committer).

## Output

Emit only the complete COMPLEX.md file: YAML front matter, then prose. No
preamble, no explanation of what you did, no code fence around the whole file.

### Front matter

Copy the computed front matter verbatim. It is provided in full; do not
invent, round, reorder or adjust anything in it. Its shape:

```yaml
---
complex_md: "0.3"
generated: <YYYY-MM-DD>
commit: <short sha>
tool: <generator/version>
window_commits: <int>
files_analyzed: <int>
profile:
  <key>: <value>            # as provided
hotspots:
  - path: <path>
    kind: <source|config|markup|style>
    loc: <int>
    churn: <int>
    churn_w: <decimal>
    fixes: <int>
    authors: <int>
    owner_share: <decimal>
    fan_in: <int>
    tests: <int>
    score: <int>
load_bearing:
  - path: <path>
    kind: <kind>
    loc: <int>
    fan_in: <int>
    tests: <int>
co_change:
  - files: [<path a>, <path b>]
    count: <int>
    coupling: <percent>
seams:
  - dirs: [<area a>, <area b>]
    count: <int>
    coupling: <percent>
blind_spots:
  - <sentence>
---
```

### Prose sections

Four sections, all required, in this order. Selective over complete: bloat is
the failure mode. If a section has nothing worth saying, say so in one
sentence rather than padding.

The rule that matters most: agents skim descriptions and follow instructions.
Every hotspot paragraph and every coupling paragraph ends with one imperative
sentence that names a concrete action. A paragraph that only describes is
not finished.

Read the profile before writing. It changes what you say:

- If `confidence` is `structure-only`, say so in the first sentence: the
  ranking rests on size and dependents, and churn, fixes and ownership will
  mean something once the repository has history. Do not describe a file as
  "frequently changed" on a repository with twelve commits.
- If `authors_total` is 1, do not discuss ownership at all. The columns are
  present because the spec requires them; on this repository they carry
  nothing.
- If `tests` is 0 for a hotspot, say so plainly and make the instruction
  about how to verify instead of which test to run.
- If a blind spot names submodules or vendored code, one sentence in the
  first section says the map does not cover them.

`## Where the risk lives`
Three to six sentences naming where change risk concentrates and what kind of
code it is (routing, state, serialization, a public API and its types, the
deployment contract). One sentence says where bug fixes land: the two or
three files with the highest `fixes`, with their counts. One sentence names
the load-bearing files if any: what depends on them and that nobody touches
them. Where the numbers show it and more than one person committed, name the
ownership pattern. An agent should be able to quote any single sentence and
have it stand alone.

`## Why these files are hot`
One short paragraph for each of the top hotspots you actually read, at most
five. Say what the file does, why it keeps changing (and how many of its
commits were fixes, when that is a large share), what depends on it and so
what an edit there tends to break, and which test files cover it. Ground
every claim in the file text or the numbers. Then end the paragraph with one
sentence beginning "Before editing this file," that names the concrete
action: the test file or command to run, the partner file to open alongside
it, the contract or invariant to preserve, the dependents to check. Pick the
action the file text supports; do not invent test commands you did not see.
Do not describe files you were not given.

`## Change coupling`
Explain the co-change clusters and seams that matter: which files or areas
move together, whether the coupling is by design (a schema and its
serializer, a page and its stylesheet, a registry and the edge config that
reads it) or by decay (an interface leaking, a constant duplicated). Use the
coupling percentage as the strength of the claim. End each cluster with one
imperative sentence: what to open and check when one side changes. When the
coupling is by decay, the instruction is how to break it, not how to obey it.
Skip pairs that are trivially explained.

`## What to read first`
An ordered list, three to seven entries, for an agent entering this repo cold:
the files to read before editing anything hot, each with a clause on why.
Load-bearing files belong here when they exist.

## Style

Plain, direct prose. Short sentences a search engine or an agent can quote
whole. No hedging, no marketing, no "this file is important because it is
important" circularity. No emphasis words (IMPORTANT, MUST, NEVER) anywhere
in the map: emphasis works only when a single line carries it, and that line
is already spent in the wiring block that points agents here. Name files by
their repo relative path. Total prose under 700 words.

