agentmd-gen 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. agentmd_gen-0.2.0/.github/workflows/ci.yml +27 -0
  2. agentmd_gen-0.2.0/.gitignore +6 -0
  3. agentmd_gen-0.2.0/LICENSE +21 -0
  4. agentmd_gen-0.2.0/PKG-INFO +137 -0
  5. agentmd_gen-0.2.0/README.md +119 -0
  6. agentmd_gen-0.2.0/agentmd/__init__.py +4 -0
  7. agentmd_gen-0.2.0/agentmd/analyzer.py +303 -0
  8. agentmd_gen-0.2.0/agentmd/cli.py +304 -0
  9. agentmd_gen-0.2.0/agentmd/detectors/__init__.py +23 -0
  10. agentmd_gen-0.2.0/agentmd/detectors/ci.py +28 -0
  11. agentmd_gen-0.2.0/agentmd/detectors/common.py +56 -0
  12. agentmd_gen-0.2.0/agentmd/detectors/context_completeness.py +325 -0
  13. agentmd_gen-0.2.0/agentmd/detectors/framework.py +111 -0
  14. agentmd_gen-0.2.0/agentmd/detectors/go.py +127 -0
  15. agentmd_gen-0.2.0/agentmd/detectors/language.py +48 -0
  16. agentmd_gen-0.2.0/agentmd/detectors/lint.py +70 -0
  17. agentmd_gen-0.2.0/agentmd/detectors/package_manager.py +60 -0
  18. agentmd_gen-0.2.0/agentmd/detectors/rust.py +123 -0
  19. agentmd_gen-0.2.0/agentmd/detectors/swift.py +111 -0
  20. agentmd_gen-0.2.0/agentmd/detectors/test_runner.py +80 -0
  21. agentmd_gen-0.2.0/agentmd/generators/__init__.py +22 -0
  22. agentmd_gen-0.2.0/agentmd/generators/base.py +369 -0
  23. agentmd_gen-0.2.0/agentmd/generators/claude.py +196 -0
  24. agentmd_gen-0.2.0/agentmd/generators/codex.py +167 -0
  25. agentmd_gen-0.2.0/agentmd/generators/copilot.py +230 -0
  26. agentmd_gen-0.2.0/agentmd/generators/cursor.py +229 -0
  27. agentmd_gen-0.2.0/agentmd/scorer.py +104 -0
  28. agentmd_gen-0.2.0/agentmd/types.py +97 -0
  29. agentmd_gen-0.2.0/all-day-build-contract-agentmd-v0.1.md +174 -0
  30. agentmd_gen-0.2.0/all-day-build-contract-agentmd-v0.2.md +120 -0
  31. agentmd_gen-0.2.0/progress-log.md +73 -0
  32. agentmd_gen-0.2.0/pyproject.toml +59 -0
  33. agentmd_gen-0.2.0/tests/unit/test_analyzer.py +47 -0
  34. agentmd_gen-0.2.0/tests/unit/test_ci_detector.py +16 -0
  35. agentmd_gen-0.2.0/tests/unit/test_cli.py +217 -0
  36. agentmd_gen-0.2.0/tests/unit/test_cli_json.py +267 -0
  37. agentmd_gen-0.2.0/tests/unit/test_framework_detector.py +35 -0
  38. agentmd_gen-0.2.0/tests/unit/test_generator_languages.py +569 -0
  39. agentmd_gen-0.2.0/tests/unit/test_generators.py +301 -0
  40. agentmd_gen-0.2.0/tests/unit/test_go_detector.py +269 -0
  41. agentmd_gen-0.2.0/tests/unit/test_language_detector.py +34 -0
  42. agentmd_gen-0.2.0/tests/unit/test_lint_detector.py +26 -0
  43. agentmd_gen-0.2.0/tests/unit/test_package_manager_detector.py +27 -0
  44. agentmd_gen-0.2.0/tests/unit/test_rust_detector.py +309 -0
  45. agentmd_gen-0.2.0/tests/unit/test_scorer.py +236 -0
  46. agentmd_gen-0.2.0/tests/unit/test_scorer_false_positives.py +215 -0
  47. agentmd_gen-0.2.0/tests/unit/test_swift_detector.py +190 -0
  48. agentmd_gen-0.2.0/tests/unit/test_test_runner_detector.py +37 -0
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up Python ${{ matrix.python-version }}
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install dependencies
24
+ run: pip install -e ".[dev]"
25
+
26
+ - name: Run tests
27
+ run: pytest tests/unit -q
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ .venv/
3
+ *.pyc
4
+ .pytest_cache/
5
+ dist/
6
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mikiships
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentmd-gen
3
+ Version: 0.2.0
4
+ Summary: Analyze codebases and generate optimized context files for AI coding agents (Claude Code, Codex, Cursor, Copilot)
5
+ Author: mikiships
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: typer>=0.16.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
16
+ Requires-Dist: pytest>=8.2.0; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # agentmd
20
+
21
+ agentmd analyzes your codebase and generates optimized context files for AI coding agents. Point it at any Python, Swift/Xcode, Rust, Go, TypeScript, or multi-language project and it produces ready-to-use `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or Copilot instruction files — scored and ranked so your agent starts with the best possible picture of your project.
22
+
23
+ ## What's New in 0.2.0
24
+
25
+ - **Multi-language support** — full detection and generation for Python, Swift/Xcode, Rust, and Go projects (see [Supported Languages](#supported-languages))
26
+ - **`--json` flag** — all commands now support structured JSON output for scripting and CI integration
27
+ - **Freshness scoring fixes** — eliminated false positives in freshness scoring that penalized projects using current stable versions
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install agentmd
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### scan — inspect a project
38
+
39
+ ```bash
40
+ agentmd scan # scan current directory
41
+ agentmd scan ~/repos/myapp # scan a specific path
42
+ agentmd scan --json # output as JSON
43
+ ```
44
+
45
+ Prints detected languages, frameworks, package managers, test runners, linters, CI systems, and existing context files.
46
+
47
+ ### generate — create agent context files
48
+
49
+ ```bash
50
+ agentmd generate # generate for all supported agents
51
+ agentmd generate --agent claude # Claude Code (CLAUDE.md)
52
+ agentmd generate --agent codex # OpenAI Codex (AGENTS.md)
53
+ agentmd generate --agent cursor # Cursor (.cursorrules)
54
+ agentmd generate --agent copilot # GitHub Copilot (.github/copilot-instructions.md)
55
+ agentmd generate --out ./docs/ # write to a custom directory
56
+ agentmd generate --json # output generated content as JSON
57
+ ```
58
+
59
+ ### score — evaluate existing context files
60
+
61
+ ```bash
62
+ agentmd score # score all context files in cwd
63
+ agentmd score CLAUDE.md # score a specific file
64
+ agentmd score --json # output scores as JSON
65
+ ```
66
+
67
+ Outputs a score (0–100) broken down by dimension.
68
+
69
+ **Example JSON output:**
70
+
71
+ ```json
72
+ {
73
+ "file": "CLAUDE.md",
74
+ "total": 84,
75
+ "dimensions": {
76
+ "completeness": 18,
77
+ "specificity": 17,
78
+ "clarity": 16,
79
+ "agent_awareness": 18,
80
+ "freshness": 15
81
+ }
82
+ }
83
+ ```
84
+
85
+ ### diff — compare context files
86
+
87
+ ```bash
88
+ agentmd diff CLAUDE.md AGENTS.md # side-by-side diff of two context files
89
+ agentmd diff --agent claude # diff current file vs freshly generated output
90
+ agentmd diff --json # output diff as JSON
91
+ ```
92
+
93
+ ## Supported Agents
94
+
95
+ | Agent | Output file |
96
+ |-------|-------------|
97
+ | Claude Code | `CLAUDE.md` |
98
+ | OpenAI Codex | `AGENTS.md` |
99
+ | Cursor | `.cursorrules` |
100
+ | GitHub Copilot | `.github/copilot-instructions.md` |
101
+
102
+ ## Supported Languages
103
+
104
+ | Language | Detection | Generators | What's detected |
105
+ |----------|-----------|------------|-----------------|
106
+ | **Python** | `requirements.txt`, `pyproject.toml`, `setup.py`, `Pipfile` | All agents | Frameworks (Django, Flask, FastAPI, Starlette, Litestar), pytest, ruff/flake8/mypy, GitHub Actions |
107
+ | **Swift/Xcode** | `.xcodeproj`, `Package.swift` | All agents | SwiftUI/UIKit/AppKit targets, SwiftLint, xcodebuild CI, Swift Package Manager |
108
+ | **Rust** | `Cargo.toml` | All agents | tokio, actix-web, serde, axum, clap, and other common crates; clippy/rustfmt, cargo-based CI |
109
+ | **Go** | `go.mod` | All agents | gin, echo, fiber, cobra, and other common modules; golangci-lint, go test CI |
110
+
111
+ ## How Scoring Works
112
+
113
+ Each context file is evaluated on five dimensions (total: 100 points):
114
+
115
+ | Dimension | Points | What it measures |
116
+ |-----------|--------|------------------|
117
+ | **Completeness** | 20 | All key project facts present (languages, stack, test commands) |
118
+ | **Specificity** | 20 | Concrete details vs. generic boilerplate |
119
+ | **Clarity** | 20 | Readable structure, scannable headings, no walls of text |
120
+ | **Agent-awareness** | 20 | Instructions tailored to the target agent's strengths and quirks |
121
+ | **Freshness** | 20 | Content reflects the current state of the codebase (no stale info) |
122
+
123
+ **Note on freshness scoring (v0.2.0):** Earlier versions could false-positive on freshness — penalizing files that referenced current stable versions or recent stable APIs. This has been corrected. The freshness dimension now only flags genuinely stale references (deprecated packages, EOL runtime versions, removed APIs).
124
+
125
+ Run `agentmd score` after generating to see where your files land and what to improve.
126
+
127
+ ## Contributing
128
+
129
+ 1. Fork the repo and create a branch
130
+ 2. `pip install -e ".[dev]"` to get dev dependencies
131
+ 3. Write tests in `tests/unit/`
132
+ 4. `pytest tests/unit -q` must pass
133
+ 5. Open a PR — CI runs on Python 3.10–3.13
134
+
135
+ ## License
136
+
137
+ MIT © 2026 mikiships
@@ -0,0 +1,119 @@
1
+ # agentmd
2
+
3
+ agentmd analyzes your codebase and generates optimized context files for AI coding agents. Point it at any Python, Swift/Xcode, Rust, Go, TypeScript, or multi-language project and it produces ready-to-use `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, or Copilot instruction files — scored and ranked so your agent starts with the best possible picture of your project.
4
+
5
+ ## What's New in 0.2.0
6
+
7
+ - **Multi-language support** — full detection and generation for Python, Swift/Xcode, Rust, and Go projects (see [Supported Languages](#supported-languages))
8
+ - **`--json` flag** — all commands now support structured JSON output for scripting and CI integration
9
+ - **Freshness scoring fixes** — eliminated false positives in freshness scoring that penalized projects using current stable versions
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install agentmd
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### scan — inspect a project
20
+
21
+ ```bash
22
+ agentmd scan # scan current directory
23
+ agentmd scan ~/repos/myapp # scan a specific path
24
+ agentmd scan --json # output as JSON
25
+ ```
26
+
27
+ Prints detected languages, frameworks, package managers, test runners, linters, CI systems, and existing context files.
28
+
29
+ ### generate — create agent context files
30
+
31
+ ```bash
32
+ agentmd generate # generate for all supported agents
33
+ agentmd generate --agent claude # Claude Code (CLAUDE.md)
34
+ agentmd generate --agent codex # OpenAI Codex (AGENTS.md)
35
+ agentmd generate --agent cursor # Cursor (.cursorrules)
36
+ agentmd generate --agent copilot # GitHub Copilot (.github/copilot-instructions.md)
37
+ agentmd generate --out ./docs/ # write to a custom directory
38
+ agentmd generate --json # output generated content as JSON
39
+ ```
40
+
41
+ ### score — evaluate existing context files
42
+
43
+ ```bash
44
+ agentmd score # score all context files in cwd
45
+ agentmd score CLAUDE.md # score a specific file
46
+ agentmd score --json # output scores as JSON
47
+ ```
48
+
49
+ Outputs a score (0–100) broken down by dimension.
50
+
51
+ **Example JSON output:**
52
+
53
+ ```json
54
+ {
55
+ "file": "CLAUDE.md",
56
+ "total": 84,
57
+ "dimensions": {
58
+ "completeness": 18,
59
+ "specificity": 17,
60
+ "clarity": 16,
61
+ "agent_awareness": 18,
62
+ "freshness": 15
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### diff — compare context files
68
+
69
+ ```bash
70
+ agentmd diff CLAUDE.md AGENTS.md # side-by-side diff of two context files
71
+ agentmd diff --agent claude # diff current file vs freshly generated output
72
+ agentmd diff --json # output diff as JSON
73
+ ```
74
+
75
+ ## Supported Agents
76
+
77
+ | Agent | Output file |
78
+ |-------|-------------|
79
+ | Claude Code | `CLAUDE.md` |
80
+ | OpenAI Codex | `AGENTS.md` |
81
+ | Cursor | `.cursorrules` |
82
+ | GitHub Copilot | `.github/copilot-instructions.md` |
83
+
84
+ ## Supported Languages
85
+
86
+ | Language | Detection | Generators | What's detected |
87
+ |----------|-----------|------------|-----------------|
88
+ | **Python** | `requirements.txt`, `pyproject.toml`, `setup.py`, `Pipfile` | All agents | Frameworks (Django, Flask, FastAPI, Starlette, Litestar), pytest, ruff/flake8/mypy, GitHub Actions |
89
+ | **Swift/Xcode** | `.xcodeproj`, `Package.swift` | All agents | SwiftUI/UIKit/AppKit targets, SwiftLint, xcodebuild CI, Swift Package Manager |
90
+ | **Rust** | `Cargo.toml` | All agents | tokio, actix-web, serde, axum, clap, and other common crates; clippy/rustfmt, cargo-based CI |
91
+ | **Go** | `go.mod` | All agents | gin, echo, fiber, cobra, and other common modules; golangci-lint, go test CI |
92
+
93
+ ## How Scoring Works
94
+
95
+ Each context file is evaluated on five dimensions (total: 100 points):
96
+
97
+ | Dimension | Points | What it measures |
98
+ |-----------|--------|------------------|
99
+ | **Completeness** | 20 | All key project facts present (languages, stack, test commands) |
100
+ | **Specificity** | 20 | Concrete details vs. generic boilerplate |
101
+ | **Clarity** | 20 | Readable structure, scannable headings, no walls of text |
102
+ | **Agent-awareness** | 20 | Instructions tailored to the target agent's strengths and quirks |
103
+ | **Freshness** | 20 | Content reflects the current state of the codebase (no stale info) |
104
+
105
+ **Note on freshness scoring (v0.2.0):** Earlier versions could false-positive on freshness — penalizing files that referenced current stable versions or recent stable APIs. This has been corrected. The freshness dimension now only flags genuinely stale references (deprecated packages, EOL runtime versions, removed APIs).
106
+
107
+ Run `agentmd score` after generating to see where your files land and what to improve.
108
+
109
+ ## Contributing
110
+
111
+ 1. Fork the repo and create a branch
112
+ 2. `pip install -e ".[dev]"` to get dev dependencies
113
+ 3. Write tests in `tests/unit/`
114
+ 4. `pytest tests/unit -q` must pass
115
+ 5. Open a PR — CI runs on Python 3.10–3.13
116
+
117
+ ## License
118
+
119
+ MIT © 2026 mikiships
@@ -0,0 +1,4 @@
1
+ """agentmd package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.1.0"
@@ -0,0 +1,303 @@
1
+ """Project analysis engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections import Counter
7
+ from pathlib import Path
8
+
9
+ from agentmd.detectors import (
10
+ detect_ci_systems,
11
+ detect_frameworks,
12
+ detect_go_project,
13
+ detect_languages,
14
+ detect_linters,
15
+ detect_package_managers,
16
+ detect_rust_project,
17
+ detect_swift_project,
18
+ detect_test_runners,
19
+ )
20
+ from agentmd.detectors.common import collect_project_files, read_text, top_ranked
21
+ from agentmd.types import (
22
+ DirectoryStructure,
23
+ ExistingContextFile,
24
+ GitHistorySummary,
25
+ ProjectAnalysis,
26
+ )
27
+
28
+ CONTEXT_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", "copilot-instructions.md"]
29
+
30
+ CONTEXT_MARKERS: dict[str, list[str]] = {
31
+ "CLAUDE.md": ["/init", "/review", "/compact", "claude"],
32
+ "AGENTS.md": ["sandbox", "approval", "codex", "apply_patch"],
33
+ ".cursorrules": ["cursor", "rules", "always"],
34
+ "copilot-instructions.md": ["copilot", "instruction", "github"],
35
+ }
36
+
37
+ SOURCE_DIR_CANDIDATES = {
38
+ "src",
39
+ "app",
40
+ "apps",
41
+ "lib",
42
+ "libs",
43
+ "services",
44
+ "server",
45
+ "client",
46
+ "backend",
47
+ "frontend",
48
+ "pkg",
49
+ "cmd",
50
+ }
51
+
52
+ TEST_DIR_CANDIDATES = {"tests", "test", "spec", "__tests__"}
53
+ MONOREPO_DIR_HINTS = {"packages", "apps", "services"}
54
+ MANIFEST_FILE_NAMES = {
55
+ "pyproject.toml",
56
+ "requirements.txt",
57
+ "setup.py",
58
+ "package.json",
59
+ "Cargo.toml",
60
+ "go.mod",
61
+ "Gemfile",
62
+ "pom.xml",
63
+ "build.gradle",
64
+ "build.gradle.kts",
65
+ }
66
+
67
+
68
+ class ProjectAnalyzer:
69
+ """Analyze codebase structure, tools, and existing context files."""
70
+
71
+ def __init__(self, max_git_commits: int = 100) -> None:
72
+ self.max_git_commits = max_git_commits
73
+
74
+ def analyze(self, path: str | Path = ".") -> ProjectAnalysis:
75
+ """Run full project analysis for a path."""
76
+ root = Path(path).resolve()
77
+ files = collect_project_files(root)
78
+
79
+ language_findings = detect_languages(root, files)
80
+ package_manager_findings = detect_package_managers(root, files)
81
+ framework_findings = detect_frameworks(root, files)
82
+ test_runner_findings = detect_test_runners(root, files)
83
+ linter_findings = detect_linters(root, files)
84
+ ci_findings = detect_ci_systems(root, files)
85
+ swift_findings = detect_swift_project(root, files)
86
+ rust_findings = detect_rust_project(root, files)
87
+ go_findings = detect_go_project(root, files)
88
+
89
+ directory_structure = self._analyze_directory_structure(root, files)
90
+ git_history = self._analyze_git_history(root)
91
+ context_files = self._detect_existing_context_files(root)
92
+
93
+ detection_reasons: dict[str, dict[str, list[str]]] = {
94
+ "languages": language_findings.evidence,
95
+ "package_managers": package_manager_findings.evidence,
96
+ "frameworks": framework_findings.evidence,
97
+ "test_runners": test_runner_findings.evidence,
98
+ "linters": linter_findings.evidence,
99
+ "ci_systems": ci_findings.evidence,
100
+ "swift": swift_findings.evidence,
101
+ "rust": rust_findings.evidence,
102
+ "go": go_findings.evidence,
103
+ "directory_structure": {
104
+ "source_directories": directory_structure.source_directories,
105
+ "test_directories": directory_structure.test_directories,
106
+ "monorepo_indicators": directory_structure.monorepo_indicators,
107
+ },
108
+ "existing_context_files": {
109
+ item.name: [item.path] for item in context_files if item.present
110
+ },
111
+ }
112
+
113
+ return ProjectAnalysis(
114
+ root_path=str(root),
115
+ languages=language_findings.values,
116
+ package_managers=package_manager_findings.values,
117
+ frameworks=framework_findings.values,
118
+ test_runners=test_runner_findings.values,
119
+ linters=linter_findings.values,
120
+ ci_systems=ci_findings.values,
121
+ swift_components=swift_findings.values,
122
+ rust_components=rust_findings.values,
123
+ go_components=go_findings.values,
124
+ directory_structure=directory_structure,
125
+ git_history=git_history,
126
+ existing_context_files=context_files,
127
+ detection_reasons=detection_reasons,
128
+ )
129
+
130
+ def _analyze_directory_structure(self, root: Path, files: list[Path]) -> DirectoryStructure:
131
+ top_level_dirs = sorted({path.parts[0] for path in files if len(path.parts) > 1})
132
+ top_level_files = sorted({path.name for path in files if len(path.parts) == 1})
133
+
134
+ source_directories = sorted(
135
+ {directory for directory in top_level_dirs if directory.lower() in SOURCE_DIR_CANDIDATES}
136
+ )
137
+ test_directories = sorted(
138
+ {directory for directory in top_level_dirs if directory.lower() in TEST_DIR_CANDIDATES}
139
+ )
140
+
141
+ monorepo_indicators = self._monorepo_indicators(root, files)
142
+ return DirectoryStructure(
143
+ top_level_directories=top_level_dirs,
144
+ top_level_files=top_level_files,
145
+ source_directories=source_directories,
146
+ test_directories=test_directories,
147
+ is_monorepo=bool(monorepo_indicators),
148
+ monorepo_indicators=monorepo_indicators,
149
+ )
150
+
151
+ def _monorepo_indicators(self, root: Path, files: list[Path]) -> list[str]:
152
+ indicators: list[str] = []
153
+ file_names = {path.name for path in files}
154
+
155
+ if "pnpm-workspace.yaml" in file_names:
156
+ indicators.append("pnpm workspace file present")
157
+
158
+ package_json = root / "package.json"
159
+ if package_json.exists():
160
+ package_json_content = read_text(package_json, max_chars=25000)
161
+ if "\"workspaces\"" in package_json_content:
162
+ indicators.append("package.json defines workspaces")
163
+
164
+ manifest_dirs = {
165
+ path.parts[0]
166
+ for path in files
167
+ if path.name in MANIFEST_FILE_NAMES and len(path.parts) > 1
168
+ }
169
+ if len(manifest_dirs) > 1:
170
+ indicators.append("multiple top-level directories contain package manifests")
171
+
172
+ for hinted_dir in MONOREPO_DIR_HINTS:
173
+ candidate = root / hinted_dir
174
+ if not candidate.exists() or not candidate.is_dir():
175
+ continue
176
+ child_count = 0
177
+ for child in candidate.iterdir():
178
+ if not child.is_dir():
179
+ continue
180
+ if any((child / manifest).exists() for manifest in MANIFEST_FILE_NAMES):
181
+ child_count += 1
182
+ if child_count >= 2:
183
+ indicators.append(f"{hinted_dir}/ has {child_count} package subprojects")
184
+
185
+ return sorted(set(indicators))
186
+
187
+ def _analyze_git_history(self, root: Path) -> GitHistorySummary:
188
+ if not (root / ".git").exists():
189
+ return GitHistorySummary()
190
+
191
+ inside_git = subprocess.run(
192
+ ["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
193
+ check=False,
194
+ capture_output=True,
195
+ text=True,
196
+ )
197
+ if inside_git.returncode != 0 or inside_git.stdout.strip().lower() != "true":
198
+ return GitHistorySummary()
199
+
200
+ log_result = subprocess.run(
201
+ [
202
+ "git",
203
+ "-C",
204
+ str(root),
205
+ "log",
206
+ f"-n{self.max_git_commits}",
207
+ "--pretty=format:__COMMIT__%s",
208
+ "--name-only",
209
+ ],
210
+ check=False,
211
+ capture_output=True,
212
+ text=True,
213
+ )
214
+ if log_result.returncode != 0:
215
+ return GitHistorySummary()
216
+
217
+ extension_counter: Counter[str] = Counter()
218
+ directory_counter: Counter[str] = Counter()
219
+ message_prefix_counter: Counter[str] = Counter()
220
+ commit_count = 0
221
+
222
+ for line in log_result.stdout.splitlines():
223
+ if not line:
224
+ continue
225
+ if line.startswith("__COMMIT__"):
226
+ commit_count += 1
227
+ message = line.replace("__COMMIT__", "", 1).strip()
228
+ if not message:
229
+ continue
230
+ if ":" in message:
231
+ prefix = message.split(":", 1)[0].strip().lower()
232
+ else:
233
+ prefix = message.split()[0].strip().lower()
234
+ if prefix:
235
+ message_prefix_counter[prefix] += 1
236
+ continue
237
+
238
+ changed_path = Path(line.strip())
239
+ if changed_path.suffix:
240
+ extension_counter[changed_path.suffix.lower()] += 1
241
+ else:
242
+ extension_counter["[no_ext]"] += 1
243
+ if changed_path.parts:
244
+ directory_counter[changed_path.parts[0]] += 1
245
+
246
+ return GitHistorySummary(
247
+ commit_count=commit_count,
248
+ common_file_extensions=top_ranked(extension_counter),
249
+ common_directories=top_ranked(directory_counter),
250
+ common_message_prefixes=top_ranked(message_prefix_counter),
251
+ )
252
+
253
+ def _detect_existing_context_files(self, root: Path) -> list[ExistingContextFile]:
254
+ records: list[ExistingContextFile] = []
255
+ for file_name in CONTEXT_FILE_NAMES:
256
+ full_path = root / file_name
257
+ relative_path = file_name
258
+ if not full_path.exists():
259
+ records.append(
260
+ ExistingContextFile(name=file_name, path=relative_path, present=False)
261
+ )
262
+ continue
263
+
264
+ content = read_text(full_path, max_chars=50000)
265
+ lines = content.splitlines()
266
+ first_heading = self._first_heading(lines)
267
+ markers = self._detect_markers(file_name, content)
268
+ records.append(
269
+ ExistingContextFile(
270
+ name=file_name,
271
+ path=relative_path,
272
+ present=True,
273
+ line_count=len(lines),
274
+ first_heading=first_heading,
275
+ agent_markers=markers,
276
+ )
277
+ )
278
+ return records
279
+
280
+ @staticmethod
281
+ def _first_heading(lines: list[str]) -> str | None:
282
+ for line in lines:
283
+ stripped = line.strip()
284
+ if not stripped:
285
+ continue
286
+ if stripped.startswith("#"):
287
+ return stripped
288
+ return stripped[:120]
289
+ return None
290
+
291
+ @staticmethod
292
+ def _detect_markers(file_name: str, content: str) -> list[str]:
293
+ lowered = content.lower()
294
+ markers = []
295
+ for marker in CONTEXT_MARKERS.get(file_name, []):
296
+ if marker.lower() in lowered:
297
+ markers.append(marker)
298
+ return sorted(set(markers))
299
+
300
+
301
+ def analyze_project(path: str | Path = ".") -> ProjectAnalysis:
302
+ """Convenience wrapper for one-shot analysis."""
303
+ return ProjectAnalyzer().analyze(path)