diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 633e428..ed15c95 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -9,3 +9,8 @@ config: MD033: false # Allow bare URLs MD034: false + +# Exclude vendored / virtual-env Markdown +ignores: + - ".venv/**" + - ".venv-ci/**" diff --git a/CHANGELOG.md b/CHANGELOG.md index 83631b5..8d55f56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## Unreleased +## [0.3.13] - 2026-03-06 + +### Added + +- `agentinit doctor` command: runs all checks (missing files, TBD content, line budgets, sync drift, llms.txt freshness, contextlint) and prints actionable fix commands with a quick-fix summary. +- `agentinit sync --diff`: show unified diffs for out-of-sync router files; works standalone or combined with `--check`. + +### Changed + +- Rewrite README with expanded documentation: installation, quick start, full/minimal modes, how-it-works diagram, CI integration, modular resources, and supported tools table. +- Deduplicate test helpers: move shared `fill_tbd` to `tests/helpers.py`; add missing `detect`/`yes` defaults to `make_init_args`; add `make_doctor_args` and update `make_sync_args` with `diff` support. +- Fix misleading test file docstrings (all previously said "Tests for agentinit.cli"). +- Add wiki documentation for architecture, commands, workflows, FAQ, troubleshooting, and contributing. + ## [0.3.12] - 2026-03-06 ### Changed diff --git a/README.md b/README.md index b866b8c..e5b90b9 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,237 @@ # agentinit -![CI](https://github.com/Lucenx9/agentinit/actions/workflows/ci.yml/badge.svg) [![PyPI](https://img.shields.io/pypi/v/agentinit.svg)](https://pypi.org/project/agentinit/) [![Python Versions](https://img.shields.io/pypi/pyversions/agentinit.svg)](https://pypi.org/project/agentinit/) +Scaffold and maintain context files for AI coding agents. + +![CI](https://github.com/Lucenx9/agentinit/actions/workflows/ci.yml/badge.svg) +[![PyPI](https://img.shields.io/pypi/v/agentinit.svg)](https://pypi.org/project/agentinit/) +[![Python 3.10+](https://img.shields.io/pypi/pyversions/agentinit.svg)](https://pypi.org/project/agentinit/) agentinit preview -Scaffold and maintain **agent context files** for modern coding assistants, with a deterministic, standard-library-only CLI. +`agentinit` generates a structured set of context files that modern AI coding assistants (Claude Code, Cursor, GitHub Copilot, Gemini CLI) read automatically. It uses a **router-first architecture**: you write your project rules once in `AGENTS.md`, and agentinit keeps the vendor-specific files in sync. + +No runtime dependencies. Pure Python standard library. Works on Linux, macOS, and Windows. -`agentinit` creates a clean router-first setup around `AGENTS.md`, plus companion files for Claude, Cursor, Copilot, Gemini CLI, and `llms.txt`. +## Why agentinit -## Why agentinit ๐ŸŽฏ +AI coding agents perform better when they have clear, structured context about your project. Without it, they guess at your stack, conventions, and constraints. -- **Single source of truth:** keep high-level rules in `AGENTS.md`. -- **Low drift workflow:** regenerate and verify router files with `sync --check`. -- **Lean context model:** keep short entry files and push detail into `docs/`. -- **No runtime dependencies:** pure Python stdlib. +**agentinit solves this by:** -## Quick Start ๐Ÿš€ +- **One source of truth** -- Write project rules in `AGENTS.md`. Vendor files (`CLAUDE.md`, `GEMINI.md`, `.cursor/rules/`, `.github/copilot-instructions.md`) are generated routers that import it. +- **Low-drift workflow** -- `sync --check` and `status --check` detect when files fall out of date. Run them in CI to catch drift early. +- **Lean context model** -- Keep always-loaded files short (under 300 lines). Push details into `docs/` where agents load them on demand. +- **Deterministic output** -- No LLM calls, no network requests. Every command produces the same output from the same input. -Requires **Python 3.10+**. +## Installation + +Requires Python 3.10 or later. ```sh -# Install (recommended) +# With pipx (recommended, installs in an isolated environment) pipx install agentinit -# Initialize in an existing repository +# With pip +pip install agentinit + +# Verify +agentinit --version +``` + +## Quick start + +### Add context files to an existing project + +```sh cd your-project -agentinit init --minimal +agentinit init ``` -Minimal profile generates: +This creates the full set of context files: + +```text +your-project/ +โ”œโ”€โ”€ AGENTS.md # Primary agent instructions (source of truth) +โ”œโ”€โ”€ CLAUDE.md # Claude Code router โ†’ imports AGENTS.md +โ”œโ”€โ”€ GEMINI.md # Gemini CLI router โ†’ imports AGENTS.md +โ”œโ”€โ”€ llms.txt # Project discovery index +โ”œโ”€โ”€ .claude/rules/ # Claude Code modular rules +โ”‚ โ”œโ”€โ”€ coding-style.md +โ”‚ โ”œโ”€โ”€ testing.md +โ”‚ โ””โ”€โ”€ repo-map.md +โ”œโ”€โ”€ .cursor/rules/project.mdc # Cursor rule routing +โ”œโ”€โ”€ .github/copilot-instructions.md # GitHub Copilot context +โ”œโ”€โ”€ .contextlintrc.json # Lint configuration +โ””โ”€โ”€ docs/ + โ”œโ”€โ”€ PROJECT.md # Stack, commands, layout, constraints + โ”œโ”€โ”€ CONVENTIONS.md # Style, naming, testing, git workflow + โ”œโ”€โ”€ TODO.md # Task tracking for agents + โ”œโ”€โ”€ DECISIONS.md # Architecture decision records + โ””โ”€โ”€ STATE.md # Session handoff notes +``` + +### Minimal mode + +For smaller projects, generate only the essential files: + +```sh +agentinit init --minimal +``` ```text your-project/ -โ”œโ”€โ”€ llms.txt โ”œโ”€โ”€ AGENTS.md โ”œโ”€โ”€ CLAUDE.md +โ”œโ”€โ”€ llms.txt โ””โ”€โ”€ docs/ โ”œโ”€โ”€ PROJECT.md โ””โ”€โ”€ CONVENTIONS.md ``` -Full profile (`agentinit init`) also includes `GEMINI.md`, `docs/STATE.md`, `docs/TODO.md`, `docs/DECISIONS.md`, Cursor/Copilot/Claude rule files, and `.contextlintrc.json`. +### After scaffolding + +1. Open `docs/PROJECT.md` and describe your project, stack, and commands. +2. Fill in `docs/CONVENTIONS.md` with your team's standards. +3. Run your coding agent -- it reads `AGENTS.md` (or its vendor-specific router) automatically. -## Core Workflow ๐Ÿงญ +Track the generated files in git so your agents can find them: ```sh -# 1) Bootstrap context files -agentinit init --detect --purpose "AI code review assistant" +git add AGENTS.md CLAUDE.md GEMINI.md llms.txt docs/ +``` -# 2) Keep llms.txt aligned with project docs -agentinit refresh-llms +## How it works -# 3) Add modular resources -agentinit add skill code-reviewer -agentinit add mcp github -agentinit add security +agentinit uses a **router-first** design. Each AI tool has its own context file format, but the content should be consistent. Instead of maintaining multiple files manually, agentinit generates thin router files that all point back to `AGENTS.md`: -# 4) Validate quality gates -agentinit status --check -agentinit sync --check -agentinit lint +```text +AGENTS.md โ† You edit this (source of truth) + โ”œโ”€โ”€ CLAUDE.md โ† @AGENTS.md (auto-generated router) + โ”œโ”€โ”€ GEMINI.md โ† @AGENTS.md (auto-generated router) + โ”œโ”€โ”€ .cursor/rules/ โ† @AGENTS.md (auto-generated router) + โ””โ”€โ”€ .github/copilot-instructions.md (auto-generated router) ``` -For minimal projects, both `status --check` and `sync --check` auto-detect the generated minimal profile. `status --minimal --check` and `sync --minimal --check` remain available if you want to force that mode explicitly. +When you run `agentinit sync`, it regenerates the router files from templates. When you run `agentinit sync --check`, it exits with code 1 if any router has drifted from the template -- useful in CI to prevent silent staleness. + +The `docs/` directory holds detailed project context that agents load on demand. This keeps the always-loaded router files short and focused. + +## Commands + +### Scaffolding + +| Command | Description | +| --- | --- | +| `agentinit init` | Add missing context files to the current directory | +| `agentinit init --minimal` | Generate only the minimal file set | +| `agentinit minimal` | Shortcut for `init --minimal` | +| `agentinit new ` | Create a new project directory and scaffold context files | +| `agentinit remove` | Delete agentinit-managed files (with confirmation) | + +Common flags for `init`, `minimal`, and `new`: -### Command Reference +| Flag | Effect | +| --- | --- | +| `--detect` | Auto-detect stack and commands from `pyproject.toml`, `package.json`, `Cargo.toml`, or `go.mod` | +| `--purpose "..."` | Set the project purpose non-interactively | +| `--prompt` | Run the interactive setup wizard (default on TTY) | +| `--translate-purpose` | Translate non-English purpose text to English for `docs/` files | +| `--skeleton fastapi` | Copy a starter project boilerplate after scaffolding | +| `--force` / `--yes` / `-y` | Overwrite existing files without confirmation | -- `agentinit init` add missing context files in current directory -- `agentinit minimal` shortcut for `init --minimal` -- `agentinit new ` create a new project and scaffold context -- `agentinit refresh-llms` (alias: `refresh`) regenerate `llms.txt` -- `agentinit sync` reconcile router files from templates -- `agentinit status` show missing/incomplete files and line budgets -- `agentinit lint` run `contextlint` checks -- `agentinit add ` install resources (`skill`, `mcp`, `security`, `soul`) -- `agentinit remove` remove or archive managed files +### Maintenance -## CI Example โœ… +| Command | Description | +| --- | --- | +| `agentinit sync` | Regenerate vendor router files from templates | +| `agentinit sync --check` | Exit 1 if routers have drifted (CI mode) | +| `agentinit sync --diff` | Show unified diff for out-of-sync routers | +| `agentinit refresh-llms` | Regenerate `llms.txt` from project files | +| `agentinit add ` | Install a modular resource (see below) | +| `agentinit remove --archive` | Move managed files to `.agentinit-archive/` instead of deleting | -Use both structure and drift checks: +### Validation + +| Command | Description | +| --- | --- | +| `agentinit status` | Show missing files, incomplete content, and line budget warnings | +| `agentinit status --check` | Exit 1 if any issues found (CI mode) | +| `agentinit lint` | Run contextlint checks (broken refs, bloat, duplication) | +| `agentinit doctor` | Run all checks and suggest fix commands | + +### Modular resources (`add`) + +Add reusable agent instructions to your project: ```sh -agentinit sync --check -agentinit status --check -agentinit lint -``` +# List available resources of a type +agentinit add skill --list + +# Install a skill (copies to .agents/skills/ or .claude/skills/) +agentinit add skill code-reviewer +agentinit add skill testing +agentinit add skill frontend-reviewer -## Tool Compatibility ๐Ÿค +# Install MCP integration guides (copies to .agents/) +agentinit add mcp github +agentinit add mcp postgres -`agentinit` is designed to work with common agentic workflows by generating: +# Install security guardrails +agentinit add security -- `AGENTS.md` as primary router -- `CLAUDE.md` for Claude Code memory/routing -- `.cursor/rules/project.mdc` for Cursor rule routing -- `.github/copilot-instructions.md` for GitHub Copilot context -- `GEMINI.md` for Gemini CLI context routing -- `llms.txt` as project discovery index +# Install agent personality definition +agentinit add soul +``` -## Troubleshooting ๐Ÿ› ๏ธ +Each `add` command also appends a reference to the resource in `AGENTS.md`. -If your agent cannot find context files: +## CI integration -- track files in git (`git add AGENTS.md CLAUDE.md GEMINI.md llms.txt docs/`) -- verify ignored files (`git status --ignored`) -- regenerate derived files (`agentinit refresh-llms` and `agentinit sync`) -- replace managed symlinks with regular files inside the repo; unsafe managed paths are skipped by design +Add these checks to your CI pipeline to catch documentation drift: -## Documentation ๐Ÿ“š +```yaml +# .github/workflows/ci.yml +- name: Check agent context + run: | + pip install agentinit + agentinit sync --check # Fail if router files drifted from templates + agentinit status --check # Fail if files are missing or incomplete + agentinit lint # Fail on broken refs, bloat, or duplication +``` -Wiki (full usage and examples): +For minimal-profile projects, `sync --check` and `status --check` auto-detect the profile. You can also force it with `--minimal`. -- [Changelog](CHANGELOG.md) -- [Wiki Home](https://github.com/Lucenx9/agentinit/wiki) -- [Quick Start](https://github.com/Lucenx9/agentinit/wiki/Quick-Start) -- [Commands](https://github.com/Lucenx9/agentinit/wiki/Commands) -- [Workflows](https://github.com/Lucenx9/agentinit/wiki/Workflows) -- [Troubleshooting](https://github.com/Lucenx9/agentinit/wiki/Troubleshooting) -- [FAQ](https://github.com/Lucenx9/agentinit/wiki/FAQ) -- [Contributing to the Wiki](https://github.com/Lucenx9/agentinit/wiki/Contributing-to-the-Wiki) +## Supported tools + +| Tool | Generated file | How it works | +| --- | --- | --- | +| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `CLAUDE.md` | Router that `@`-imports `AGENTS.md` and `docs/` files | +| [Cursor](https://cursor.com) | `.cursor/rules/project.mdc` | Project-level rules pointing to `AGENTS.md` | +| [GitHub Copilot](https://github.com/features/copilot) | `.github/copilot-instructions.md` | Repository-level instructions referencing `AGENTS.md` | +| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `GEMINI.md` | Router that imports `AGENTS.md` and `docs/` files | +| [llms.txt](https://llmstxt.org/) | `llms.txt` | Standard discovery index with project summary and key files | -## Development ๐Ÿงช +## Development ```sh python3 -m venv .venv . .venv/bin/activate pip install -e . --group dev + +# Run tests +python3 -m pytest tests/ -v + +# Lint and format check python3 -m ruff check agentinit tests cli python3 -m ruff format --check agentinit tests cli -python3 -m pytest tests/ -v ``` -On distro-managed Python installs that enforce PEP 668, run the development -commands inside a virtual environment instead of the system interpreter. +On distro-managed Python installs that enforce PEP 668, use a virtual environment instead of the system interpreter. + +## Documentation + +- [Changelog](CHANGELOG.md) +- [Wiki](https://github.com/Lucenx9/agentinit/wiki) -- detailed guides, workflows, architecture, and FAQ ## License diff --git a/agentinit/_doctor.py b/agentinit/_doctor.py new file mode 100644 index 0000000..ecb5843 --- /dev/null +++ b/agentinit/_doctor.py @@ -0,0 +1,243 @@ +"""Diagnostic command: run all checks and suggest fix commands.""" + +from __future__ import annotations + +import os +from argparse import Namespace +from pathlib import Path +from typing import Callable + +from agentinit._profiles import looks_like_minimal_profile + + +def _read_text(path: str) -> str | None: + try: + with open(path, "r", encoding="utf-8") as f: + return f.read() + except (OSError, UnicodeDecodeError): + return None + + +def _check_missing_files( + dest: str, + files: list[str], +) -> list[tuple[str, str]]: + """Return (message, fix_hint) pairs for missing managed files.""" + issues: list[tuple[str, str]] = [] + for rel in files: + path = os.path.join(dest, rel) + if not os.path.isfile(path): + issues.append((f"{rel} is missing", "agentinit init")) + return issues + + +def _check_tbd_content( + dest: str, + files: list[str], +) -> list[tuple[str, str]]: + """Return (message, fix_hint) pairs for files containing TBD.""" + issues: list[tuple[str, str]] = [] + for rel in files: + path = os.path.join(dest, rel) + content = _read_text(path) + if content and "TBD" in content: + issues.append((f"{rel} contains TBD placeholders", f"edit {rel}")) + return issues + + +def _check_line_budgets( + dest: str, + files: list[str], +) -> list[tuple[str, str]]: + """Return (message, fix_hint) for oversized always-hot files.""" + always_hot = { + f + for f in files + if not f.startswith("docs/") + and f + not in { + ".gitignore", + ".contextlintrc.json", + } + } + issues: list[tuple[str, str]] = [] + for rel in always_hot: + path = os.path.join(dest, rel) + content = _read_text(path) + if content: + lines = len(content.splitlines()) + if lines >= 300: + issues.append( + ( + f"{rel} is {lines} lines (hard limit 300)", + f"move details from {rel} to docs/", + ) + ) + return issues + + +def _check_sync_drift( + dest: str, + template_dir: str, + minimal_mode: bool, + missing_files: set[str], +) -> list[tuple[str, str]]: + """Return (message, fix_hint) for out-of-sync router files. + + Files already in *missing_files* are skipped to avoid duplicate reports. + """ + from agentinit._sync import ( + FULL_SYNC_ROUTER_FILES, + MINIMAL_SYNC_ROUTER_FILES, + _template_path_for, + ) + + router_files = MINIMAL_SYNC_ROUTER_FILES if minimal_mode else FULL_SYNC_ROUTER_FILES + issues: list[tuple[str, str]] = [] + + for rel in router_files: + if rel in missing_files: + continue + + tmpl_path = _template_path_for(rel, template_dir, minimal_mode) + if not os.path.isfile(tmpl_path): + continue + + dst = os.path.join(dest, rel) + if not os.path.isfile(dst): + continue + + expected = _read_text(tmpl_path) + current = _read_text(dst) + if expected and current != expected: + issues.append((f"{rel} is out of sync", "agentinit sync")) + + return issues + + +def _check_llms_freshness(dest: str) -> list[tuple[str, str]]: + """Check if llms.txt exists and has no unconfigured placeholders.""" + llms_path = os.path.join(dest, "llms.txt") + if not os.path.isfile(llms_path): + return [("llms.txt is missing", "agentinit refresh-llms")] + content = _read_text(llms_path) + if content and "(not configured" in content: + return [("llms.txt has unconfigured fields", "agentinit refresh-llms")] + return [] + + +def _check_contextlint( + dest: str, minimal_mode: bool, managed_files: list[str] +) -> list[tuple[str, str]]: + """Run contextlint and return issues with fix hints.""" + try: + from agentinit.contextlint_adapter import get_checks_module + + checks_mod = get_checks_module() + selected_paths = None + if minimal_mode: + selected_paths = {p for p in managed_files if p.endswith((".md", ".mdc"))} + + try: + result = checks_mod.run_checks( + root=Path(dest), selected_paths=selected_paths + ) + except TypeError: + result = checks_mod.run_checks(root=Path(dest)) + + issues: list[tuple[str, str]] = [] + for diag in result.diagnostics: + if diag.hard: + loc = f"{diag.path}:{diag.lineno}" if diag.lineno else diag.path + if "broken ref" in diag.message: + issues.append( + (f"{loc}: {diag.message}", "fix or remove the broken reference") + ) + elif "duplicate" in diag.message: + issues.append( + (f"{loc}: {diag.message}", "consolidate duplicated content") + ) + else: + issues.append((f"{loc}: {diag.message}", "agentinit lint")) + return issues + except Exception: + return [("contextlint checks unavailable", "pip install agentinit")] + + +def cmd_doctor( + args: Namespace, + *, + managed_files: list[str], + minimal_managed_files: list[str], + template_dir: str, + resolves_within: Callable[[str, str], bool], +) -> None: + """Run all checks and print actionable fix suggestions.""" + dest = os.path.abspath(".") + explicit_minimal = bool(getattr(args, "minimal", False)) + detected_minimal = not explicit_minimal and looks_like_minimal_profile(dest) + minimal_mode = explicit_minimal or detected_minimal + files = minimal_managed_files if minimal_mode else managed_files + + print("agentinit doctor") + print(f"Directory: {dest}") + if explicit_minimal: + print("Profile: minimal") + elif detected_minimal: + print("Profile: minimal (auto-detected)") + print() + + all_issues: list[tuple[str, str]] = [] + + # 1. Missing files + missing_issues = _check_missing_files(dest, files) + all_issues.extend(missing_issues) + missing_rels = {msg.split(" is missing")[0] for msg, _ in missing_issues} + + # 2. TBD content + all_issues.extend(_check_tbd_content(dest, files)) + + # 3. Line budgets + all_issues.extend(_check_line_budgets(dest, files)) + + # 4. Sync drift (skip files already reported as missing) + if os.path.isfile(os.path.join(dest, "AGENTS.md")): + all_issues.extend( + _check_sync_drift(dest, template_dir, minimal_mode, missing_rels) + ) + + # 5. llms.txt freshness (skip if already reported as missing) + if "llms.txt" not in missing_rels: + all_issues.extend(_check_llms_freshness(dest)) + + # 6. Contextlint + all_issues.extend(_check_contextlint(dest, minimal_mode, files)) + + if not all_issues: + print("All checks passed. Project is healthy.") + return + + # Deduplicate by message + seen: set[str] = set() + unique: list[tuple[str, str]] = [] + for msg, fix in all_issues: + if msg not in seen: + seen.add(msg) + unique.append((msg, fix)) + + print(f"Found {len(unique)} issue(s):\n") + for msg, fix in unique: + print(f" x {msg}") + print(f" fix: {fix}") + print() + + # Group fixes by command for a summary + fix_commands: dict[str, int] = {} + for _, fix in unique: + if fix.startswith("agentinit "): + fix_commands[fix] = fix_commands.get(fix, 0) + 1 + + if fix_commands: + print("Quick fixes:") + for cmd in sorted(fix_commands): + print(f" $ {cmd}") diff --git a/agentinit/_parser.py b/agentinit/_parser.py index 17f33d4..a3c2ea1 100644 --- a/agentinit/_parser.py +++ b/agentinit/_parser.py @@ -149,6 +149,15 @@ def build_parser(skeleton_choices, add_resource_types): help="Repository root to lint (default: current directory).", ) + # agentinit doctor + p_doctor = sub.add_parser( + "doctor", + help="Run all checks and show actionable fix commands.", + ) + p_doctor.add_argument( + "--minimal", action="store_true", help="Check only the minimal core files." + ) + # agentinit refresh-llms p_refresh = sub.add_parser( "refresh-llms", @@ -171,6 +180,11 @@ def build_parser(skeleton_choices, add_resource_types): action="store_true", help="Exit with code 1 if router files are out of sync (CI mode).", ) + p_sync.add_argument( + "--diff", + action="store_true", + help="Show unified diff for each out-of-sync router file.", + ) p_sync.add_argument( "--minimal", action="store_true", diff --git a/agentinit/_sync.py b/agentinit/_sync.py index 81899f4..48423d9 100644 --- a/agentinit/_sync.py +++ b/agentinit/_sync.py @@ -2,6 +2,7 @@ from __future__ import annotations +import difflib import os import sys from typing import Callable @@ -77,6 +78,47 @@ def _validate_destination( return None +def _diff_single_router( + rel: str, + *, + dest: str, + dest_real: str, + template_dir: str, + minimal_mode: bool, + resolves_within: Callable[[str, str], bool], +) -> str | None: + """Return a unified diff string for *rel*, or None if identical/unavailable.""" + template_path = _template_path_for(rel, template_dir, minimal_mode) + if not os.path.isfile(template_path): + return None + + dst = os.path.join(dest, rel) + if _validate_destination(dst, dest_real, resolves_within) is not None: + return None + + expected, _ = _read_text(template_path, "template") + if expected is None: + return None + + current = "" + if os.path.isfile(dst): + current, _ = _read_text(dst, "destination") + if current is None: + current = "" + + if current == expected: + return None + + from_label = f"a/{rel}" if current else "/dev/null" + diff = difflib.unified_diff( + current.splitlines(keepends=True), + expected.splitlines(keepends=True), + fromfile=from_label, + tofile=f"b/{rel}", + ) + return "".join(diff) + + def _sync_single_router( rel: str, *, @@ -162,6 +204,7 @@ def cmd_sync( dest = os.path.abspath(args.root or os.getcwd()) dest_real = os.path.realpath(dest) check_mode = bool(getattr(args, "check", False)) + diff_mode = bool(getattr(args, "diff", False)) explicit_minimal = bool(getattr(args, "minimal", False)) detected_minimal = False if not explicit_minimal: @@ -176,6 +219,23 @@ def cmd_sync( _validate_sync_root(dest) + if diff_mode: + has_diff = False + for rel in router_files: + diff_text = _diff_single_router( + rel, + dest=dest, + dest_real=dest_real, + template_dir=template_dir, + minimal_mode=minimal_mode, + resolves_within=resolves_within, + ) + if diff_text: + if not has_diff: + print() + print(diff_text, end="" if diff_text.endswith("\n") else "\n") + has_diff = True + drift: list[tuple[str, str]] = [] updated: list[tuple[str, str]] = [] unchanged: list[str] = [] diff --git a/agentinit/cli.py b/agentinit/cli.py index 7b6908f..8c716bd 100644 --- a/agentinit/cli.py +++ b/agentinit/cli.py @@ -8,6 +8,7 @@ from argparse import Namespace from agentinit._add import ADD_RESOURCE_TYPES, cmd_add as _cmd_add_impl +from agentinit._doctor import cmd_doctor as _cmd_doctor_impl from agentinit._parser import build_parser as _build_parser_impl from agentinit._project_detect import ( _replace_commands_section as _replace_commands_section_impl, @@ -276,6 +277,17 @@ def cmd_sync(args: Namespace) -> None: _cmd_sync_impl(args, template_dir=TEMPLATE_DIR, resolves_within=_resolves_within) +def cmd_doctor(args: Namespace) -> None: + """Run all checks and show actionable fix commands.""" + _cmd_doctor_impl( + args, + managed_files=MANAGED_FILES, + minimal_managed_files=MINIMAL_MANAGED_FILES, + template_dir=TEMPLATE_DIR, + resolves_within=_resolves_within, + ) + + def cmd_lint(args: Namespace) -> None: """Run contextlint on the current directory (or --root).""" from agentinit.contextlint_adapter import run_contextlint @@ -318,6 +330,7 @@ def _dispatch_command(args: Namespace, parser) -> None: "add": cmd_add, "lint": cmd_lint, "sync": cmd_sync, + "doctor": cmd_doctor, } if args.command == "minimal": diff --git a/pyproject.toml b/pyproject.toml index 1f2823a..12cd2c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentinit" -version = "0.3.12" +version = "0.3.13" description = "Scaffold agent context files into a project." readme = "README.md" license = "MIT" diff --git a/tests/helpers.py b/tests/helpers.py index f992e81..05a51e5 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -23,9 +23,11 @@ def make_args(**kwargs): def make_init_args(**kwargs): defaults = { "force": False, + "yes": False, "minimal": False, "purpose": None, "prompt": False, + "detect": False, "translate_purpose": False, "skeleton": None, } @@ -57,7 +59,25 @@ def make_add_args(**kwargs): return argparse.Namespace(**defaults) +def make_doctor_args(**kwargs): + defaults = {"minimal": False} + defaults.update(kwargs) + return argparse.Namespace(**defaults) + + def make_sync_args(**kwargs): - defaults = {"check": False, "root": None, "minimal": False} + defaults = {"check": False, "diff": False, "root": None, "minimal": False} defaults.update(kwargs) return argparse.Namespace(**defaults) + + +def fill_tbd(root, files): + """Replace all TBD markers in the given managed files.""" + from pathlib import Path + + root = Path(root) + for rel in files: + path = root / rel + if path.is_file(): + content = path.read_text(encoding="utf-8") + path.write_text(content.replace("TBD", "done"), encoding="utf-8") diff --git a/tests/test_cli_lint_add.py b/tests/test_cli_lint_add.py index 9f6e185..645c2b5 100644 --- a/tests/test_cli_lint_add.py +++ b/tests/test_cli_lint_add.py @@ -1,4 +1,4 @@ -"""Tests for agentinit.cli.""" +"""Tests for lint, add, and contextlint integration.""" import json import sys @@ -7,6 +7,7 @@ import agentinit.cli as cli from tests.helpers import ( + fill_tbd, make_add_args, make_init_args, make_lint_args, @@ -15,18 +16,11 @@ class TestCmdLint: - def _fill_tbd(self, root, files): - for rel in files: - path = root / rel - if path.is_file(): - content = path.read_text(encoding="utf-8") - path.write_text(content.replace("TBD", "done"), encoding="utf-8") - def test_lint_clean_project_exits_0(self, tmp_path, monkeypatch): """agentinit init โ†’ agentinit lint returns 0 on a clean project.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) # Rewrite AGENTS.md without broken refs (tmp_path / "AGENTS.md").write_text( "# Agents\n\nSee [project](docs/PROJECT.md).\n", encoding="utf-8" @@ -39,7 +33,7 @@ def test_status_check_exits_1_on_broken_ref(self, tmp_path, monkeypatch, capsys) """Inject broken ref in .claude/rules/, verify status --check exits 1.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) # Clean AGENTS.md of broken refs (tmp_path / "AGENTS.md").write_text( "# Agents\n\nSee [project](docs/PROJECT.md).\n", encoding="utf-8" @@ -60,7 +54,7 @@ def test_lint_format_json_valid(self, tmp_path, monkeypatch, capsys): """agentinit lint --format json produces valid JSON with expected keys.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) (tmp_path / "AGENTS.md").write_text( "# Agents\n\nSee [project](docs/PROJECT.md).\n", encoding="utf-8" ) diff --git a/tests/test_cli_project_commands.py b/tests/test_cli_project_commands.py index 6bacca0..9e22393 100644 --- a/tests/test_cli_project_commands.py +++ b/tests/test_cli_project_commands.py @@ -1,4 +1,4 @@ -"""Tests for agentinit.cli.""" +"""Tests for project commands (new, init, remove, sync, doctor, main).""" import sys from pathlib import Path @@ -8,6 +8,7 @@ import agentinit.cli as cli from tests.helpers import ( make_args, + make_doctor_args, make_init_args, make_remove_args, make_sync_args, @@ -578,6 +579,54 @@ def test_sync_requires_agents_md(self, tmp_path, monkeypatch): cli.cmd_sync(make_sync_args()) assert exc.value.code == 1 + def test_sync_diff_shows_changes(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + (tmp_path / "CLAUDE.md").write_text("custom content\n", encoding="utf-8") + + cli.cmd_sync(make_sync_args(diff=True)) + + out = capsys.readouterr().out + assert "--- a/CLAUDE.md" in out + assert "+++ b/CLAUDE.md" in out + assert "-custom content" in out + + def test_sync_diff_no_output_when_in_sync(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + + cli.cmd_sync(make_sync_args(diff=True)) + + out = capsys.readouterr().out + assert "---" not in out + assert "+++" not in out + + def test_sync_check_diff_shows_diff_and_exits_1( + self, tmp_path, monkeypatch, capsys + ): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + (tmp_path / "GEMINI.md").write_text("drift\n", encoding="utf-8") + + with pytest.raises(SystemExit) as exc: + cli.cmd_sync(make_sync_args(check=True, diff=True)) + assert exc.value.code == 1 + + out = capsys.readouterr().out + assert "--- a/GEMINI.md" in out + assert "+++ b/GEMINI.md" in out + + def test_sync_diff_missing_file_shows_dev_null(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + (tmp_path / ".github" / "copilot-instructions.md").unlink() + + cli.cmd_sync(make_sync_args(diff=True)) + + out = capsys.readouterr().out + assert "--- /dev/null" in out + assert "+++ b/.github/copilot-instructions.md" in out + def test_sync_command_from_main_with_root(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) @@ -591,6 +640,76 @@ def test_sync_command_from_main_with_root(self, tmp_path, monkeypatch): assert exc.value.code == 1 +class TestCmdDoctor: + def test_healthy_project_reports_no_issues(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args(purpose="A real project")) + cli.cmd_doctor(make_doctor_args()) + out = capsys.readouterr().out + assert "All checks passed" in out + + def test_reports_missing_files(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + (tmp_path / "GEMINI.md").unlink() + cli.cmd_doctor(make_doctor_args()) + out = capsys.readouterr().out + assert "GEMINI.md is missing" in out + assert "agentinit init" in out + + def test_missing_router_not_reported_twice(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + (tmp_path / "CLAUDE.md").unlink() + capsys.readouterr() # discard init output + cli.cmd_doctor(make_doctor_args()) + out = capsys.readouterr().out + # Should report "missing" once, not also "missing (router)" or "out of sync" + assert out.count("CLAUDE.md") == 1 + + def test_reports_sync_drift(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + (tmp_path / "CLAUDE.md").write_text("custom", encoding="utf-8") + cli.cmd_doctor(make_doctor_args()) + out = capsys.readouterr().out + assert "CLAUDE.md is out of sync" in out + assert "agentinit sync" in out + + def test_reports_llms_unconfigured(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + cli.cmd_doctor(make_doctor_args()) + out = capsys.readouterr().out + assert "llms.txt has unconfigured fields" in out + assert "agentinit refresh-llms" in out + + def test_quick_fixes_summary(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args()) + (tmp_path / "CLAUDE.md").write_text("custom", encoding="utf-8") + cli.cmd_doctor(make_doctor_args()) + out = capsys.readouterr().out + assert "Quick fixes:" in out + + def test_doctor_via_main(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args(purpose="A real project")) + monkeypatch.setattr(sys, "argv", ["agentinit", "doctor"]) + cli.main() + out = capsys.readouterr().out + assert "agentinit doctor" in out + + def test_doctor_minimal_mode(self, tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + cli.cmd_init(make_init_args(minimal=True)) + cli.cmd_doctor(make_doctor_args(minimal=True)) + out = capsys.readouterr().out + assert "Profile: minimal" in out + # Should not complain about GEMINI.md (not in minimal profile) + assert "GEMINI.md" not in out + + class TestMain: def test_no_args_prints_help(self, monkeypatch, capsys): monkeypatch.setattr(sys, "argv", ["agentinit"]) diff --git a/tests/test_cli_scaffold.py b/tests/test_cli_scaffold.py index 3c70543..3bc0361 100644 --- a/tests/test_cli_scaffold.py +++ b/tests/test_cli_scaffold.py @@ -1,4 +1,4 @@ -"""Tests for agentinit.cli.""" +"""Tests for scaffold operations (copy_template, write_todo, apply_updates, refresh_llms).""" import sys from pathlib import Path diff --git a/tests/test_cli_status_detect.py b/tests/test_cli_status_detect.py index 2cde25c..1af4da9 100644 --- a/tests/test_cli_status_detect.py +++ b/tests/test_cli_status_detect.py @@ -1,4 +1,4 @@ -"""Tests for agentinit.cli.""" +"""Tests for status, detect, and template packaging.""" import json import os @@ -8,6 +8,7 @@ import agentinit.cli as cli from tests.helpers import ( + fill_tbd, make_args, make_init_args, make_status_args, @@ -15,19 +16,11 @@ class TestCmdStatus: - def _fill_tbd(self, root, files): - """Replace all TBD markers in the given managed files.""" - for rel in files: - path = root / rel - if path.is_file(): - content = path.read_text(encoding="utf-8") - path.write_text(content.replace("TBD", "done"), encoding="utf-8") - def test_all_present_and_filled(self, tmp_path, monkeypatch, capsys): """When all files exist and none contain TBD, reports 'Ready'.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) cli.cmd_status(make_status_args()) out = capsys.readouterr().out assert "Ready" in out @@ -66,7 +59,7 @@ def test_check_exits_1_when_contextlint_unavailable( """--check should fail closed when contextlint cannot be loaded.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) def _boom(): raise RuntimeError("simulated contextlint failure") @@ -83,7 +76,7 @@ def test_check_exits_0_when_ready(self, tmp_path, monkeypatch, capsys): """--check should exit with code 0 when everything is filled.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) with pytest.raises(SystemExit) as exc: cli.cmd_status(make_status_args(check=True)) assert exc.value.code == 0 @@ -102,7 +95,7 @@ def test_minimal_checks_fewer_files(self, tmp_path, monkeypatch, capsys): """--minimal only checks the minimal core files.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args(minimal=True)) - self._fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) + fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) with pytest.raises(SystemExit) as exc: cli.cmd_status(make_status_args(minimal=True, check=True)) assert exc.value.code == 0 @@ -115,7 +108,7 @@ def test_minimal_status_ignores_non_core_contextlint_errors( ): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args(minimal=True)) - self._fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) + fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) (tmp_path / "docs" / "GUIDE.md").write_text( "See [missing](missing.md)\n", encoding="utf-8", @@ -135,7 +128,7 @@ def test_minimal_profile_auto_detected_without_flag( """A scaffolded minimal project should not require repeating --minimal.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args(minimal=True)) - self._fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) + fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) with pytest.raises(SystemExit) as exc: cli.cmd_status(make_status_args(check=True)) @@ -150,7 +143,7 @@ def test_core_only_files_without_minimal_markers_still_fail_full_check( """Auto-detection should rely on scaffold markers, not just missing files.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args(minimal=True)) - self._fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) + fill_tbd(tmp_path, cli.MINIMAL_MANAGED_FILES) agents = tmp_path / "AGENTS.md" llms = tmp_path / "llms.txt" @@ -239,7 +232,7 @@ def test_not_a_file(self, tmp_path, monkeypatch, capsys): def test_soft_line_budget(self, tmp_path, monkeypatch, capsys): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) agents = tmp_path / "AGENTS.md" agents.write_text("line\n" * 201, encoding="utf-8") @@ -254,7 +247,7 @@ def test_contextlintrc_above_300_is_warning_only( ): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) config = tmp_path / ".contextlintrc.json" config.write_text("line\n" * 301, encoding="utf-8") @@ -268,7 +261,7 @@ def test_contextlintrc_above_300_is_warning_only( def test_hard_line_budget(self, tmp_path, monkeypatch, capsys): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) agents = tmp_path / "AGENTS.md" agents.write_text("line\n" * 301, encoding="utf-8") @@ -282,7 +275,7 @@ def test_hard_line_budget(self, tmp_path, monkeypatch, capsys): def test_broken_reference(self, tmp_path, monkeypatch, capsys): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) agents = tmp_path / "AGENTS.md" agents.write_text( 'Here is a [link](docs/missing.md "Title") and `docs/also-missing.md`', @@ -302,7 +295,7 @@ def test_broken_reference_no_false_positive_from_markdown( """Markdown link syntax must not be reported as a second broken ref.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) agents = tmp_path / "AGENTS.md" agents.write_text( "[broken](docs/nope.md)", @@ -320,7 +313,7 @@ def test_gitignore_excluded_from_top_offenders(self, tmp_path, monkeypatch, caps """Ensure .gitignore is not listed in Top offenders even when it has many lines.""" monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) (tmp_path / ".gitignore").write_text( "\n".join(f"line{i}" for i in range(50)), encoding="utf-8" @@ -349,7 +342,7 @@ def test_gitignore_excluded_from_top_offenders(self, tmp_path, monkeypatch, caps def test_valid_reference(self, tmp_path, monkeypatch, capsys): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) agents = tmp_path / "AGENTS.md" agents.write_text( "Valid link: [project](docs/PROJECT.md) and [x](../secret.md)", @@ -365,7 +358,7 @@ def test_valid_reference(self, tmp_path, monkeypatch, capsys): def test_outside_reference_ignored_no_crash(self, tmp_path, monkeypatch, capsys): monkeypatch.chdir(tmp_path) cli.cmd_init(make_init_args()) - self._fill_tbd(tmp_path, cli.MANAGED_FILES) + fill_tbd(tmp_path, cli.MANAGED_FILES) agents = tmp_path / "AGENTS.md" # Test paths that resolve outside the root (should be ignored, not crash) agents.write_text("See `../secret.md` or `../../outside.txt`", encoding="utf-8") diff --git a/wiki/Architecture.md b/wiki/Architecture.md new file mode 100644 index 0000000..fb1c844 --- /dev/null +++ b/wiki/Architecture.md @@ -0,0 +1,130 @@ +# Architecture + +## Router-first design + +agentinit's core idea: you write project rules once in `AGENTS.md`, and thin **router files** keep each AI tool in sync. + +```text +AGENTS.md (source of truth -- you edit this) + โ”œโ”€โ”€ CLAUDE.md (@AGENTS.md import) + โ”œโ”€โ”€ GEMINI.md (@AGENTS.md import) + โ”œโ”€โ”€ .cursor/rules/project.mdc (references AGENTS.md) + โ”œโ”€โ”€ .github/copilot-instructions.md (references AGENTS.md) + โ””โ”€โ”€ llms.txt (generated discovery index) +``` + +Each router file is a short document (under 20 lines) that uses vendor-specific syntax to import `AGENTS.md` and the `docs/` files. When you change project rules, you update `AGENTS.md` once and run `agentinit sync` to regenerate the routers. + +## Why routers + +Without agentinit, teams maintaining context for multiple AI tools end up with duplicated instructions across `CLAUDE.md`, `GEMINI.md`, `.cursor/rules/`, and `.github/copilot-instructions.md`. These files drift apart over time. + +The router-first approach means: + +- **One place to edit** -- change `AGENTS.md`, not five files +- **Drift detection** -- `sync --check` catches stale routers in CI +- **Token efficiency** -- router files are short; detailed context lives in `docs/` and loads on demand + +## Profiles + +agentinit supports two profiles that control which files get generated. + +### Full profile (default) + +Generated by `agentinit init`: + +| File | Purpose | +| --- | --- | +| `AGENTS.md` | Primary agent instructions (source of truth) | +| `CLAUDE.md` | Claude Code router | +| `GEMINI.md` | Gemini CLI router | +| `llms.txt` | Discovery index for AI tools | +| `.claude/rules/coding-style.md` | Coding style rules for Claude Code | +| `.claude/rules/testing.md` | Testing rules for Claude Code | +| `.claude/rules/repo-map.md` | Repository layout map for Claude Code | +| `.cursor/rules/project.mdc` | Cursor project rules | +| `.github/copilot-instructions.md` | GitHub Copilot instructions | +| `.contextlintrc.json` | Lint configuration for contextlint | +| `docs/PROJECT.md` | Stack, commands, layout, constraints | +| `docs/CONVENTIONS.md` | Style, naming, testing, git workflow | +| `docs/TODO.md` | Task tracking for agents | +| `docs/DECISIONS.md` | Architecture decision records | +| `docs/STATE.md` | Session handoff notes | + +### Minimal profile + +Generated by `agentinit init --minimal` or `agentinit minimal`: + +| File | Purpose | +| --- | --- | +| `AGENTS.md` | Primary agent instructions (with minimal-profile marker) | +| `CLAUDE.md` | Claude Code router (minimal variant) | +| `llms.txt` | Discovery index | +| `docs/PROJECT.md` | Stack, commands, layout | +| `docs/CONVENTIONS.md` | Style and conventions | + +The minimal profile omits `GEMINI.md`, all `.claude/rules/`, `.cursor/rules/`, `.github/copilot-instructions.md`, `.contextlintrc.json`, and the `docs/TODO.md`, `docs/DECISIONS.md`, `docs/STATE.md` files. + +### Profile detection + +agentinit auto-detects which profile a project uses by checking for a marker comment (``) in `AGENTS.md` and by checking which files exist. This means `sync --check` and `status --check` work correctly without the `--minimal` flag. + +## Template system + +Generated files are copied from the `agentinit/template/` directory inside the package. The template directory structure mirrors the project output: + +```text +agentinit/template/ +โ”œโ”€โ”€ AGENTS.md, CLAUDE.md, GEMINI.md, llms.txt +โ”œโ”€โ”€ .claude/rules/ +โ”œโ”€โ”€ .cursor/rules/ +โ”œโ”€โ”€ .github/ +โ”œโ”€โ”€ docs/ +โ”œโ”€โ”€ minimal/ # Override templates for minimal profile +โ”‚ โ”œโ”€โ”€ AGENTS.md +โ”‚ โ”œโ”€โ”€ CLAUDE.md +โ”‚ โ””โ”€โ”€ llms.txt +โ”œโ”€โ”€ skeletons/ # Starter project boilerplate +โ”‚ โ””โ”€โ”€ fastapi/ +โ””โ”€โ”€ add/ # Modular resources + โ”œโ”€โ”€ skills/ + โ”œโ”€โ”€ mcp/ + โ”œโ”€โ”€ security.md + โ””โ”€โ”€ soul.md +``` + +When you run `agentinit init`, the tool copies each template file to the project root, skipping files that already exist (unless `--force` is used). + +## llms.txt generation + +`llms.txt` is rendered from a template with placeholder variables: + +- `{{PROJECT_NAME}}` -- Detected from `pyproject.toml` โ†’ `package.json` โ†’ `docs/PROJECT.md` โ†’ directory name +- `{{PROJECT_SUMMARY}}` -- Extracted from the Purpose field in `docs/PROJECT.md` +- `{{KEY_FILES}}` -- Lists existing context files with relative links +- `{{HARDENED_MANDATES}}` -- Extracts `MUST ALWAYS` / `MUST NEVER` rules from `AGENTS.md` +- `{{SKILLS_AND_ROUTERS}}` -- Lists installed resources from `.agents/` + +Run `agentinit refresh-llms` to regenerate it after making changes. + +## Stack detection + +The `--detect` flag reads manifest files to auto-fill commands in `docs/PROJECT.md`: + +| Manifest | Detected information | +| --- | --- | +| `pyproject.toml` | Python project name, setup/test/lint commands (pip, uv, poetry) | +| `package.json` | Node.js project name, npm/yarn scripts | +| `Cargo.toml` | Rust project name, cargo commands | +| `go.mod` | Go module path, go build/test commands | + +Detection only fills factual fields (language, commands). It does not generate prose descriptions. + +## Security hardening + +agentinit validates all file operations: + +- Managed file paths are checked to stay within the project root +- Symlink destinations outside the project are rejected +- Skeleton copying filters build artifacts, cache directories, and `.egg-info` +- Resource names from `add` are validated against traversal patterns diff --git a/wiki/Commands.md b/wiki/Commands.md new file mode 100644 index 0000000..d60c8c3 --- /dev/null +++ b/wiki/Commands.md @@ -0,0 +1,166 @@ +# Commands + +Full reference for every agentinit command and flag. + +Run `agentinit --help` or `agentinit --help` for built-in usage. + +## `init` + +Add missing context files to the current directory. Existing files are preserved unless `--force` is used. + +```sh +agentinit init [flags] +``` + +| Flag | Effect | +| --- | --- | +| `--minimal` | Generate only `AGENTS.md`, `CLAUDE.md`, `llms.txt`, `docs/PROJECT.md`, `docs/CONVENTIONS.md` | +| `--detect` | Auto-detect stack and commands from manifest files (`pyproject.toml`, `package.json`, `Cargo.toml`, `go.mod`) | +| `--purpose "..."` | Pre-fill the Purpose field in `AGENTS.md` and `docs/PROJECT.md` | +| `--prompt` | Run the interactive setup wizard (default when on a TTY) | +| `--translate-purpose` | Translate non-English purpose text (Italian, Spanish, French) to English for `docs/` files | +| `--skeleton fastapi` | Copy starter project boilerplate after scaffolding context files | +| `--force` | Overwrite existing agentinit-managed files | +| `--yes` / `-y` | Alias for `--force`; skip all confirmation prompts | + +## `minimal` + +Shortcut for `init --minimal`. Accepts all the same flags as `init`. + +```sh +agentinit minimal [flags] +``` + +## `new` + +Create a new project directory and scaffold context files inside it. + +```sh +agentinit new [flags] +``` + +| Flag | Effect | +| --- | --- | +| `--dir ` | Parent directory for the new project (default: current directory) | + +All `init` flags (`--minimal`, `--detect`, `--purpose`, `--prompt`, `--skeleton`, `--force`, `--yes`) also apply. + +## `remove` + +Remove agentinit-managed files from the current directory. Non-managed files are never touched. + +```sh +agentinit remove [flags] +``` + +| Flag | Effect | +| --- | --- | +| `--dry-run` | Print what would be deleted without changing anything | +| `--archive` | Move files to `.agentinit-archive//` instead of deleting | +| `--force` | Skip the confirmation prompt | + +Empty parent directories (`docs/`, `.cursor/rules/`, `.claude/rules/`) are cleaned up after removal. + +## `sync` + +Regenerate vendor router files (`CLAUDE.md`, `GEMINI.md`, `.cursor/rules/project.mdc`, `.github/copilot-instructions.md`) from their templates. Only updates files that have drifted. + +```sh +agentinit sync [flags] +``` + +| Flag | Effect | +| --- | --- | +| `--check` | Exit with code 1 if any router file is out of sync (CI mode) | +| `--diff` | Show a unified diff for each out-of-sync file | +| `--minimal` | Sync only the minimal router set (`CLAUDE.md` only) | +| `--root ` | Project root directory (default: current directory) | + +For minimal-profile projects, `sync` auto-detects the profile. Use `--minimal` to force it. + +## `status` + +Show which context files are present, missing, incomplete, or exceeding line budgets. Also runs contextlint checks. + +```sh +agentinit status [flags] +``` + +| Flag | Effect | +| --- | --- | +| `--check` | Exit with code 1 if any issues are found (CI mode) | +| `--minimal` | Check only the minimal file set | + +Status checks include: + +- Missing managed files +- Files still containing `(not configured)` placeholders +- Files exceeding the 300-line hard limit or 200-line warning threshold +- Broken `@`-references in `AGENTS.md` +- contextlint violations (broken refs, bloat, duplication) + +## `doctor` + +Run all available checks (status, sync drift, llms.txt freshness, contextlint) and print grouped fix suggestions. + +```sh +agentinit doctor [flags] +``` + +| Flag | Effect | +| --- | --- | +| `--minimal` | Check only the minimal file set | + +## `refresh-llms` + +Regenerate `llms.txt` from current project files. Alias: `refresh`. + +```sh +agentinit refresh-llms [--root ] +``` + +The generated `llms.txt` includes: + +- Project name (detected from `pyproject.toml`, `package.json`, or directory name) +- Project summary (from `docs/PROJECT.md` Purpose field) +- Key context files with relative links +- Hardened mandates extracted from `AGENTS.md` +- References to installed resources in `.agents/` + +## `lint` + +Run contextlint checks on agent context files. + +```sh +agentinit lint [flags] +``` + +| Flag | Effect | +| --- | --- | +| `--config ` | Path to `.contextlintrc.json` config file | +| `--format text\|json` | Output format (default: `text`) | +| `--no-dup` | Disable duplicate-block detection | +| `--root ` | Repository root to lint (default: current directory) | + +## `add` + +Install modular resources into the current project. + +```sh +agentinit add [flags] +agentinit add --list +``` + +| Type | Available resources | Install location | +| --- | --- | --- | +| `skill` | `code-reviewer`, `testing`, `frontend-reviewer` | `.agents/skills//` or `.claude/skills//` | +| `mcp` | `github`, `postgres` | `.agents/mcp-.md` | +| `security` | *(no name needed)* | `.agents/security.md` | +| `soul` | *(no name needed)* | `.agents/soul.md` | + +| Flag | Effect | +| --- | --- | +| `--list` | List available resources for the given type | +| `--force` | Overwrite if the resource already exists | + +Each `add` command appends a reference line to the appropriate section in `AGENTS.md`. diff --git a/wiki/Contributing.md b/wiki/Contributing.md new file mode 100644 index 0000000..f0a26c9 --- /dev/null +++ b/wiki/Contributing.md @@ -0,0 +1,93 @@ +# Contributing + +## Development setup + +```sh +git clone https://github.com/Lucenx9/agentinit.git +cd agentinit +python3 -m venv .venv +. .venv/bin/activate +pip install -e . --group dev +``` + +On distro-managed Python installs (PEP 668), the virtual environment is required. + +## Running tests + +```sh +# Full test suite +python3 -m pytest tests/ -v + +# With coverage +python3 -m pytest tests/ -v --cov=agentinit --cov-report=term-missing + +# Single test file +python3 -m pytest tests/test_cli_project_commands.py -v +``` + +The test suite (170+ tests) covers all CLI commands, template operations, profile detection, security checks, and edge cases. + +## Linting and formatting + +```sh +python3 -m ruff check agentinit tests cli +python3 -m ruff format --check agentinit tests cli +``` + +Both checks must pass before committing. CI runs these on every push. + +## Project structure + +```text +agentinit/ +โ”œโ”€โ”€ cli.py # CLI entrypoint, color helpers, stable wrappers +โ”œโ”€โ”€ _parser.py # Argument parser builder +โ”œโ”€โ”€ _scaffold.py # Core scaffold and file operations +โ”œโ”€โ”€ _status.py # Status command implementation +โ”œโ”€โ”€ _sync.py # Router file sync logic +โ”œโ”€โ”€ _add.py # Resource installation (add command) +โ”œโ”€โ”€ _doctor.py # Diagnostic checks and fix suggestions +โ”œโ”€โ”€ _llms.py # llms.txt rendering +โ”œโ”€โ”€ _project_detect.py # Stack detection and language translation +โ”œโ”€โ”€ _project_updates.py # Project doc updates and wizard logic +โ”œโ”€โ”€ _profiles.py # Profile detection (minimal vs full) +โ”œโ”€โ”€ _contextlint/ # Vendored contextlint implementation +โ””โ”€โ”€ template/ # Template files copied to user projects +``` + +The CLI is a thin dispatch layer (`cli.py` + `_parser.py`). Each command's logic lives in a focused internal module. + +## Adding a template file + +1. Add the file to `agentinit/template/` +2. If it's a managed file, add its relative path to `MANAGED_FILES` in `cli.py` +3. If it should be removable, add it to `REMOVABLE_FILES` +4. If it has a minimal-profile variant, add an override in `template/minimal/` +5. Add test coverage +6. Run the full test suite + +## Adding a modular resource + +1. Add the resource file to `agentinit/template/add//` +2. If it's a new resource type, register it in `_add.py` (`ADD_RESOURCE_TYPES`) +3. Add test coverage +4. Run the full test suite + +## CI pipeline + +CI runs on every push and PR to `main`: + +- **Lint** -- ruff check + format (Python 3.13) +- **Dependency audit** -- pip-audit +- **Unit tests** -- pytest with coverage (Python 3.10-3.13) +- **Package check** -- Build and validate wheel/sdist +- **Smoke tests** -- Install from wheel and run commands (Linux, macOS, Windows x Python 3.10-3.13) + +## Submitting changes + +1. Fork the repository +2. Create a branch for your change +3. Run `python3 -m pytest tests/ -v` and `python3 -m ruff check agentinit tests cli` +4. Open a pull request against `main` + +Keep changes minimal and focused. Run tests before committing. diff --git a/wiki/FAQ.md b/wiki/FAQ.md new file mode 100644 index 0000000..d7e9ae6 --- /dev/null +++ b/wiki/FAQ.md @@ -0,0 +1,61 @@ +# FAQ + +## What is AGENTS.md? + +`AGENTS.md` is a convention for providing structured instructions to AI coding agents. It serves as the primary entry point that agents read when starting a session. agentinit generates it with sections for purpose, core mandates, commands, conventions, and context file references. + +## What is llms.txt? + +`llms.txt` is a proposed standard ([llmstxt.org](https://llmstxt.org/)) for providing a machine-readable project summary. agentinit generates it with your project name, purpose, key files, and extracted mandates from `AGENTS.md`. + +## Does agentinit use AI or make network requests? + +No. agentinit is deterministic and offline. It copies template files and fills in values from your manifest files. No LLM calls, no API requests, no telemetry. + +## What are the runtime dependencies? + +None. agentinit uses only the Python standard library. Dev dependencies (pytest, ruff) are only needed for contributing. + +## Can I edit the generated files? + +Yes, with a caveat: + +- **`AGENTS.md` and `docs/` files** -- Edit freely. These are your source of truth. +- **Router files** (`CLAUDE.md`, `GEMINI.md`, `.cursor/rules/project.mdc`, `.github/copilot-instructions.md`) -- You can edit them, but `agentinit sync` will overwrite your changes. If you need custom content in a router, add it to `AGENTS.md` instead. +- **`llms.txt`** -- Regenerated by `agentinit refresh-llms`. Custom edits will be overwritten. + +## Should I commit the generated files? + +Yes. AI agents need to read them, and most agents only see files tracked in git. Run: + +```sh +git add AGENTS.md CLAUDE.md GEMINI.md llms.txt docs/ +``` + +## What's the difference between minimal and full profile? + +**Minimal** generates 5 files: `AGENTS.md`, `CLAUDE.md`, `llms.txt`, `docs/PROJECT.md`, `docs/CONVENTIONS.md`. Good for small projects or when you only use Claude Code. + +**Full** generates 15+ files including `GEMINI.md`, Cursor/Copilot rules, Claude Code modular rules, lint config, and additional docs (`TODO.md`, `DECISIONS.md`, `STATE.md`). Use this when you work with multiple AI tools or want the full session-handoff workflow. + +## How does sync differ from init? + +`init` creates files that don't exist yet. `sync` regenerates router files from templates, overwriting them if they've changed. Use `init` for first-time setup and `sync` for ongoing maintenance. + +## Can I use agentinit with tools not listed? + +Yes. The `llms.txt` file follows an open standard that any tool can read. `AGENTS.md` is a plain Markdown file that any agent can parse. The vendor-specific routers are optional -- if you don't use Cursor, the `.cursor/rules/project.mdc` file is harmless. + +## Why are line budgets enforced? + +Research shows that shorter, focused context files lead to better agent performance. agentinit warns at 200 lines and fails `status --check` at 300 lines for always-loaded files. This encourages pushing detailed content into `docs/` where agents load it on demand. + +## How do I add support for a new AI tool? + +If a tool reads a specific file (like `AGENTS.md` or `llms.txt`), it may already work. For tool-specific router files, you can: + +1. Add a template to `agentinit/template/` +2. Register it in the sync logic +3. Open a pull request + +See [Contributing](Contributing.md) for development setup. diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..a21094f --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,27 @@ +# agentinit Wiki + +**agentinit** scaffolds and maintains context files for AI coding agents. It generates a structured set of files that Claude Code, Cursor, GitHub Copilot, and Gemini CLI read automatically, using a router-first architecture centered on `AGENTS.md`. + +For installation and a quick overview, see the [README](https://github.com/Lucenx9/agentinit#readme). + +## Pages + +- **[Quick Start](Quick-Start.md)** -- Step-by-step setup for new and existing projects +- **[Commands](Commands.md)** -- Full reference for every command and flag +- **[Architecture](Architecture.md)** -- Router-first design, profiles, templates, and generated files +- **[Workflows](Workflows.md)** -- CI integration, team workflows, and maintenance patterns +- **[Troubleshooting](Troubleshooting.md)** -- Common problems and solutions +- **[FAQ](FAQ.md)** -- Frequently asked questions +- **[Contributing](Contributing.md)** -- Development setup, testing, and how to contribute + +## Key concepts + +| Concept | Meaning | +| --- | --- | +| **Router file** | A thin vendor-specific file (e.g. `CLAUDE.md`) that imports `AGENTS.md` via `@`-references | +| **Source of truth** | `AGENTS.md` -- the single file where you define project rules | +| **Profile** | Either `minimal` (5 files) or `full` (16+ files); controls what gets generated | +| **Managed file** | A file created and maintained by agentinit; safe to regenerate | +| **Context file** | Any file that provides instructions or context to an AI coding agent | +| **Sync** | Regenerating router files from templates to match the current `AGENTS.md` | +| **Drift** | When a managed router file no longer matches its template | diff --git a/wiki/Quick-Start.md b/wiki/Quick-Start.md new file mode 100644 index 0000000..04f14f8 --- /dev/null +++ b/wiki/Quick-Start.md @@ -0,0 +1,99 @@ +# Quick Start + +## Prerequisites + +- Python 3.10 or later +- An existing project directory (or use `agentinit new` to create one) + +## Install + +```sh +# Recommended: install in an isolated environment +pipx install agentinit + +# Alternative: install with pip +pip install agentinit +``` + +## Initialize an existing project + +```sh +cd your-project +agentinit init +``` + +This generates the full set of context files. You'll see output listing each created file. + +For smaller projects, use minimal mode: + +```sh +agentinit init --minimal +``` + +## Create a new project from scratch + +```sh +agentinit new my-project +``` + +This creates the `my-project/` directory and scaffolds context files inside it. + +To also copy a starter project boilerplate: + +```sh +agentinit new my-api --skeleton fastapi +``` + +## Auto-detect your stack + +If your project already has a `pyproject.toml`, `package.json`, `Cargo.toml`, or `go.mod`, agentinit can fill in the commands section automatically: + +```sh +agentinit init --detect +``` + +This reads your manifest files and populates `docs/PROJECT.md` with the correct setup, build, test, lint, and run commands. + +## Set a project purpose + +Provide a purpose description to pre-fill the Purpose field in `AGENTS.md` and `docs/PROJECT.md`: + +```sh +agentinit init --detect --purpose "REST API for managing user subscriptions" +``` + +For non-interactive environments (CI, scripts), combine with `--yes`: + +```sh +agentinit init --detect --purpose "REST API" --yes +``` + +## Interactive wizard + +On a TTY, agentinit runs an interactive wizard by default. It asks for your project purpose and lets you confirm before writing files. You can explicitly request it: + +```sh +agentinit init --prompt +``` + +Or skip it entirely: + +```sh +agentinit init --yes +``` + +## After scaffolding + +1. **Fill in `docs/PROJECT.md`** with your project's stack, commands, and layout. +2. **Fill in `docs/CONVENTIONS.md`** with your team's style, naming, testing, and git workflow standards. +3. **Track files in git** so your AI agents can find them: + +```sh +git add AGENTS.md CLAUDE.md GEMINI.md llms.txt docs/ +``` + +1. **Start your AI coding agent.** It will read `AGENTS.md` (or the vendor-specific router) automatically and follow your project rules. + +## What was generated + +See the [Architecture](Architecture.md) page for a detailed explanation of every generated file, the router-first design, and how minimal vs full profiles differ. diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md new file mode 100644 index 0000000..bc3da48 --- /dev/null +++ b/wiki/Troubleshooting.md @@ -0,0 +1,109 @@ +# Troubleshooting + +## Agent cannot find context files + +AI agents typically only read files that are tracked in git. + +```sh +# Check if files are tracked +git status + +# Track the generated files +git add AGENTS.md CLAUDE.md GEMINI.md llms.txt docs/ + +# Check if files are gitignored +git status --ignored +``` + +If your `.gitignore` excludes any managed files, add explicit exceptions: + +```gitignore +# Allow agent context files +!AGENTS.md +!CLAUDE.md +!GEMINI.md +!llms.txt +!docs/ +``` + +## Router files are out of sync + +If `agentinit sync --check` fails in CI: + +```sh +# See what's different +agentinit sync --diff + +# Regenerate routers +agentinit sync + +# Commit the updated files +git add CLAUDE.md GEMINI.md .cursor/rules/project.mdc .github/copilot-instructions.md +git commit -m "Sync agent router files" +``` + +## Status check fails + +If `agentinit status --check` fails: + +```sh +# See the full report +agentinit status + +# Run doctor for grouped fix suggestions +agentinit doctor +``` + +Common causes: + +- **Missing files** -- Run `agentinit init` to regenerate them +- **Unfilled placeholders** -- Open `docs/PROJECT.md` and `docs/CONVENTIONS.md` and replace `(not configured)` with real values +- **Files over 300 lines** -- Move detailed content to separate files in `docs/` and use `@`-imports +- **Broken references** -- Check that files referenced in `AGENTS.md` actually exist + +## Symlink warnings + +agentinit skips managed file paths that resolve to symlinks pointing outside the project root. This is a security measure. + +If you see "skipped: symlink" warnings: + +- Replace the symlink with a regular file inside the repository +- Or remove the symlink and let agentinit create the file + +## PEP 668 errors on Linux + +On distro-managed Python installs (Debian, Ubuntu, Fedora), `pip install` may fail with an "externally managed environment" error. + +**Fix:** Use `pipx` (recommended) or create a virtual environment: + +```sh +# Option 1: pipx +pipx install agentinit + +# Option 2: virtual environment +python3 -m venv .venv +. .venv/bin/activate +pip install agentinit +``` + +## `--detect` doesn't find my stack + +`--detect` reads specific manifest files: + +- `pyproject.toml` (Python) +- `package.json` (Node.js) +- `Cargo.toml` (Rust) +- `go.mod` (Go) + +If your manifest file isn't in the project root, `--detect` won't find it. In that case, fill in `docs/PROJECT.md` manually. + +## llms.txt shows wrong project name + +`agentinit refresh-llms` detects the project name in this order: + +1. `name` field in `pyproject.toml` +2. `name` field in `package.json` +3. Purpose text in `docs/PROJECT.md` +4. Directory name + +If the wrong name appears, check your manifest files or set the Purpose field in `docs/PROJECT.md`. diff --git a/wiki/Workflows.md b/wiki/Workflows.md new file mode 100644 index 0000000..ca78071 --- /dev/null +++ b/wiki/Workflows.md @@ -0,0 +1,111 @@ +# Workflows + +## CI pipeline + +Add agentinit checks to your CI to prevent documentation drift. + +### GitHub Actions + +```yaml +name: Agent Context +on: [push, pull_request] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install agentinit + - name: Check router sync + run: agentinit sync --check + - name: Check file completeness + run: agentinit status --check + - name: Lint context files + run: agentinit lint +``` + +### What each check does + +| Command | Catches | +| --- | --- | +| `sync --check` | Router files that have drifted from their templates | +| `status --check` | Missing files, unfilled placeholders, files over 300 lines, broken `@`-references | +| `lint` | Broken refs, content bloat, duplicated blocks across files | + +All three commands exit with code 0 on success and code 1 on failure. + +## Keeping files in sync + +After editing `AGENTS.md` or `docs/` files: + +```sh +# Regenerate router files +agentinit sync + +# Regenerate llms.txt +agentinit refresh-llms +``` + +Run `agentinit doctor` to check everything at once and see grouped fix suggestions. + +## Adding resources over time + +As your project grows, add modular resources: + +```sh +# Add a code review skill +agentinit add skill code-reviewer + +# Add GitHub MCP integration guide +agentinit add mcp github + +# Add security guardrails +agentinit add security +``` + +Each command copies the resource file and appends a reference in `AGENTS.md`. + +## Team onboarding + +When a new team member clones the repository, the context files are already in git. Their AI coding agent reads `AGENTS.md` (or the vendor-specific router) and follows the project rules immediately. + +If context files were not tracked: + +```sh +# Regenerate everything +agentinit init + +# Or just the routers +agentinit sync +``` + +## Removing agentinit + +To remove all managed files: + +```sh +# Preview what will be deleted +agentinit remove --dry-run + +# Archive instead of deleting +agentinit remove --archive + +# Delete immediately +agentinit remove --force +``` + +Non-managed files are never touched. Empty directories (`docs/`, `.cursor/rules/`, etc.) are cleaned up after removal. + +## Profile migration + +To upgrade from minimal to full profile: + +```sh +# Re-run init without --minimal to add the remaining files +agentinit init +``` + +This adds the missing full-profile files without overwriting existing ones. diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md new file mode 100644 index 0000000..c8c887c --- /dev/null +++ b/wiki/_Sidebar.md @@ -0,0 +1,21 @@ +# agentinit + +## Getting Started + +- [[Home]] +- [[Quick Start]] + +## Reference + +- [[Commands]] +- [[Architecture]] +- [[Workflows]] + +## Help + +- [[Troubleshooting]] +- [[FAQ]] + +## Development + +- [[Contributing]]