Skip to content

Commit 3355078

Browse files
committed
feat: add FUNDING.yml, CLAUDE.md, and claudemd-generator example
- FUNDING.yml enables GitHub Sponsor button linking to Gumroad products - CLAUDE.md demonstrates context engineering pattern (meta-example) - claudemd-generator.py: viral utility to auto-generate CLAUDE.md for any project
1 parent 8abd1a0 commit 3355078

3 files changed

Lines changed: 379 additions & 0 deletions

File tree

.github/FUNDING.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Funding links for self-evolving-agent-patterns
2+
# These are displayed as a "Sponsor" button on the GitHub repository page
3+
4+
# Gumroad products
5+
custom:
6+
- https://tutuoai.gumroad.com/l/agent-blueprint
7+
- https://tutuoai.gumroad.com/l/context-engineering-playbook
8+
- https://tutuoai.com

CLAUDE.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# CLAUDE.md — self-evolving-agent-patterns
2+
3+
> This file IS the pattern. It's a working example of Context Engineering — the #1 most impactful pattern in this repo. Claude Code reads this file automatically to understand your project.
4+
5+
## What This Project Is
6+
7+
Battle-tested patterns for building production AI agents that modify their own code, learn from outcomes, and compound capability over time. Every pattern was extracted from JARVIS — a real system running 24/7 on Claude API.
8+
9+
## Project Structure
10+
11+
```
12+
self-evolving-agent-patterns/
13+
├── examples/ # Runnable code — start here
14+
│ ├── minimal_agent.py # Complete self-evolving agent in 150 lines
15+
│ ├── self_evolving_loop.py # Evolve → verify → commit loop
16+
│ ├── claudemd-generator.py # Auto-generate CLAUDE.md for any Python project
17+
│ ├── context-manifest.yaml # Working config for context assembly
18+
│ └── tool-discovery.py # Tool auto-registration pattern
19+
├── docs/ # Deep-dive pattern documentation
20+
│ ├── context-engineering.md # 🔥 #1 pattern — shape behavior through input
21+
│ ├── immune-system.md # Self-healing failure resistance
22+
│ ├── anti-patterns.md # 20+ real failures with root causes
23+
│ ├── two-paradigm.md # When to use code vs LLM vs context
24+
│ ├── agent-memory-learning.md # Reward-driven learning systems
25+
│ └── build-ai-agent-tools.md # Tool system design
26+
├── src/agent_patterns/ # Pip-installable library
27+
│ ├── agent.py # Core Agent class with tool loop
28+
│ └── tools.py # File + shell tool implementations
29+
├── dist/ # Built packages
30+
└── pyproject.toml # Package config (pip install .)
31+
```
32+
33+
## Key Commands
34+
35+
```bash
36+
# Run the minimal agent example
37+
cd examples && python minimal_agent.py
38+
39+
# Run with a custom task
40+
AGENT_TASK="Build a calculator" python examples/minimal_agent.py
41+
42+
# Generate CLAUDE.md for any project
43+
python examples/claudemd-generator.py /path/to/project
44+
45+
# Install as a library
46+
pip install .
47+
48+
# Run tests (if contributing)
49+
pytest
50+
```
51+
52+
## Architecture Principles
53+
54+
1. **Context Engineering > Prompt Engineering** — Control what the LLM sees (input), not what it says (output). Never parse LLM output with regex.
55+
2. **Two-Paradigm Discipline** — Use code for mechanical decisions, LLM for semantic judgment. Never mix them.
56+
3. **Immune System Pattern** — Every failure becomes a permanent behavioral guardrail. The system gets stronger from what breaks it.
57+
4. **Skills as Knowledge** — Reusable patterns stored as readable files, loaded on-demand. Knowledge compounds.
58+
59+
## Code Style
60+
61+
- Python 3.11+, type hints on public APIs
62+
- 4-space indentation, Google-style docstrings
63+
- Functions under 50 lines, files under 300 lines
64+
- No external ML dependencies — pure Python + anthropic SDK
65+
66+
## Contributing
67+
68+
See CONTRIBUTING.md. Key rule: every pattern must come from real operational experience, not theory.

examples/claudemd-generator.py

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
#!/usr/bin/env python3
2+
"""Auto-generate optimized CLAUDE.md for any Python project. No external deps.
3+
4+
Based on context engineering patterns from
5+
https://github.com/AFunLS/self-evolving-agent-patterns
6+
7+
Usage: python claudemd-generator.py /path/to/project [-o output.md] [--stdout]
8+
"""
9+
import argparse, os, re, subprocess, sys
10+
from collections import Counter
11+
from pathlib import Path
12+
13+
SKIP = {".git", "__pycache__", ".tox", ".mypy_cache", ".pytest_cache", "node_modules",
14+
".eggs", "dist", "build", ".venv", "venv", "env", ".env", ".nox", ".ruff_cache"}
15+
16+
17+
def find_py_files(root: Path, limit: int = 500) -> list[Path]:
18+
"""Walk project tree, return .py files, skipping irrelevant dirs."""
19+
results = []
20+
for dirpath, dirnames, filenames in os.walk(root):
21+
dirnames[:] = [d for d in dirnames if d not in SKIP and not d.endswith(".egg-info")]
22+
for f in filenames:
23+
if f.endswith(".py"):
24+
results.append(Path(dirpath) / f)
25+
if len(results) >= limit:
26+
return results
27+
return results
28+
29+
30+
def build_tree(root: Path, max_depth: int = 3) -> str:
31+
"""Build an ASCII directory tree."""
32+
lines = [f"{root.name}/"]
33+
def _walk(d: Path, prefix: str, depth: int):
34+
if depth > max_depth:
35+
return
36+
try:
37+
entries = sorted(d.iterdir(), key=lambda e: (not e.is_dir(), e.name))
38+
except PermissionError:
39+
return
40+
entries = [e for e in entries if e.name not in SKIP and not e.name.endswith(".egg-info")]
41+
for i, entry in enumerate(entries):
42+
last = i == len(entries) - 1
43+
lines.append(f"{prefix}{'└── ' if last else '├── '}{entry.name}")
44+
if entry.is_dir():
45+
_walk(entry, prefix + (" " if last else "│ "), depth + 1)
46+
_walk(root, "", 1)
47+
return "\n".join(lines)
48+
49+
50+
def detect_structure(root: Path) -> dict:
51+
"""Detect src layout, packages, and test dirs."""
52+
info = {"layout": "flat", "packages": [], "test_dirs": []}
53+
src = root / "src"
54+
if src.is_dir():
55+
info["layout"] = "src-layout"
56+
info["packages"] = [d.name for d in src.iterdir() if d.is_dir() and (d / "__init__.py").exists()]
57+
else:
58+
info["packages"] = [d.name for d in root.iterdir()
59+
if d.is_dir() and (d / "__init__.py").exists()
60+
and d.name not in SKIP and d.name != "tests"]
61+
for name in ("tests", "test"):
62+
if (root / name).is_dir():
63+
info["test_dirs"].append(name)
64+
return info
65+
66+
67+
def detect_deps(root: Path) -> dict:
68+
"""Read dependencies from requirements.txt / pyproject.toml."""
69+
deps = {"source": None, "packages": []}
70+
req = root / "requirements.txt"
71+
if req.exists():
72+
deps["source"] = "requirements.txt"
73+
deps["packages"] = [ln.strip().split("==")[0].split(">=")[0].split("<")[0].strip()
74+
for ln in req.read_text(errors="ignore").splitlines()
75+
if ln.strip() and not ln.startswith(("#", "-"))][:30]
76+
pyproj = root / "pyproject.toml"
77+
if pyproj.exists():
78+
deps["source"] = deps["source"] or "pyproject.toml"
79+
m = re.search(r'dependencies\s*=\s*\[(.*?)\]', pyproj.read_text(errors="ignore"), re.DOTALL)
80+
if m:
81+
deps["packages"] += re.findall(r'"([^"<>=!~\[]+)', m.group(1))
82+
deps["packages"] = sorted(set(p.strip().lower() for p in deps["packages"] if p.strip()))
83+
return deps
84+
85+
86+
def detect_tests(root: Path, py_files: list[Path]) -> dict:
87+
"""Detect test framework (pytest/unittest) and command."""
88+
for marker in ("pytest.ini", "setup.cfg", "pyproject.toml", "tox.ini"):
89+
p = root / marker
90+
if p.exists() and ("[tool.pytest" in p.read_text(errors="ignore") or
91+
"[pytest]" in p.read_text(errors="ignore")):
92+
return {"framework": "pytest", "command": "pytest"}
93+
for f in py_files:
94+
if "test" in f.name.lower() or "test" in str(f.parent).lower():
95+
try:
96+
head = f.read_text(errors="ignore")[:2000]
97+
except OSError:
98+
continue
99+
if "import pytest" in head:
100+
return {"framework": "pytest", "command": "pytest"}
101+
if "import unittest" in head or "from unittest" in head:
102+
return {"framework": "unittest", "command": "python -m unittest discover"}
103+
return {"framework": "unknown", "command": "pytest # (no config found — guessed)"}
104+
105+
106+
def detect_linters(root: Path) -> list[str]:
107+
"""Detect linting/formatting tools from config files."""
108+
found = []
109+
checks = {"ruff": ["ruff.toml"], "flake8": [".flake8"], "mypy": ["mypy.ini", ".mypy.ini"],
110+
"pylint": [".pylintrc"], "isort": [".isort.cfg"]}
111+
toml_tools = ("ruff", "black", "isort", "mypy", "pylint", "flake8")
112+
for tool, files in checks.items():
113+
if any((root / f).exists() for f in files):
114+
found.append(tool)
115+
pyproj = root / "pyproject.toml"
116+
if pyproj.exists():
117+
text = pyproj.read_text(errors="ignore")
118+
for tool in toml_tools:
119+
if f"[tool.{tool}]" in text and tool not in found:
120+
found.append(tool)
121+
setup_cfg = root / "setup.cfg"
122+
if setup_cfg.exists():
123+
text = setup_cfg.read_text(errors="ignore")
124+
for tool in ("flake8", "isort", "mypy"):
125+
if f"[{tool}]" in text and tool not in found:
126+
found.append(tool)
127+
return sorted(set(found))
128+
129+
130+
def detect_conventions(py_files: list[Path]) -> dict:
131+
"""Sample Python files to detect coding style."""
132+
indents, hints, docs, total = Counter(), 0, 0, 0
133+
for f in py_files[:40]:
134+
try:
135+
content = f.read_text(errors="ignore")
136+
except OSError:
137+
continue
138+
total += 1
139+
for line in content.split("\n")[:200]:
140+
s = line.lstrip()
141+
if s and line != s:
142+
n = len(line) - len(s)
143+
if n in (2, 4, 8):
144+
indents[n] += 1
145+
if re.search(r'def \w+\(.*:.*\)\s*->', content[:5000]):
146+
hints += 1
147+
if re.search(r'(def|class)\s+\w+.*:\s*\n\s+"""', content[:5000]):
148+
docs += 1
149+
if total == 0:
150+
return {"indent": "4 spaces", "type_hints": False, "docstrings": False}
151+
top = indents.most_common(1)
152+
return {
153+
"indent": f"{top[0][0]} spaces" if top else "4 spaces",
154+
"type_hints": hints > total * 0.3,
155+
"docstrings": docs > total * 0.3,
156+
}
157+
158+
159+
def get_git_info(root: Path) -> dict:
160+
"""Get branch and recent commits."""
161+
info = {"branch": "", "commits": [], "has_git": False}
162+
def _run(args):
163+
try:
164+
r = subprocess.run(args, cwd=root, capture_output=True, text=True, timeout=5)
165+
return r.stdout.strip() if r.returncode == 0 else ""
166+
except (FileNotFoundError, subprocess.TimeoutExpired):
167+
return ""
168+
branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"])
169+
if branch:
170+
info["has_git"] = True
171+
info["branch"] = branch
172+
log = _run(["git", "log", "--oneline", "-8", "--no-decorate"])
173+
info["commits"] = [ln for ln in log.splitlines() if ln.strip()]
174+
return info
175+
176+
177+
def get_readme_summary(root: Path) -> str:
178+
"""Extract first meaningful paragraph from README."""
179+
for name in ("README.md", "README.rst", "README.txt", "README"):
180+
p = root / name
181+
if not p.exists():
182+
continue
183+
lines, out = p.read_text(errors="ignore")[:3000].splitlines(), []
184+
for line in lines:
185+
s = line.strip()
186+
if s.startswith(("#", "[!", "![", "===", "---")):
187+
continue
188+
if s:
189+
out.append(s)
190+
elif out:
191+
break
192+
return " ".join(out)[:500] if out else ""
193+
return ""
194+
195+
196+
def detect_scripts(root: Path) -> list[str]:
197+
"""Find Makefile targets, scripts/ dir, CLI entry points."""
198+
scripts = []
199+
mk = root / "Makefile"
200+
if mk.exists():
201+
targets = re.findall(r'^([a-zA-Z_][\w-]*):', mk.read_text(errors="ignore")[:5000], re.MULTILINE)
202+
if targets:
203+
scripts.append(f"Makefile targets: {', '.join(targets[:10])}")
204+
sd = root / "scripts"
205+
if sd.is_dir():
206+
files = [f.name for f in sd.iterdir() if f.is_file()][:10]
207+
if files:
208+
scripts.append(f"scripts/: {', '.join(files)}")
209+
pyproj = root / "pyproject.toml"
210+
if pyproj.exists():
211+
m = re.search(r'\[project\.scripts\](.*?)(\[|$)', pyproj.read_text(errors="ignore"), re.DOTALL)
212+
if m:
213+
entries = re.findall(r'(\w[\w-]*)\s*=', m.group(1))
214+
if entries:
215+
scripts.append(f"CLI entry points: {', '.join(entries[:10])}")
216+
return scripts
217+
218+
219+
def generate_claude_md(root: Path) -> str:
220+
"""Assemble the complete CLAUDE.md content."""
221+
root = root.resolve()
222+
py_files = find_py_files(root)
223+
pkg = detect_structure(root)
224+
deps = detect_deps(root)
225+
tests = detect_tests(root, py_files)
226+
linters = detect_linters(root)
227+
conv = detect_conventions(py_files)
228+
git = get_git_info(root)
229+
readme = get_readme_summary(root)
230+
scripts = detect_scripts(root)
231+
232+
out = [f"# CLAUDE.md — {root.name}\n", "> Auto-generated project context for Claude Code.\n"]
233+
if readme:
234+
out.append(f"## Overview\n\n{readme}\n")
235+
236+
out.append(f"## Project Structure\n\n```\n{build_tree(root)}\n```\n")
237+
if pkg["layout"] == "src-layout":
238+
out.append("**Layout:** src-layout (packages under `src/`)\n")
239+
if pkg["packages"]:
240+
out.append(f"**Packages:** {', '.join(pkg['packages'])}\n")
241+
out.append(f"**Python files:** {len(py_files)}\n")
242+
243+
if deps["packages"]:
244+
out.append(f"## Dependencies\n\nSource: `{deps['source']}`\n\n"
245+
f"Key packages: {', '.join(deps['packages'][:15])}\n")
246+
247+
out.append("## Coding Conventions\n\n"
248+
f"- **Indentation:** {conv['indent']}\n"
249+
f"- **Type hints:** {'Yes — use them in new code' if conv['type_hints'] else 'Sparse — match existing style'}\n"
250+
f"- **Docstrings:** {'Yes — add to public functions' if conv['docstrings'] else 'Minimal — match existing style'}\n")
251+
252+
out.append(f"## Testing\n\n**Framework:** {tests['framework']}\n\n"
253+
f"```bash\n{tests['command']}\n```\n")
254+
if pkg["test_dirs"]:
255+
out.append(f"Test directories: {', '.join(pkg['test_dirs'])}\n")
256+
257+
if linters:
258+
out.append(f"## Linting & Formatting\n\nTools: {', '.join(linters)}\n\n"
259+
"Run before committing to match project style.\n")
260+
if scripts:
261+
out.append("## Available Scripts\n\n" + "\n".join(f"- {s}" for s in scripts) + "\n")
262+
263+
if git["has_git"]:
264+
out.append(f"## Git\n\n**Default branch:** {git['branch']}\n")
265+
if git["commits"]:
266+
out.append("**Recent commits:**\n```\n" + "\n".join(git["commits"][:7]) + "\n```\n")
267+
268+
key_files = [n for n in ("setup.py", "setup.cfg", "pyproject.toml", "Makefile",
269+
"Dockerfile", "docker-compose.yml", ".env.example",
270+
"alembic.ini", "manage.py", "app.py", "main.py", "cli.py")
271+
if (root / n).exists()]
272+
if key_files:
273+
out.append("## Key Files\n\n" + "\n".join(f"- `{f}`" for f in key_files) + "\n")
274+
275+
out.append("## Working in This Codebase\n\n"
276+
"- Read existing code before modifying — match the style you see.\n"
277+
"- Run tests after changes to verify nothing broke.\n"
278+
"- Keep functions focused and under 50 lines when possible.\n"
279+
"- Commit messages: imperative mood (\"add feature\" not \"added feature\").\n")
280+
return "\n".join(out)
281+
282+
283+
def main():
284+
parser = argparse.ArgumentParser(description="Generate an optimized CLAUDE.md for a Python project.")
285+
parser.add_argument("project_dir", help="Path to the Python project root")
286+
parser.add_argument("-o", "--output", default=None, help="Output file path (default: CLAUDE.md in project root)")
287+
parser.add_argument("--stdout", action="store_true", help="Print to stdout instead of writing a file")
288+
args = parser.parse_args()
289+
project = Path(args.project_dir).resolve()
290+
if not project.is_dir():
291+
print(f"Error: {project} is not a directory", file=sys.stderr)
292+
sys.exit(1)
293+
content = generate_claude_md(project)
294+
if args.stdout:
295+
print(content)
296+
else:
297+
out_path = Path(args.output) if args.output else project / "CLAUDE.md"
298+
out_path.write_text(content)
299+
print(f"✓ Generated {out_path} ({len(content)} chars)")
300+
301+
302+
if __name__ == "__main__":
303+
main()

0 commit comments

Comments
 (0)