Editor File Generation

August 14, 2026 · View on GitHub

repowise can generate and maintain AI-editor configuration files — CLAUDE.md, cursor.md, and similar — from the already-indexed codebase data. No LLM calls are made. All content is derived from the repowise SQL database and filesystem manifests.


Table of Contents

  1. Why this exists
  2. How it works
  3. The two-section file structure
  4. What goes into the Repowise section
  5. Data sources
  6. File reference
  7. CLI usage
  8. REST API
  9. Configuration
  10. Adding a new editor file

1. Why this exists

Claude Code reads CLAUDE.md on every session start but does not proactively call MCP tools — even when an MCP server is configured. Users have to explicitly tell Claude Code to "use MCP" every time.

By embedding codebase intelligence and MCP workflow guidance directly into CLAUDE.md, Claude Code treats it as project context and naturally reaches for Repowise tools without being prompted.

The same principle applies to Cursor's .cursor/rules / cursor.md, GitHub Copilot's .github/copilot-instructions.md, and any other file an AI editor auto-loads at session start.


2. How it works

repowise init (or update)


All wiki pages and git metadata already persisted in DB


EditorFileDataFetcher
  Queries DB for: architecture summary, top modules by PageRank,
  entry points, hotspot files, active decision records
  Scans filesystem for: tech stack, build commands
  Returns: EditorFileData (frozen dataclass, no DB types)


ClaudeMdGenerator.write(repo_path, data)
  Renders claude_md.j2 template with EditorFileData
  Reads existing CLAUDE.md (if any)
  Merges: user content preserved, Repowise section replaced
  Writes atomically (temp file + rename)


CLAUDE.md written to repo root

Key properties:

  • Runs after _persist() completes, so all data is current
  • Best-effort in repowise update — never fails the command
  • Instant — no LLM calls, typically < 200ms
  • Idempotent — re-running produces identical output if data hasn't changed
  • Deterministic — lists sorted by stable keys, with path asc as the final tiebreaker. Most lists lead on PageRank desc; entry points deliberately do not, and rank on execution-start evidence instead.

3. The two-section file structure

# CLAUDE.md

<!-- Add your custom instructions below.
     Repowise will never modify anything outside the REPOWISE markers. -->
<!-- Examples: coding style rules, test commands, workflow preferences -->

[user's own content — untouched by repowise]

<!-- REPOWISE:START — Do not edit below this line. Auto-generated by Repowise. -->
## Codebase Intelligence — myrepo (Repowise)

[auto-generated content]
<!-- REPOWISE:END -->

Merge rules (applied in BaseEditorFileGenerator.write()):

SituationAction
No CLAUDE.md existsCreate with user placeholder + Repowise section
File exists, no markersAppend Repowise section at bottom; leave existing content untouched
File exists with markersReplace only the content between REPOWISE:START and REPOWISE:END

The regex used for marker replacement:

pattern = re.escape(MARKER_START) + r".*?" + re.escape(MARKER_END)
re.sub(pattern, new_wrapped_content, existing, flags=re.DOTALL)

This is the entire preservation strategy. The only way to lose user content is to manually insert text inside the REPOWISE:START / REPOWISE:END block, which the placeholder comment warns against.


4. What goes into the Repowise section

The section has three parts. Total target length: 150–250 lines. Conciseness is intentional — research shows AI assistants ignore bloated configuration files.

Part 1: Codebase Intelligence

Auto-generated from indexed data. Updates on every repowise update.

Sub-sectionSourceCap
Architecture summaryFirst 4 sentences from repo_overview wiki page4 sentences
Key Modulesmodule_page pages sorted by PageRank desc, joined with git_metadata for ownerTop 10
Entry PointsThe curated kg_project_meta list; otherwise graph_nodes where is_entry_point=True, ranked on execution-start evidenceTop 10
Tech StackFilesystem scan (package.json, pyproject.toml, Cargo.toml, go.mod, etc.)All detected
Hotspotsgit_metadata where is_hotspot=True, sorted by churn_percentile descTop 5

Part 2: MCP Tools Workflow Guide

Static — the same text for every repo. Hardcoded in claude_md.j2. Teaches Claude Code when to call each MCP tool using natural workflow framing rather than imperatives.

This is the most important part. The phrasing matters: "Starting a new task? Call get_overview() first" is more effective than "ALWAYS call get_overview() before doing anything."

Part 3: Codebase Conventions

Auto-generated from indexed data.

Sub-sectionSourceCap
Architectural Decisionsdecision_records where status='active', sorted by staleness_score ascTop 8
CommandsFilesystem scan: package.json scripts, Makefile targets, pyproject.toml pytest/ruffAll detected

5. Data sources

Database queries (in EditorFileDataFetcher)

Architecture summary (_get_architecture_summary)

crud.list_pages(session, repo_id, page_type="repo_overview", limit=1)
# → extracts first 4 sentences, strips markdown headers/code fences

Key modules (_get_key_modules)

SELECT page, GraphNode.pagerank, GraphNode.symbol_count
FROM wiki_pages
JOIN graph_nodes ON graph_nodes.node_id = wiki_pages.target_path
WHERE wiki_pages.page_type = 'module_page'
  AND wiki_pages.repository_id = :repo_id
ORDER BY graph_nodes.pagerank DESC NULLS LAST
LIMIT 10
# → owner resolved via separate git_metadata lookup

Entry points (_get_entry_points)

The curated kg_project_meta.entry_points_json list wins when the curation pass has run. Otherwise the raw flag is read unbounded and ranked in Python, because the ordering cannot be expressed as an ORDER BY:

SELECT node_id, pagerank, betweenness FROM graph_nodes
WHERE repository_id = :repo_id AND is_entry_point = TRUE
# then rank_entry_points(...): conventional entry name, then shallower path,
# then centrality as a tiebreak; sliced to 10 after ranking.

PageRank deliberately does not lead here. Centrality rewards fan-in, so it floats a widely-imported barrel above the real front door — see generation/entry_points.py.

Hotspots (_get_hotspots)

SELECT file_path, churn_percentile, commit_count_90d, primary_owner_name
FROM git_metadata
WHERE repository_id = :repo_id AND is_hotspot = TRUE
ORDER BY churn_percentile DESC, file_path ASC   -- deterministic tie-break
LIMIT 5

Active decisions (_get_decisions)

SELECT * FROM decision_records
WHERE repository_id = :repo_id AND status = 'active'
ORDER BY staleness_score ASC
LIMIT 8
# → uses first 100 chars of rationale field

Average confidence (_get_avg_confidence)

SELECT AVG(confidence) FROM wiki_pages WHERE repository_id = :repo_id

Filesystem scan (in tech_stack.py)

detect_tech_stack(repo_path) scans the repo root for:

FileDetects
package.jsonNode.js, TypeScript, React, Next.js, Vue, Express, Prisma, Tailwind, …
pyproject.toml / setup.pyPython, FastAPI, Django, Flask, SQLAlchemy, Celery, …
Cargo.tomlRust
go.modGo (extracts version from go X.Y directive)
pom.xml / build.gradleJava / Kotlin + Maven / Gradle
GemfileRuby
composer.jsonPHP
DockerfileDocker
docker-compose.ymlDocker Compose

detect_build_commands(repo_path) returns a dict with keys from: build, test, lint, dev, format, typecheck.

Priority: package.json scripts → pyproject.tomlMakefile. Each key is only set once — the first source wins.


6. File reference

packages/core/src/repowise/core/generation/editor_files/
├── __init__.py          Exports: ClaudeMdGenerator, EditorFileData, EditorFileDataFetcher
├── base.py              BaseEditorFileGenerator — marker-merge logic, Jinja2 setup, atomic write
├── data.py              Frozen dataclasses: EditorFileData, TechStackItem, KeyModule,
│                        HotspotFile, DecisionSummary
├── fetcher.py           EditorFileDataFetcher — all DB queries + filesystem calls
├── tech_stack.py        detect_tech_stack(), detect_build_commands()
└── claude_md.py         ClaudeMdGenerator — filename, marker_tag, template_name, user_placeholder

packages/core/src/repowise/core/generation/templates/
└── claude_md.j2         Jinja2 template for the Repowise-managed section

packages/cli/src/repowise/cli/commands/
├── claude_md_cmd.py     `repowise generate-claude-md` command
└── init_cmd.py          _maybe_generate_claude_md(), _write_claude_md_async() helpers

packages/server/src/repowise/server/routers/
└── claude_md.py         GET/POST /api/repos/{repo_id}/claude-md

tests/unit/generation/
├── test_editor_file_base.py     Marker-merge logic, idempotency, file structure
├── test_editor_file_fetcher.py  DB query correctness with in-memory SQLite
└── test_tech_stack.py           Filesystem detection with tmp_path fixtures

Class hierarchy

BaseEditorFileGenerator   (base.py)
│   filename: str         — abstract property
│   marker_tag: str       — abstract property
│   template_name: str    — abstract property
│   user_placeholder: str — abstract property
│   render(data) → str
│   write(repo_path, data) → Path
│   render_full(repo_path, data) → str

└── ClaudeMdGenerator     (claude_md.py)
        filename = "CLAUDE.md"
        marker_tag = "REPOWISE"
        template_name = "claude_md.j2"
        user_placeholder = "# CLAUDE.md\n\n<!-- ... -->\n"

Data flow

EditorFileDataFetcher.fetch()
    │  AsyncSession + repo_id + repo_path

    ├── crud.get_repository()          → repo.name
    ├── _get_architecture_summary()    → str (2-4 sentences)
    ├── _get_key_modules()             → list[KeyModule]
    ├── _get_entry_points()            → list[str]
    ├── detect_tech_stack()            → list[TechStackItem]
    ├── _get_hotspots()                → list[HotspotFile]
    ├── _get_decisions()               → list[DecisionSummary]
    ├── detect_build_commands()        → dict[str, str]
    └── _get_avg_confidence()          → float


EditorFileData (frozen dataclass)


BaseEditorFileGenerator.render(data)
    │  Jinja2 template rendered with data

str (managed section content, without markers)


BaseEditorFileGenerator.write(repo_path, data)
    │  Wraps with markers, merges with existing file

Path (written file)

7. CLI usage

repowise generate-claude-md [PATH]
  PATH          Repo root to generate for (default: current directory)
  --output FILE Write to a custom path instead of CLAUDE.md in repo root
  --stdout      Print generated content to stdout (does not write a file)

Examples:

# Generate CLAUDE.md for the current directory
repowise generate-claude-md .

# Preview what would be written without touching the file
repowise generate-claude-md . --stdout

# Write to a custom path
repowise generate-claude-md /path/to/repo --output /tmp/preview.md

Auto-generation during init and update:

repowise init generates CLAUDE.md after the persistence phase completes. repowise update regenerates it (best-effort) after each incremental sync.

Both can be disabled:

# Skip CLAUDE.md on this init run and persist the preference to config
repowise init --no-claude-md .

Project-local files vs. global registration:

init writes in two different places, and the flags split along that line.

Project-local, all inside the repo, versionable, one set per repo: .repowise/mcp.json, .mcp.json, .claude/CLAUDE.md, AGENTS.md, .vscode/mcp.json, .vscode/extensions.json, .codex/. Three have an opt-out flag (--no-claude-md, --agents, --codex) and the VS Code pair is prompt-gated in an interactive run. Only .repowise/mcp.json and the root .mcp.json are written unconditionally.

Machine-wide, outside the repo, one shared copy for every repo you index, all written by register_editor_clients() in editor_setup.py:

  • the repowise MCP entry in ~/.claude/settings.json and in Claude Desktop's config
  • the Claude Code PostToolUse and SessionStart hooks
  • env.ENABLE_TOOL_SEARCH in ~/.claude/settings.json (skipped for repos on the lean MCP tool profile, and never overwritten if you already set it)

Only the Claude integration implements register_client; Codex and VS Code read project-local config, so theirs are no-ops.

The distill command-rewrite hook is machine-wide too, but it is offered separately (offer_distill_rewrite_hook) because it is strictly opt-in.

--no-editor-setup turns off both groups: the machine-wide registrations above, including the rewrite hook offer, and the project-local files (.mcp.json, .claude/CLAUDE.md, .vscode/mcp.json, .vscode/extensions.json). Only .repowise/ is written.

# Index the repo, write nothing into it and nothing outside it
repowise init --no-editor-setup --yes .

It used to cover the machine-wide half only, and said so — which meant there was no combination of flags that indexed a repo without writing four files into the working tree, since VS Code had no opt-out flag of its own. Issue #1499 is the report of exactly that. One switch, one meaning.

.repowise/mcp.json is the one deliberate exception and is written either way. No editor reads it unless pointed at it, and it is what repowise mcp . prints — so skipping it would mean opting out of editor setup also opted out of ever opting back in.

Reach for it whenever the checkout or the binary running init is temporary: a scratch clone, a release smoke test from a throwaway venv, a git worktree, a benchmark loop over many repos. Each config holds a single repowise MCP key, so a second init replaces the entry rather than adding one beside it, and the breakage only shows up later, when the path it now points at is gone and the MCP server quietly stops loading. init prints a notice when it is about to repoint an existing entry, but the flag is how you avoid it.

Two exceptions, so the flags do not cancel each other out. --no-editor-setup --no-distill-hook still records the distill.commands.enabled: false opt-out in this repo's config.yaml. That record is repo-local, and it is the only thing that gates an already-installed global rewrite hook off here.

The same reasoning covers the instruction files: --no-editor-setup --no-claude-md still records editor_files.claude_md: false, and likewise for --no-agents-md. Those flags mean "never generate this file", not "skip it this once", and the generator declining on its way past used to be the only thing that wrote the preference down — so suppressing the writes would also have suppressed the memory of the refusal, and the next repowise update would have generated the file anyway. A preference is not a write.

REPOWISE_SKIP_EDITOR_SETUP=1 is the same switch as an env var, which is the better fit for CI and sandboxes where no one is passing flags by hand. Either source disables setup; the flag never re-enables what the env var turned off. Neither is persisted to config.yaml: this is a per-run decision about your machine, not a property of the repo, so a later init without the flag registers normally.


8. REST API

GET /api/repos/{repo_id}/claude-md

Returns the generated Repowise section as JSON. Does not write to disk. Useful for web UI preview.

{
  "content": "## Codebase Intelligence — myrepo (Repowise)\n...",
  "generated_at": "2026-03-28",
  "repo_name": "myrepo",
  "sections": ["Architecture", "Key Modules", "Entry Points", "Tech Stack",
               "Hotspots (High Churn)", "Repowise MCP Tools", "Codebase Conventions"]
}

POST /api/repos/{repo_id}/claude-md/generate

Regenerates CLAUDE.md and writes it to the repository's local_path on disk. Returns 422 if local_path is not accessible from the server.

{
  "status": "generated",
  "path": "/home/user/myrepo/CLAUDE.md",
  "generated_at": "2026-03-28"
}

9. Configuration

# .repowise/config.yaml
editor_files:
  claude_md: true     # default: true. Set false to disable entirely.

The --no-claude-md CLI flag sets editor_files.claude_md: false in config.yaml and all future repowise update runs will skip it.


10. Adding a new editor file

Example: adding cursor.md support.

Step 1 — Create the subclass

packages/core/src/repowise/core/generation/editor_files/cursor_md.py

from .base import BaseEditorFileGenerator

class CursorMdGenerator(BaseEditorFileGenerator):
    filename = "cursor.md"
    marker_tag = "REPOWISE"
    template_name = "cursor_md.j2"
    user_placeholder = (
        "# cursor.md\n\n"
        "<!-- Add your Cursor rules below. "
        "Repowise will never modify anything outside the REPOWISE markers. -->\n"
    )

That's the entire subclass. All merge logic, atomic write, Jinja2 setup, and render() / write() / render_full() are inherited from BaseEditorFileGenerator.

Step 2 — Create the template

packages/core/src/repowise/core/generation/templates/cursor_md.j2

The template receives data (an EditorFileData instance). All fields are the same as claude_md.j2 — the fetcher is shared. Write cursor-specific framing around the same data.

Minimal starting point:

## Project Context (Repowise)
Last indexed: {{ data.indexed_at }}.

{% if data.architecture_summary %}
### Architecture
{{ data.architecture_summary }}
{% endif %}

### Key Files
{% for ep in data.entry_points %}
- `{{ ep }}`
{% endfor %}

Step 3 — Add config key

# .repowise/config.yaml
editor_files:
  claude_md: true
  cursor_md: true    # NEW

Step 4 — Export from the subpackage

packages/core/src/repowise/core/generation/editor_files/__init__.py

from .cursor_md import CursorMdGenerator  # add this line

Step 5 — Hook into init and update

In _maybe_generate_claude_md() (or extract a more generic _maybe_generate_editor_files() helper):

# After existing CLAUDE.md generation
if cfg.get("editor_files", {}).get("cursor_md", False):  # default off
    from repowise.core.generation.editor_files import CursorMdGenerator
    CursorMdGenerator().write(repo_path, data)

Since data is already fetched by this point (reuse from CLAUDE.md generation), the cursor.md write costs only the template render — no extra DB queries.

Step 6 — Add a REST endpoint (optional)

Follow packages/server/src/repowise/server/routers/claude_md.py exactly. Swap ClaudeMdGenerator for CursorMdGenerator. Register the router in app.py.

Step 7 — Add tests

Follow tests/unit/generation/test_editor_file_base.py. The _TestGenerator fixture already tests BaseEditorFileGenerator behavior — add a test that instantiates CursorMdGenerator directly to verify the filename/marker_tag/ template_name properties and that the template renders without error.


What you do NOT need to do

  • Write any DB queries — EditorFileDataFetcher and EditorFileData are shared.
  • Write any file I/O or merge logic — BaseEditorFileGenerator handles everything.
  • Register a new CLI command — use generate-claude-md as a reference if you want a standalone command, but it is not required.
  • Update the ORM schema — no new tables needed.

The only required artifacts for a new editor file are:

  1. A 10–30 line subclass (*.py)
  2. A Jinja2 template (*.j2)