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
- Why this exists
- How it works
- The two-section file structure
- What goes into the Repowise section
- Data sources
- File reference
- CLI usage
- REST API
- Configuration
- 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()):
| Situation | Action |
|---|---|
| No CLAUDE.md exists | Create with user placeholder + Repowise section |
| File exists, no markers | Append Repowise section at bottom; leave existing content untouched |
| File exists with markers | Replace 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-section | Source | Cap |
|---|---|---|
| Architecture summary | First 4 sentences from repo_overview wiki page | 4 sentences |
| Key Modules | module_page pages sorted by PageRank desc, joined with git_metadata for owner | Top 10 |
| Entry Points | The curated kg_project_meta list; otherwise graph_nodes where is_entry_point=True, ranked on execution-start evidence | Top 10 |
| Tech Stack | Filesystem scan (package.json, pyproject.toml, Cargo.toml, go.mod, etc.) | All detected |
| Hotspots | git_metadata where is_hotspot=True, sorted by churn_percentile desc | Top 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-section | Source | Cap |
|---|---|---|
| Architectural Decisions | decision_records where status='active', sorted by staleness_score asc | Top 8 |
| Commands | Filesystem scan: package.json scripts, Makefile targets, pyproject.toml pytest/ruff | All 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:
| File | Detects |
|---|---|
package.json | Node.js, TypeScript, React, Next.js, Vue, Express, Prisma, Tailwind, … |
pyproject.toml / setup.py | Python, FastAPI, Django, Flask, SQLAlchemy, Celery, … |
Cargo.toml | Rust |
go.mod | Go (extracts version from go X.Y directive) |
pom.xml / build.gradle | Java / Kotlin + Maven / Gradle |
Gemfile | Ruby |
composer.json | PHP |
Dockerfile | Docker |
docker-compose.yml | Docker Compose |
detect_build_commands(repo_path) returns a dict with keys from:
build, test, lint, dev, format, typecheck.
Priority: package.json scripts → pyproject.toml → Makefile. 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
repowiseMCP entry in~/.claude/settings.jsonand in Claude Desktop's config - the Claude Code PostToolUse and SessionStart hooks
env.ENABLE_TOOL_SEARCHin~/.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 —
EditorFileDataFetcherandEditorFileDataare shared. - Write any file I/O or merge logic —
BaseEditorFileGeneratorhandles everything. - Register a new CLI command — use
generate-claude-mdas 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:
- A 10–30 line subclass (
*.py) - A Jinja2 template (
*.j2)