# Agent Context

> Generate evidence-driven context files (AGENTS.md, CLAUDE.md, GEMINI.md, docs/agents/, .claude/settings.json, cross-vendor configs) from Understand-Anything knowledge graphs. Use when the user runs /agent-context, asks to "bootstrap agent context", "generate AGENTS.md", "initialize Claude Code context", or "make this repo AI-agent-ready". Requires $PROJECT_ROOT/.understand-anything/knowledge-graph.json — produced by running /understand on the target repo first.

- Skill: `jonaskahn/agent-context` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add jonaskahn/agent-context`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jonaskahn/agent-context/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: jonaskahn (https://skillmd.com/u/jonaskahn)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jonaskahn/agent-context

---


# /agent-context

Self-contained execution runbook. Read top-to-bottom. Do not backtrack.

Every output file reads like a human wrote it from real evidence. No `[to fill]` residue except in
`docs/agents/testing.md`
Mock stance. If the graph does not support a section, omit the section entirely — do not scaffold.

Reference files (read on-demand, not required for normal execution):

- `references/SCHEMAS.md` — full graph schemas, 20-rule lint spec, sample data
- `references/TEMPLATES.md` — verbatim output file templates numbered 1–28 matching write order

---

## Variables

All variables set during execution. Phases that set each variable are noted.

| Variable                 | Type           | Set in  | Description                                                                                               |
|--------------------------|----------------|---------|-----------------------------------------------------------------------------------------------------------|
| `PROJECT_ROOT`           | string         | Phase 0 | Absolute path to target repo                                                                              |
| `FORCE`                  | bool           | Phase 0 | Overwrite existing files                                                                                  |
| `DRY_RUN`                | bool           | Phase 0 | Print content, write nothing                                                                              |
| `WITH_CI`                | bool           | Phase 0 | Generate CI workflow and hook                                                                             |
| `MINIMAL`                | bool           | Phase 0 | Emit only AGENTS.md + CLAUDE.md + .claude/settings.json; skip docs/agents/ and cross-vendor files        |
| `IS_GIT_REPO`            | bool           | Phase 0 | Whether PROJECT_ROOT is a git repo                                                                        |
| `KNOWLEDGE_GRAPH`        | object         | Phase 1 | Parsed knowledge-graph.json                                                                               |
| `DOMAIN_GRAPH`           | object or null | Phase 1 | Parsed domain-graph.json or null                                                                          |
| `GRAPH_STALE`            | bool           | Phase 1 | HEAD != graph commit hash                                                                                 |
| `DOMAIN_QUALITY`         | enum           | Phase 1 | "high", "mixed", "low", or "missing"                                                                      |
| `EXISTING_CONVENTIONS`   | string or null | Phase 1 | Content of existing CONVENTIONS.md                                                                        |
| `nodesById`              | Map            | Phase 2 | node.id → Node                                                                                            |
| `functionsByFile`        | Map            | Phase 2 | filePath → function Node[]                                                                                |
| `importsOut`             | Map            | Phase 2 | node.id → target id[]                                                                                     |
| `importsIn`              | Map            | Phase 2 | node.id → source id[]                                                                                     |
| `containsOut`            | Map            | Phase 2 | file node.id → function node.id[]                                                                         |
| `layersByNodeId`         | Map            | Phase 2 | node.id → layer name                                                                                      |
| `nodesByLayer`           | Map            | Phase 2 | layer name → node.id[]                                                                                    |
| `COMMANDS`               | dict           | Phase 3 | keys: install, dev, test, lint, build (any subset)                                                        |
| `FRAMEWORK`              | string         | Phase 3 | Primary framework name (e.g. "Nuxt.js", "FastAPI") or "unknown" if manifest found but no match            |
| `LANG`                   | string         | Phase 3 | Primary language (e.g. "TypeScript/JavaScript", "Python", "Rust", "Go") or empty if no manifest found     |
| `PROJECT_SUMMARY`        | string         | Phase 3 | One-line project summary from knowledge-graph project.description or first tour step (120 chars max)      |
| `MONOREPO_TOOL`          | string or null | Phase 3 | Detected monorepo manager: "pnpm-workspaces", "npm-workspaces", "yarn-workspaces", "lerna", "nx", "turborepo", "cargo-workspace", or null |
| `MONOREPO_WORKSPACES`    | string[]       | Phase 3 | Package paths from workspace manifest; empty if not a monorepo                                            |
| `CHANGELOG_SNIPPET`      | string or null | Phase 3 | Last 3–5 entries extracted from CHANGELOG.md; null if file absent                                         |
| `NON_OBVIOUS`            | string[]       | Phase 4 | Up to 5 phrased bullet strings from topology signals (4a–4e)                                              |
| `CONTENT_SIGNALS`        | string[]       | Phase 4 | Up to 3 phrased bullet strings from content signal (4g); may be empty                                     |
| `CONVENTIONS_DIRECTIVES` | map or null    | Phase 4 | Extracted directives from CONVENTIONS.md, grouped by category (safety, naming, patterns, workflow, other) |
| `CONVENTIONS_ACTION`     | enum or null   | Phase 1 | "stub", "skip", or "legacy" when EXISTING_CONVENTIONS is null; null otherwise                             |
| `total_flow_count`       | int            | Phase 2 | Count of `type=="flow"` nodes in DOMAIN_GRAPH; 0 if DOMAIN_GRAPH is null                                  |
| `flowsByDomain`          | Map            | Phase 2 | domain.id → flow Node[] via `contains_flow` edges                                                         |
| `cross_domain_edges`     | tuple[]        | Phase 2 | Ordered (source.name, target.name) pairs from `cross_domain` edges                                        |
| `LAYER_GLOBS`            | Map            | Phase 5 | layer.name → glob pattern derived from common path prefix of layer's file nodes                           |

---

## Phase 0 — Argument parsing

1. Tokenise `$ARGUMENTS` on whitespace.
2. Extract flags: `--force` → `FORCE=true`, `--dry-run` → `DRY_RUN=true`, `--with-ci` → `WITH_CI=true`,
   `--minimal` → `MINIMAL=true`. Defaults: all false.
3. First non-flag token → target path. Resolve relative to CWD. If absent → `PROJECT_ROOT = CWD` (current working directory becomes `$PROJECT_ROOT`).
4. Verify `PROJECT_ROOT` is a directory. If not → print `agent-context: <path> is not a directory.` → stop.
5. Run `git -C $PROJECT_ROOT rev-parse --git-dir`. Success → `IS_GIT_REPO=true`. Failure → `IS_GIT_REPO=false` (warn,
   continue).
6. If `MINIMAL=true` and `WITH_CI=true`: print
   `agent-context: --minimal and --with-ci are mutually exclusive. --with-ci ignored.` Set `WITH_CI=false`.

---

## Phase 1 — Prerequisite gates

Run gates A → B → C → D in order. Gate A is hard (stop on failure). Gates B, C, D are soft (warn, continue).

### Gate A — Knowledge graph (HARD)

Check `$PROJECT_ROOT/.understand-anything/knowledge-graph.json`.

**Missing** — print verbatim and stop:

```
agent-context: knowledge graph not found.

This plugin needs $PROJECT_ROOT/.understand-anything/knowledge-graph.json in the target
repo to generate useful context files. If you have not set up
Understand-Anything yet, run:

  /plugin marketplace add Lum1104/understand-anything
  /plugin install understand-anything

Then, in the repo you want to generate context for, run:

  /understand

That will produce $PROJECT_ROOT/.understand-anything/knowledge-graph.json. Re-run
/agent-context once it is present.
```

**Present but not valid JSON** — print verbatim and stop:

```
agent-context: knowledge graph is not valid JSON.

$PROJECT_ROOT/.understand-anything/knowledge-graph.json exists but cannot be parsed.
This usually means /understand was interrupted. Re-run:

  /understand

and try again.
```

**Missing required top-level keys** (`version`, `project`, `nodes`, `edges`, `layers`) — print verbatim and stop:

```
agent-context: knowledge graph schema does not match what this plugin
expects. Required top-level keys: version, project, nodes, edges, layers.
Missing: <list>.

Update Understand-Anything (/plugin update understand-anything) or file
an issue at github.com/jonaskahn/agent-context describing the schema
mismatch.
```

**Valid** — set `KNOWLEDGE_GRAPH` to parsed object.

### Gate B — Freshness (SOFT)

Skip if `IS_GIT_REPO=false`. Compare `git rev-parse HEAD` vs `project.gitCommitHash`.

**Mismatch** — set `GRAPH_STALE=true`. Print:

```
agent-context: knowledge graph was generated against commit <graph_hash>
but the repo is at <head_hash>. Generated files may be out of date.
Re-run /understand for the best results.
```

**Match or no git** — set `GRAPH_STALE=false`.

Also check `project.analyzedAt`: if older than 14 days from now, print a staleness warning. Continue either way.

### Gate C — Domain graph (SOFT)

Check `$PROJECT_ROOT/.understand-anything/domain-graph.json`.

**Missing** — set `DOMAIN_QUALITY="missing"`, `DOMAIN_GRAPH=null`. Print:

```
agent-context: domain graph not found. Glossary will be stubbed.
Run /understand-domain to populate docs/agents/glossary.md with
domain-level content.
```

**Present** — parse it. Set `DOMAIN_GRAPH` to parsed object. Compute quality grade:

```
HEURISTIC_COUNT = count of domain-type nodes whose summary contains "Heuristic"
TOTAL_COUNT     = count of domain-type nodes (type == "domain")

if TOTAL_COUNT == 0:             DOMAIN_QUALITY = "missing"
elif HEURISTIC_COUNT / TOTAL_COUNT >= 0.5: DOMAIN_QUALITY = "low"
elif HEURISTIC_COUNT / TOTAL_COUNT >= 0.1: DOMAIN_QUALITY = "mixed"
else:                            DOMAIN_QUALITY = "high"
```

### Gate D — Existing CONVENTIONS.md (SOFT)

Search for `CONVENTIONS.md` (case-insensitive) in expected locations and report findings.

#### Step D.1: Grep search in standard locations

Run `grep -ri "^" $PROJECT_ROOT/CONVENTIONS.md $PROJECT_ROOT/docs/CONVENTIONS.md 2>/dev/null` to find files.

Check in this order:
1. `$PROJECT_ROOT/CONVENTIONS.md` (case-insensitive)
2. `$PROJECT_ROOT/docs/CONVENTIONS.md` (case-insensitive)

#### Step D.2: File found in expected location

**Found in root or docs/** — read content. Set `EXISTING_CONVENTIONS` to content string. Set `CONVENTIONS_ACTION=null`. Print:

```
agent-context: found existing CONVENTIONS.md (<N> lines).
Existing conventions will be transformed into AI-targeted directives in docs/agents/conventions.md.
Location: <root or docs/>
```

Existing conventions are parsed and distilled into `docs/agents/conventions.md` (terse AI directives, not verbatim
copy) and linked from AGENTS.md §6 (Deeper Context).
They are NOT inlined into AGENTS.md — the 100-line cap forbids it.

#### Step D.3: Fallback grep search

If not found in standard locations, run:
```bash
find $PROJECT_ROOT -iname "CONVENTIONS.md" -not -path "*/node_modules/*" -not -path "*/.git/*"
```

**Found elsewhere** — print diagnostic:

```
⚠️  CONVENTIONS.md found at unexpected location

Searched the repository — CONVENTIONS.md was found at:
  - <path>

Expected location: $PROJECT_ROOT or docs/ folder

If this is your conventions file, please either:
1. Move it to $PROJECT_ROOT (recommended)
2. Move it to the docs/ folder
3. Provide the exact location in your project context

agent-context will skip loading conventions and use default stub.
```

Set `EXISTING_CONVENTIONS=null`. Continue to Step D.4.

**Not found anywhere** — set `EXISTING_CONVENTIONS=null`. Print diagnostic:

```
⚠️  CONVENTIONS.md not found

Searched the repository — no CONVENTIONS.md file exists in:
  - $PROJECT_ROOT: ❌
  - docs/ folder: ❌
  - Anywhere else in the project: ❌

According to your README and SKILL.md documentation, the agent-context plugin expects a CONVENTIONS.md file in $PROJECT_ROOT (case-insensitive lookup). This file should contain team coding standards and directives.

If you have a conventions file:
  • Move it to $PROJECT_ROOT, or
  • Move it to docs/, or
  • Specify its location below

If not, agent-context will create a starter stub.
```

Continue to Step D.4.

#### Step D.4: Handle missing CONVENTIONS.md

Only reached if `EXISTING_CONVENTIONS=null`. Determine `CONVENTIONS_ACTION`:

- If `DRY_RUN=true` OR AskUserQuestion is unavailable (headless / script invocation): set `CONVENTIONS_ACTION="stub"`.
  Print: `agent-context: no CONVENTIONS.md — defaulting to starter stub.`
- Otherwise call AskUserQuestion with question
  `"No CONVENTIONS.md found. How should /agent-context handle CONVENTIONS.md?"` and four options:
    - (a) *(default, recommended)* `Create a starter stub with empty Safety / Naming / Patterns / Workflow sections.`
      → `CONVENTIONS_ACTION="stub"`
    - (b) `Skip — do not create CONVENTIONS.md.` → `CONVENTIONS_ACTION="skip"`
    - (c) `Legacy — write a verbatim copy of AGENTS.md.` → `CONVENTIONS_ACTION="legacy"`
    - (d) `Provide custom path — I have a CONVENTIONS file elsewhere.` → prompt user for path with text input, then attempt to read that file and treat as found.

Do NOT mutate `EXISTING_CONVENTIONS`. It stays `null` for this run. The stub written by option (a) is not parsed back as
conventions on the same run; on the next run, Gate D detects it via the Found branch.

---

## Phase 2 — Graph indexing

### Schema quick-ref

Node key fields: `id` (opaque, `file:` or `func:` prefix), `type` ("file"|"function"), `filePath` (always present),
`lineRange` ([start,end], function nodes only), `summary` (generic if starts with `"Source file "` or ends with
`"— function in this module."`), `complexity` ("simple"|"moderate"|"complex").

Edge types: `contains` (file→function), `imports` (file→file, one-directional).

Layer: `{ id, name, description, nodeIds[] }`. Pre-computed — do not recompute.

Tour: `{ order, title, description, nodeIds[] }`. Dependency-ordered onboarding steps.

For full field-level documentation, see `references/SCHEMAS.md` §1–§6.

### Build indexes

One pass each:

1. `nodes[]` → `nodesById` (Map: node.id → Node).
2. `edges[]`:
    - `type == "contains"` → push target into `containsOut[source]`
    - `type == "imports"` → push target into `importsOut[source]`, push source into `importsIn[target]`
3. `layers[]` → `nodesByLayer` (Map: layer.name → node.id[]), and for each nodeId:
   `layersByNodeId[nodeId] = layer.name`.
4. Filter `nodesById` values where `type == "function"` → group by `filePath` into `functionsByFile`.
5. If `DOMAIN_GRAPH` is non-null:
    - `total_flow_count` = count of `DOMAIN_GRAPH.nodes` where `type == "flow"`. If `DOMAIN_GRAPH` is null, set to 0.
    - `flowsByDomain` = Map of `domain.id` → flow Node[], built by walking `DOMAIN_GRAPH.edges` of type
      `contains_flow`.
    - `cross_domain_edges` = ordered list of `(source.name, target.name)` tuples from edges of type `cross_domain`,
      using `nodesById` lookups against `DOMAIN_GRAPH.nodes`. Skip if either endpoint is missing.

### Integrity assertions (warn, do not stop)

- All edge source/target ids exist in `nodesById`. Print warning with orphan count if any.
- All `layer.nodeIds` entries exist in `nodesById`. Same.
- All `tour[].nodeIds` entries exist in `nodesById`. Same.
- No node in more than one layer. If conflict, use first occurrence, warn.

### Edge case: empty arrays

- `nodes[]` empty → all indexes are empty maps. Phase 4 produces zero signals. Phase 5 emits AGENTS.md with header +
  §3 (if commands found) + §5 (Safety) + §6 (Deeper Context pointing to stubs). Omit §1, §2, §4.
- `layers[]` empty → `layersByNodeId` and `nodesByLayer` are empty. AGENTS.md §2 tagline becomes:
  `**No layers detected. Run /understand with more source files.**`
- `tour[]` empty → AGENTS.md §1 has tagline and test sentence but zero bullets. Tagline becomes:
  `**Layers are the architecture. Read the module map below.**`

---

## Phase 3 — Shallow command discovery

The graph does not contain build commands. Read manifests directly from PROJECT_ROOT:

### package.json (check first)

If `package.json` exists, parse it:

- `scripts.dev` → `COMMANDS.dev`
- `scripts.test` → `COMMANDS.test`
- `scripts.lint` → `COMMANDS.lint`
- `scripts.build` → `COMMANDS.build`

Infer install command from lockfile. Check in this order, use first found:

1. `bun.lockb` → `bun install`
2. `pnpm-lock.yaml` → `pnpm install`
3. `yarn.lock` → `yarn install`
4. `package-lock.json` → `npm install`
5. None found → `npm install` (default)

Set `COMMANDS.install` to the inferred command.

### pyproject.toml (if no package.json)

- Install: `pip install -e .`
- Test: `pytest`

### Cargo.toml (if no package.json or pyproject.toml)

- Build: `cargo build`
- Test: `cargo test`
- Lint: `cargo check`

### go.mod (if none of the above)

- Build: `go build ./...`
- Test: `go test ./...`
- Lint: `go vet ./...`

### No manifest found

`COMMANDS` is empty. Omit AGENTS.md §3 entirely.

### Framework and language detection

Run this step using whichever manifest was found above (or "none" if none found). Set `FRAMEWORK`, `LANG`, and
`PROJECT_SUMMARY`.

**`LANG`** — infer from which manifest was found:

- `package.json` → `"TypeScript/JavaScript"`
- `pyproject.toml` → `"Python"`
- `Cargo.toml` → `"Rust"`
- `go.mod` → `"Go"`
- None found → `""` (empty — omit Stack line from architecture.md)

**`FRAMEWORK`** — scan the manifest found above for these keys (check in order listed, use first match):

`package.json` — inspect `dependencies` and `devDependencies` keys:

| Key present               | FRAMEWORK   |
|---------------------------|-------------|
| `nuxt` or any `@nuxtjs/*` | `"Nuxt.js"` |
| `next`                    | `"Next.js"` |
| `@nestjs/core`            | `"NestJS"`  |
| `astro`                   | `"Astro"`   |
| `svelte`                  | `"Svelte"`  |
| `solid-js`                | `"SolidJS"` |
| `hono`                    | `"Hono"`    |
| `fastify`                 | `"Fastify"` |
| `express`                 | `"Express"` |
| `react`                   | `"React"`   |
| `vue`                     | `"Vue.js"`  |
| None of the above         | `"unknown"` |

`pyproject.toml` — inspect `[tool.poetry.dependencies]` or `[project.dependencies]`:

| Key present       | FRAMEWORK    |
|-------------------|--------------|
| `fastapi`         | `"FastAPI"`  |
| `django`          | `"Django"`   |
| `litestar`        | `"Litestar"` |
| `flask`           | `"Flask"`    |
| None of the above | `"unknown"`  |

`Cargo.toml` — inspect `[dependencies]`:

| Key present       | FRAMEWORK     |
|-------------------|---------------|
| `actix-web`       | `"Actix Web"` |
| `axum`            | `"Axum"`      |
| `rocket`          | `"Rocket"`    |
| None of the above | `"unknown"`   |

`go.mod` — inspect `require` block:

| Module present             | FRAMEWORK   |
|----------------------------|-------------|
| `github.com/gin-gonic/gin` | `"Gin"`     |
| `github.com/labstack/echo` | `"Echo"`    |
| `github.com/gofiber/fiber` | `"Fiber"`   |
| None of the above          | `"unknown"` |

No manifest → `FRAMEWORK = ""` (empty).

**`PROJECT_SUMMARY`** — derive from KNOWLEDGE_GRAPH:

1. If `project.description` is non-empty → use it (truncate to 120 chars at word boundary).
2. Else if `tour` is non-empty → use `tour[0].description` (truncate to 120 chars at word boundary).
3. Else → `""` (empty).

### Monorepo detection

Run after manifest discovery. Check these signals in order:

| Signal | Condition | `MONOREPO_TOOL` |
|--------|-----------|-----------------|
| `nx.json` exists | — | `"nx"` |
| `turbo.json` exists | — | `"turborepo"` |
| `lerna.json` exists | — | `"lerna"` |
| `package.json` found | `workspaces` key is an array | `"pnpm-workspaces"` (if pnpm lockfile) / `"yarn-workspaces"` (if yarn lockfile) / `"npm-workspaces"` |
| `Cargo.toml` found | `[workspace]` section present | `"cargo-workspace"` |
| None of the above | — | `null` |

If `MONOREPO_TOOL` is not null: set `MONOREPO_WORKSPACES` to the list of workspace package paths from the relevant
manifest's `workspaces` / `members` field. If the field uses glob patterns rather than explicit paths, keep the
patterns as-is (e.g., `packages/*`). If the manifest has no such field, set to `[]`.

If `MONOREPO_TOOL` is null: set `MONOREPO_WORKSPACES = []`.

When `MONOREPO_TOOL` is not null, append a note to `COMMANDS.install` (if present):
`<install_command>   # monorepo root — see {MONOREPO_TOOL} workspaces`

### CHANGELOG extraction

Check for `CHANGELOG.md`, `CHANGELOG`, `CHANGES.md`, or `HISTORY.md` at `PROJECT_ROOT` (case-insensitive). Use the
first match found.

**Found** — extract the last 3–5 changelog entries. An "entry" is any `## ` heading block (version or date heading)
plus the bullets/lines immediately following it until the next `## ` heading. Extract text only — strip any markdown
formatting except backticks. Truncate each entry to 300 chars. Set `CHANGELOG_SNIPPET` to the joined entries as a
plain-text string.

**Not found** — set `CHANGELOG_SNIPPET = null`.

---

## Phase 4 — Convention mining

Apply 5 signals. Each produces zero or more candidate bullets. Rank all candidates by rarity. Take top 3–5. Phrase every
bullet negation-forward (lead with "Don't", "No", "Never", or "If X, stop").

### Signal 4a — Cross-layer import anomalies

For each `imports` edge, look up source and target layers via `layersByNodeId`. For each (source_layer, target_layer)
pair, count edges. Pairs with count == 1 or count == 2 are anomalies.

Phrase each anomaly as:
`- {source_layer} rarely imports {target_layer} directly — the one exception is \`{source.filePath}\` importing
\`{target.filePath}\`. Don't remove it.`

Rarity per candidate: `1.0 / count` (where count is the pair's edge count).

### Signal 4b — Naming deviators within a layer

For each layer with >=5 file nodes:

1. Tokenise each file's basename on `.`, `-`, and `_`.
2. Find the modal (most frequent) last-token (e.g. `.vue`, `.ts`).
3. If >80% of files share the modal token, the remaining files are deviators.

Phrase as:
`- Most {layer} files end in \`{token}\`. Don't rename the exceptions — {list of up to 3 deviators} — they are
intentional.`

Rarity per candidate: `1.0 / deviator_count`.

### Signal 4c — Layer/path disagreements

For each file node, predict its layer from `filePath` top-level directory prefix:

- `app/pages/` or `pages/` → "Pages"
- `app/components/` or `components/` → "Components"
- `app/composables/` or `composables/` → "Composables"
- `server/` → "Server"
- `app/utils/` or `utils/` or `lib/` → "App utils" or "Utility"
- `src/` → infer from next directory segment

Compare predicted layer to actual layer from `layersByNodeId`. Collect disagreements.

If total disagreements > 3: skip this signal entirely (bulk disagreements indicate graph-level misclassification, not
conventions).

If <= 3 disagreements, phrase each as:
`- \`{filePath}\` lives under {predicted} by path but is classified as {actual}. Don't reorganise it.`

Rarity per candidate: `1.0 / total_disagreement_count`.

### Signal 4d — Dependency direction violations

Select precedence array based on `project.frameworks`:

- If frameworks contain "Nuxt" or "Vue" (case-insensitive):
  `["Pages", "Components", "Composables", "App utils", "Server"]`
- Otherwise: `["API", "Service", "Data", "Utility"]`

For each `imports` edge, get source and target layer positions in the precedence array. If target position < source
position (target is "higher" than source), it is an upward violation.

Skip edges where either layer is not in the precedence array.

Phrase as:
`- {source_layer} normally does not import from {target_layer}, but \`{source.filePath}\` does. If removing it, verify
it is not an event callback.`

Rarity per candidate: `1.0 / violation_count_for_this_layer_pair`.

### Signal 4e — Rarity ranking and selection

1. Collect all candidates from 4a–4d.
2. Sort by rarity descending (highest rarity = rarest = most worth surfacing).
3. Tie-breaker when rarity scores are equal: prefer 4a > 4c > 4d > 4b.
4. Take top 5. If fewer than 3 candidates total, emit only what exists — do not pad.
5. Set `NON_OBVIOUS` to the selected bullet strings.

### Signal 4f — Convention directive extraction (only if `EXISTING_CONVENTIONS` not null)

Parse `EXISTING_CONVENTIONS` into AI-targeted directives and set `CONVENTIONS_DIRECTIVES`.

**Step 1 — Identify imperative lines.** For each line or bullet in `EXISTING_CONVENTIONS`, check if it contains any
of the following keywords (case-insensitive): `must`, `must not`, `never`, `always`, `don't`, `do not`, `avoid`,
`use`, `prefer`, `require`. Lines matching at least one keyword are rule candidates. Lines that are headings
(`#`-prefixed), blank, or pure prose narrative (no imperative verb) are skipped.

**Step 2 — Strip rationale.** For each candidate, truncate at the first occurrence of ` — `, ` because `, ` (this`,
or `. ` that follows the core directive clause. Keep only the imperative clause.

**Step 3 — Normalize prefix.**

- If the line contains a negation (`must not`, `never`, `don't`, `do not`, `avoid`, `no `): normalize prefix to
  `MUST NOT`.
- If the line expresses obligation (`must`, `always`, `require`, `use`, `prefer`): normalize prefix to `MUST`.
- Otherwise: keep the original imperative verb.

**Step 4 — Categorize** each directive into exactly one bucket:

- `safety` — keywords: secret, credential, env, migration, destructive, delete, drop, test disab, commit
- `naming` — keywords: name, naming, case, suffix, prefix, file name, variable, module name, casing
- `patterns` — keywords: import, abstract, class, struct, coupling, layer, circular, depend
- `workflow` — keywords: phase, PR, pull request, review, gate, branch, deploy, merge, approval
- `other` — anything not matching the above

**Step 5 — Deduplicate.** For each directive, check if it semantically overlaps with any string in `NON_OBVIOUS`
(same file path mentioned, or >70% word overlap). If so, skip it — `NON_OBVIOUS` already surfaces it.

**Step 6 — Set result.** If at least one directive was extracted: set `CONVENTIONS_DIRECTIVES` to a map of
`{safety: [...], naming: [...], patterns: [...], workflow: [...], other: [...]}` (omit empty categories).
If zero directives were extracted: set `CONVENTIONS_DIRECTIVES = null`.

### Signal 4g — Content-based pattern extraction (from function summaries)

Complements topology signals (4a–4e) with semantic patterns inferred from non-generic function summaries.

**Step 1 — Collect substantive function summaries.** From `nodesById`, filter to function nodes where `summary` is
non-generic (does not end with `"— function in this module."`). Require at least 10 such nodes to proceed; if fewer,
set `CONTENT_SIGNALS = []` and skip remaining steps.

**Step 2 — Identify recurring return-shape patterns.** Scan summaries for these phrases (case-insensitive). For each
that appears in ≥3 distinct function summaries, record a candidate:

| Phrase pattern | Candidate bullet |
|---|---|
| `returns {data, error}` or `returns (data, error)` | `- Functions return \`{data, error}\` pairs — don't throw, return the error field.` |
| `throws` or `raise` | `- Errors are thrown, not returned as values — don't swallow exceptions.` |
| `async` / `await` / `Promise` | `- Most functions are async — don't mix sync I/O in async call chains.` |
| decorator pattern: `@` prefix in summary | `- Decorators are the extension point — don't subclass where a decorator fits.` |
| `middleware` | `- Middleware is the primary composition pattern — chain it, don't fork it.` |

**Step 3 — Identify naming conventions from function names.** Among the substantive function nodes, tokenise
`node.name` by camelCase / snake_case / kebab-case boundaries. Find the modal prefix (appears in ≥4 function names
within any single layer). If that prefix is not already surfaced in `NON_OBVIOUS`, add:
`- Functions in {layer} start with \`{prefix}\` — match it when adding new ones.`

**Step 4 — Deduplicate against `NON_OBVIOUS`.** Skip any candidate that overlaps >70% with an existing
`NON_OBVIOUS` bullet (word-overlap check).

**Step 5 — Set result.** Take up to 3 candidates from Steps 2–3 (Step 2 first). Set `CONTENT_SIGNALS` to the
selected bullet strings. If zero candidates, set `CONTENT_SIGNALS = []`.

`CONTENT_SIGNALS` are appended to `NON_OBVIOUS` **after** the top-5 topology picks, capped so the total
`NON_OBVIOUS + CONTENT_SIGNALS` list does not exceed 7 bullets going into Phase 5.

---

## Phase 5 — File generation

### Write-or-skip decision table

| Condition                                                        | Action                                                                                                                                                                                                                                                                                    |
|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| File does not exist                                              | Create                                                                                                                                                                                                                                                                                    |
| File exists + `FORCE=false`                                      | Skip. Add to "skipped" list for Phase 7.                                                                                                                                                                                                                                                  |
| File exists + `FORCE=true`                                       | Overwrite                                                                                                                                                                                                                                                                                 |
| `AGENTS.md` exists + contains `## Absolute rules` + `FORCE=true` | Overwrite all sections **except** `## Absolute rules` — read the existing file, extract the section from its heading to the next `##` heading, and splice it into the freshly-generated content in place of the template's §5. This protects user customisations across forced refreshes. |
| `.claude/settings.json` (any state)                              | Always merge (union deny arrays, preserve existing hooks). Never overwrite.                                                                                                                                                                                                               |
| `.gitignore` (any state, `DRY_RUN=false` only)                   | Append `CLAUDE.local.md` if line not already present. Under `DRY_RUN=true`, print the would-be append but do not write.                                                                                                                                                                  |
| `.aider.conf.yml` exists                                         | Merge: append to `read` list if not present.                                                                                                                                                                                                                                              |
| `DRY_RUN=true`                                                   | For all files including merge targets: compute final content (reading existing files for merges), print between `===== FILE: <path> =====` separators, write nothing.                                                                                                                     |
| `MINIMAL=true`                                                   | Skip items #1–7 (docs/agents/), #13–#19 (cross-vendor), #20–#28 (new vendor/rules). Write only #8–#12 + #20 (.claude/rules/) and AGENTS.md/CLAUDE.md/.claude/settings.json.                                                                                                              |

### Write order (process sequentially, 1–28)

Items #1–7 are skipped when `MINIMAL=true`. Items #13–19 and #22–28 are skipped when `MINIMAL=true`.

| #  | File                                                  | Condition                                                             | Template                                                                |
|----|-------------------------------------------------------|-----------------------------------------------------------------------|-------------------------------------------------------------------------|
| 1  | `docs/agents/tech-debt.md`                            | skip if `MINIMAL`                                                     | TEMPLATES.md §11                                                        |
| 2  | `docs/agents/testing.md`                              | skip if `MINIMAL`                                                     | TEMPLATES.md §10                                                        |
| 3  | `docs/agents/glossary.md`                             | skip if `MINIMAL`                                                     | TEMPLATES.md §7 (if quality "high"/"mixed") or §8 (if "low"/"missing")  |
| 4  | `docs/agents/conventions.md`                          | only if `EXISTING_CONVENTIONS` not null; skip if `MINIMAL`           | TEMPLATES.md §9                                                         |
| 5  | `docs/agents/patterns.md`                             | skip if `MINIMAL`                                                     | TEMPLATES.md §6                                                         |
| 6  | `docs/agents/architecture.md`                         | skip if `MINIMAL`                                                     | TEMPLATES.md §5                                                         |
| 7  | `docs/agents/flow.md` or `docs/agents/flows/`         | skip if `MINIMAL` (mode below)                                        | TEMPLATES.md §19 (single) or §20+§21 (folder)                           |
| 8  | `.claude/settings.json`                               | always (merge)                                                        | TEMPLATES.md §4                                                         |
| 9  | `.gitignore`                                          | always (append, no-op under `DRY_RUN`)                                | —                                                                       |
| 10 | `CLAUDE.local.md`                                     | always                                                                | TEMPLATES.md §3                                                         |
| 11 | `CLAUDE.md`                                           | always                                                                | TEMPLATES.md §2                                                         |
| 12 | `AGENTS.md`                                           | always                                                                | TEMPLATES.md §1                                                         |
| 13 | `.cursor/rules/agents.mdc`                            | skip if `MINIMAL`                                                     | TEMPLATES.md §12                                                        |
| 14 | `.cursor/rules/<layer>.mdc` (one per layer)           | skip if `MINIMAL`; skip if `LAYER_GLOBS` is empty                    | TEMPLATES.md §26                                                        |
| 15 | `.github/copilot-instructions.md`                     | skip if `MINIMAL`                                                     | TEMPLATES.md §13                                                        |
| 16 | `.github/instructions/<layer>.instructions.md`        | skip if `MINIMAL`; one per layer in `LAYER_GLOBS`                    | TEMPLATES.md §27                                                        |
| 17 | `.codex/instructions.md`                              | skip if `MINIMAL`                                                     | TEMPLATES.md §14                                                        |
| 18 | `GEMINI.md`                                           | skip if `MINIMAL`                                                     | TEMPLATES.md §24                                                        |
| 19 | `.windsurf/rules/agents.md`                           | skip if `MINIMAL`                                                     | TEMPLATES.md §25                                                        |
| 20 | `.claude/rules/<layer>.md` (one per layer)            | always (even with `MINIMAL`); skip if no layers                       | TEMPLATES.md §23                                                        |
| 21 | `CONVENTIONS.md`                                      | only if `EXISTING_CONVENTIONS` is null AND `CONVENTIONS_ACTION != "skip"`; skip if `MINIMAL` | TEMPLATES.md §22 if `CONVENTIONS_ACTION=="stub"`; §15 if "legacy" |
| 22 | `.aider.conf.yml`                                     | skip if `MINIMAL` (merge if exists)                                   | TEMPLATES.md §16                                                        |
| 23 | `.github/workflows/agent-context-freshness.yml`       | only if `WITH_CI=true`                                                | TEMPLATES.md §17                                                        |
| 24 | `hooks/check-freshness.sh`                            | only if `WITH_CI=true`                                                | TEMPLATES.md §18 (chmod +x)                                             |
| 25 | `.agent-context/manifest.json`                        | always                                                                | TEMPLATES.md §28                                                        |

### Flow output mode (Step 7)

Decide between single-file and folder mode using the indexes from Phase 2:

- `total_flow_count <= 8` (or `DOMAIN_GRAPH` is null): write a single `docs/agents/flow.md` from TEMPLATES.md §19. If
  `DOMAIN_GRAPH` is null or `total_flow_count == 0`, the template emits the "no flows extracted" stub branch.
- `total_flow_count > 8`: write a folder `docs/agents/flows/` containing:
    - `docs/agents/flows/index.md` (TEMPLATES.md §20)
    - `docs/agents/flows/<domain.slug>.md` for each domain in `domains_with_flows` (TEMPLATES.md §21)

Domain slug derivation (used by §20 and §21):

1. Lowercase `domain.name`.
2. Strip diacritics. Replace any run of `[^a-z0-9]+` with `-`. Trim leading/trailing `-`.
3. Reserved word: never emit `index`. If a domain slugs to `index`, suffix with `-domain`.
4. Collisions across domains: append `-2`, `-3`, … in domain iteration order. Record the final slug on the domain
   object so cross-references (§21 "See also") stay consistent.

### Format migration (automatic, not gated by `--force`)

Before writing flow output, detect and clean up leftovers from a prior run that used the other mode:

- Folder mode active and `docs/agents/flow.md` exists → recognise it as the old single-file version, delete it, then
  write the folder. Report `↻ docs/agents/flow.md → docs/agents/flows/  (migra

…(truncated)
