codeforerunner 0.3.0__py3-none-any.whl

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.
@@ -0,0 +1,177 @@
1
+ """Minimal stdio MCP server exposing prompt bundles as tools. See SPEC.md §D.mcp.
2
+
3
+ Hand-rolled JSON-RPC 2.0 over line-delimited stdio. Stdlib only.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any, Iterable
12
+
13
+ PROTOCOL_VERSION = "2024-11-05"
14
+ SERVER_NAME = "codeforerunner"
15
+ SERVER_VERSION = "0.2.0"
16
+
17
+
18
+ def _repo_root(start: Path | None = None) -> Path:
19
+ here = (start or Path.cwd()).resolve()
20
+ for candidate in [here, *here.parents]:
21
+ if (candidate / "prompts" / "tasks").is_dir():
22
+ return candidate
23
+ raise FileNotFoundError(
24
+ "could not locate codeforerunner repo root (no prompts/tasks/ found upward)"
25
+ )
26
+
27
+
28
+ def _read(path: Path) -> str:
29
+ return path.read_text(encoding="utf-8")
30
+
31
+
32
+ def resolve_bundle(repo: Path, task: str) -> str:
33
+ """Concatenate system/base.md + sorted partials/*.md + tasks/<task>.md with marker comments."""
34
+ task_path = repo / "prompts" / "tasks" / f"{task}.md"
35
+ if not task_path.is_file():
36
+ raise FileNotFoundError(f"unknown task '{task}' (no {task_path})")
37
+
38
+ parts: list[str] = []
39
+ base = repo / "prompts" / "system" / "base.md"
40
+ if base.is_file():
41
+ parts.append(f"<!-- system: base.md -->\n{_read(base).rstrip()}")
42
+
43
+ partials_dir = repo / "prompts" / "partials"
44
+ if partials_dir.is_dir():
45
+ for p in sorted(partials_dir.glob("*.md")):
46
+ parts.append(f"<!-- partial: {p.name} -->\n{_read(p).rstrip()}")
47
+
48
+ parts.append(f"<!-- task: {task_path.name} -->\n{_read(task_path).rstrip()}")
49
+ return "\n\n".join(parts) + "\n"
50
+
51
+
52
+ def _list_tasks(repo: Path) -> list[Path]:
53
+ tasks_dir = repo / "prompts" / "tasks"
54
+ if not tasks_dir.is_dir():
55
+ return []
56
+ return sorted(tasks_dir.glob("*.md"))
57
+
58
+
59
+ def _description_for(task_path: Path) -> str:
60
+ """First non-empty markdown line, stripped of leading '#' chars and whitespace."""
61
+ for raw in task_path.read_text(encoding="utf-8").splitlines():
62
+ line = raw.strip()
63
+ if not line:
64
+ continue
65
+ return line.lstrip("#").strip()
66
+ return task_path.stem
67
+
68
+
69
+ def _tools(repo: Path) -> list[dict[str, Any]]:
70
+ return [
71
+ {
72
+ "name": p.stem,
73
+ "description": _description_for(p),
74
+ "inputSchema": {"type": "object", "properties": {}, "required": []},
75
+ }
76
+ for p in _list_tasks(repo)
77
+ ]
78
+
79
+
80
+ def _ok(req_id: Any, result: Any) -> dict[str, Any]:
81
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
82
+
83
+
84
+ def _err(req_id: Any, code: int, message: str) -> dict[str, Any]:
85
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
86
+
87
+
88
+ SCAN_EXEMPT_TOOLS = frozenset({"init-agent-onboarding", "scan"})
89
+
90
+
91
+ def _handle(repo: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
92
+ method = msg.get("method")
93
+ req_id = msg.get("id")
94
+ params = msg.get("params") or {}
95
+
96
+ # Notifications: no id, no response.
97
+ if method == "notifications/initialized":
98
+ return None
99
+ if req_id is None and isinstance(method, str) and method.startswith("notifications/"):
100
+ return None
101
+
102
+ if method == "initialize":
103
+ return _ok(
104
+ req_id,
105
+ {
106
+ "protocolVersion": PROTOCOL_VERSION,
107
+ "capabilities": {"tools": {}},
108
+ "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
109
+ },
110
+ )
111
+
112
+ if method == "tools/list":
113
+ return _ok(req_id, {"tools": _tools(repo)})
114
+
115
+ if method == "tools/call":
116
+ name = params.get("name")
117
+ task_path = repo / "prompts" / "tasks" / f"{name}.md"
118
+ if not isinstance(name, str) or not task_path.is_file():
119
+ return _err(req_id, -32602, f"unknown tool: {name!r}")
120
+ if name not in SCAN_EXEMPT_TOOLS and not state.get("scan_called"):
121
+ return _err(
122
+ req_id,
123
+ -32000,
124
+ "scan-first required: call tools/call name=scan before this task (SPEC V2)",
125
+ )
126
+ if name == "scan":
127
+ state["scan_called"] = True
128
+ try:
129
+ text = resolve_bundle(repo, name)
130
+ except Exception as e: # pragma: no cover - defensive
131
+ return _err(req_id, -32603, f"internal error: {e}")
132
+ return _ok(
133
+ req_id,
134
+ {"content": [{"type": "text", "text": text}], "isError": False},
135
+ )
136
+
137
+ return _err(req_id, -32601, f"method not found: {method!r}")
138
+
139
+
140
+ def serve(repo: Path, stdin: Iterable[str] = sys.stdin, stdout=sys.stdout, stderr=sys.stderr) -> int:
141
+ state: dict[str, Any] = {"scan_called": False}
142
+ for raw in stdin:
143
+ line = raw.strip()
144
+ if not line:
145
+ continue
146
+ try:
147
+ msg = json.loads(line)
148
+ except json.JSONDecodeError as e:
149
+ print(f"mcp_server: invalid JSON: {e}", file=stderr)
150
+ resp = {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "parse error"}}
151
+ stdout.write(json.dumps(resp) + "\n")
152
+ stdout.flush()
153
+ continue
154
+
155
+ try:
156
+ resp = _handle(repo, msg, state)
157
+ except Exception as e: # pragma: no cover - defensive
158
+ print(f"mcp_server: handler error: {e}", file=stderr)
159
+ resp = _err(msg.get("id"), -32603, f"internal error: {e}")
160
+
161
+ if resp is not None:
162
+ stdout.write(json.dumps(resp) + "\n")
163
+ stdout.flush()
164
+ return 0
165
+
166
+
167
+ def main(argv: list[str] | None = None) -> int:
168
+ try:
169
+ repo = _repo_root()
170
+ except FileNotFoundError as e:
171
+ print(f"mcp_server: {e}", file=sys.stderr)
172
+ return 2
173
+ return serve(repo)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ raise SystemExit(main())
@@ -0,0 +1,36 @@
1
+ """Provider registry. See SPEC.md §T38."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codeforerunner.providers.anthropic import AnthropicProvider
6
+ from codeforerunner.providers.base import CompletionResult, Provider, ProviderError
7
+ from codeforerunner.providers.google import GoogleProvider
8
+ from codeforerunner.providers.ollama import OllamaProvider
9
+ from codeforerunner.providers.openai import OpenAIProvider
10
+
11
+ __all__ = [
12
+ "AnthropicProvider",
13
+ "CompletionResult",
14
+ "GoogleProvider",
15
+ "OllamaProvider",
16
+ "OpenAIProvider",
17
+ "Provider",
18
+ "ProviderError",
19
+ "REGISTRY",
20
+ "get",
21
+ ]
22
+
23
+ REGISTRY: dict[str, type] = {
24
+ "anthropic": AnthropicProvider,
25
+ "openai": OpenAIProvider,
26
+ "google": GoogleProvider,
27
+ "ollama": OllamaProvider,
28
+ }
29
+
30
+
31
+ def get(name: str) -> type:
32
+ if name not in REGISTRY:
33
+ raise ProviderError(
34
+ f"unknown provider '{name}' (expected one of {sorted(REGISTRY)})"
35
+ )
36
+ return REGISTRY[name]
@@ -0,0 +1,61 @@
1
+ """Anthropic Messages API provider. Stdlib HTTP only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+
9
+ from codeforerunner.providers.base import CompletionResult, ProviderError
10
+
11
+
12
+ class AnthropicProvider:
13
+ name = "anthropic"
14
+ default_env_var = "ANTHROPIC_API_KEY"
15
+ default_model = "claude-opus-4-5"
16
+
17
+ endpoint = "https://api.anthropic.com/v1/messages"
18
+
19
+ def complete(
20
+ self,
21
+ *,
22
+ prompt: str,
23
+ model: str | None = None,
24
+ api_key: str | None = None,
25
+ ) -> CompletionResult:
26
+ if not api_key:
27
+ raise ProviderError(f"missing API key (set ${self.default_env_var})")
28
+ model = model or self.default_model
29
+ body = json.dumps(
30
+ {
31
+ "model": model,
32
+ "max_tokens": 4096,
33
+ "messages": [{"role": "user", "content": prompt}],
34
+ }
35
+ ).encode("utf-8")
36
+ req = urllib.request.Request(
37
+ self.endpoint,
38
+ data=body,
39
+ method="POST",
40
+ headers={
41
+ "x-api-key": api_key,
42
+ "anthropic-version": "2023-06-01",
43
+ "content-type": "application/json",
44
+ },
45
+ )
46
+ try:
47
+ with urllib.request.urlopen(req) as resp:
48
+ raw = resp.read()
49
+ except urllib.error.HTTPError as e:
50
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
51
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
52
+ except urllib.error.URLError as e:
53
+ raise ProviderError(f"network error: {e.reason}") from e
54
+ try:
55
+ data = json.loads(raw.decode("utf-8"))
56
+ text = data["content"][0]["text"]
57
+ except (json.JSONDecodeError, KeyError, IndexError, TypeError) as e:
58
+ raise ProviderError(f"malformed response: {e}") from e
59
+ return CompletionResult(
60
+ text=text, model=data.get("model", model), usage=data.get("usage")
61
+ )
@@ -0,0 +1,31 @@
1
+ """Provider protocol + shared types. See SPEC.md §T38."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Protocol
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CompletionResult:
11
+ text: str
12
+ model: str
13
+ usage: dict | None = None # provider-reported token counts; None if unknown
14
+
15
+
16
+ class Provider(Protocol):
17
+ name: str
18
+ default_env_var: str # e.g. "ANTHROPIC_API_KEY"
19
+ default_model: str # provider's recommended default
20
+
21
+ def complete(
22
+ self,
23
+ *,
24
+ prompt: str,
25
+ model: str | None = None,
26
+ api_key: str | None = None,
27
+ ) -> CompletionResult: ...
28
+
29
+
30
+ class ProviderError(Exception):
31
+ """Raised on provider HTTP failures, missing keys, or malformed responses."""
@@ -0,0 +1,62 @@
1
+ """Google Gemini generateContent provider. Stdlib HTTP only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.parse
8
+ import urllib.request
9
+
10
+ from codeforerunner.providers.base import CompletionResult, ProviderError
11
+
12
+
13
+ class GoogleProvider:
14
+ name = "google"
15
+ default_env_var = "GOOGLE_API_KEY"
16
+ default_model = "gemini-2.5-pro"
17
+
18
+ endpoint_template = (
19
+ "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
20
+ )
21
+
22
+ def complete(
23
+ self,
24
+ *,
25
+ prompt: str,
26
+ model: str | None = None,
27
+ api_key: str | None = None,
28
+ ) -> CompletionResult:
29
+ if not api_key:
30
+ raise ProviderError(f"missing API key (set ${self.default_env_var})")
31
+ model = model or self.default_model
32
+ url = self.endpoint_template.format(
33
+ model=urllib.parse.quote(model, safe=""),
34
+ key=urllib.parse.quote(api_key, safe=""),
35
+ )
36
+ body = json.dumps(
37
+ {"contents": [{"parts": [{"text": prompt}]}]}
38
+ ).encode("utf-8")
39
+ req = urllib.request.Request(
40
+ url,
41
+ data=body,
42
+ method="POST",
43
+ headers={"content-type": "application/json"},
44
+ )
45
+ try:
46
+ with urllib.request.urlopen(req) as resp:
47
+ raw = resp.read()
48
+ except urllib.error.HTTPError as e:
49
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
50
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
51
+ except urllib.error.URLError as e:
52
+ raise ProviderError(f"network error: {e.reason}") from e
53
+ try:
54
+ data = json.loads(raw.decode("utf-8"))
55
+ text = data["candidates"][0]["content"]["parts"][0]["text"]
56
+ except (json.JSONDecodeError, KeyError, IndexError, TypeError) as e:
57
+ raise ProviderError(f"malformed response: {e}") from e
58
+ return CompletionResult(
59
+ text=text,
60
+ model=data.get("modelVersion", model),
61
+ usage=data.get("usageMetadata"),
62
+ )
@@ -0,0 +1,56 @@
1
+ """Ollama local provider. Stdlib HTTP only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import urllib.error
8
+ import urllib.request
9
+
10
+ from codeforerunner.providers.base import CompletionResult, ProviderError
11
+
12
+ DEFAULT_HOST = "http://localhost:11434"
13
+
14
+
15
+ class OllamaProvider:
16
+ name = "ollama"
17
+ default_env_var = "OLLAMA_HOST"
18
+ default_model = "llama3"
19
+
20
+ def complete(
21
+ self,
22
+ *,
23
+ prompt: str,
24
+ model: str | None = None,
25
+ api_key: str | None = None,
26
+ ) -> CompletionResult:
27
+ # api_key is interpreted as a base URL override; fall back to env then default.
28
+ base = api_key or os.environ.get(self.default_env_var) or DEFAULT_HOST
29
+ base = base.rstrip("/")
30
+ model = model or self.default_model
31
+ url = f"{base}/api/generate"
32
+ body = json.dumps(
33
+ {"model": model, "prompt": prompt, "stream": False}
34
+ ).encode("utf-8")
35
+ req = urllib.request.Request(
36
+ url,
37
+ data=body,
38
+ method="POST",
39
+ headers={"content-type": "application/json"},
40
+ )
41
+ try:
42
+ with urllib.request.urlopen(req) as resp:
43
+ raw = resp.read()
44
+ except urllib.error.HTTPError as e:
45
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
46
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
47
+ except urllib.error.URLError as e:
48
+ raise ProviderError(f"network error: {e.reason}") from e
49
+ try:
50
+ data = json.loads(raw.decode("utf-8"))
51
+ text = data["response"]
52
+ except (json.JSONDecodeError, KeyError, TypeError) as e:
53
+ raise ProviderError(f"malformed response: {e}") from e
54
+ usage_keys = ("prompt_eval_count", "eval_count", "total_duration")
55
+ usage = {k: data[k] for k in usage_keys if k in data} or None
56
+ return CompletionResult(text=text, model=data.get("model", model), usage=usage)
@@ -0,0 +1,59 @@
1
+ """OpenAI chat completions provider. Stdlib HTTP only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+
9
+ from codeforerunner.providers.base import CompletionResult, ProviderError
10
+
11
+
12
+ class OpenAIProvider:
13
+ name = "openai"
14
+ default_env_var = "OPENAI_API_KEY"
15
+ default_model = "gpt-4o"
16
+
17
+ endpoint = "https://api.openai.com/v1/chat/completions"
18
+
19
+ def complete(
20
+ self,
21
+ *,
22
+ prompt: str,
23
+ model: str | None = None,
24
+ api_key: str | None = None,
25
+ ) -> CompletionResult:
26
+ if not api_key:
27
+ raise ProviderError(f"missing API key (set ${self.default_env_var})")
28
+ model = model or self.default_model
29
+ body = json.dumps(
30
+ {
31
+ "model": model,
32
+ "messages": [{"role": "user", "content": prompt}],
33
+ }
34
+ ).encode("utf-8")
35
+ req = urllib.request.Request(
36
+ self.endpoint,
37
+ data=body,
38
+ method="POST",
39
+ headers={
40
+ "Authorization": f"Bearer {api_key}",
41
+ "content-type": "application/json",
42
+ },
43
+ )
44
+ try:
45
+ with urllib.request.urlopen(req) as resp:
46
+ raw = resp.read()
47
+ except urllib.error.HTTPError as e:
48
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
49
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
50
+ except urllib.error.URLError as e:
51
+ raise ProviderError(f"network error: {e.reason}") from e
52
+ try:
53
+ data = json.loads(raw.decode("utf-8"))
54
+ text = data["choices"][0]["message"]["content"]
55
+ except (json.JSONDecodeError, KeyError, IndexError, TypeError) as e:
56
+ raise ProviderError(f"malformed response: {e}") from e
57
+ return CompletionResult(
58
+ text=text, model=data.get("model", model), usage=data.get("usage")
59
+ )
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: codeforerunner
3
+ Version: 0.3.0
4
+ Summary: Model-agnostic repository documentation tooling (prompt-first; thin CLI).
5
+ Author: Derek Palmer
6
+ License-Expression: LicenseRef-Codeforerunner-SAL-0.1
7
+ Project-URL: Repository, https://github.com/derek-palmer/codeforerunner
8
+ Project-URL: Issues, https://github.com/derek-palmer/codeforerunner/issues
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.md
12
+ Requires-Dist: PyYAML>=6.0
13
+ Dynamic: license-file
14
+
15
+ ![codeForerunner — your codebase gets a Forerunner; your docs finally see the light](images/readme_banner.png)
16
+
17
+ # codeForerunner
18
+
19
+ CodeForerunner is a model-agnostic documentation agent that acts as overwatch for your repository, automatically analyzing code and maintaining docs, diagrams, and architecture knowledge as your codebase evolves over time.
20
+
21
+ The current repo is the prompt-first foundation for that agent: it ships prompt assets for understanding a codebase and generating developer docs. A thin Python CLI (including `forerunner mcp-server` and a scoped `forerunner init --full / --agents-only`), an idempotent skill installer, pre-commit + CI hooks, and a PyPI publish workflow now wrap those prompts; the first published PyPI release remains pending.
22
+
23
+ ## Current State
24
+
25
+ - Core product: Markdown prompts in `prompts/`.
26
+ - Agent package artifacts: Codex plugin files under `plugins/codeforerunner/` and Claude Code plugin files under `.claude-plugin/` plus `skills/codeforerunner/`.
27
+ - Python package: `pyproject.toml` + `src/codeforerunner/` expose a `forerunner` console script. `forerunner doc <task>` resolves the prompt bundle (base + partials + task) to stdout; `forerunner install <agent>` idempotently writes the canonical skill into agent-specific directories; `forerunner init` resolves the agent-onboarding bundle (with `--full` to prepend a scan or `--agents-only` for the default scope); `forerunner scan` resolves the scan bundle; `forerunner mcp-server` serves prompt bundles as MCP tools over stdio.
28
+ - Hooks: `.pre-commit-hooks.yaml` exposes a `forerunner-check` hook; `.github/workflows/forerunner-check.yml` mirrors it in CI. Both no-op when `forerunner.config.yaml` is absent.
29
+ - Current config: `forerunner.config.yaml.example` documents the schema now parsed by `src/codeforerunner/config.py`; see "Configuration" below.
30
+ - Not currently present: Docker image, Makefile, published PyPI release.
31
+
32
+ ## Install
33
+
34
+ After the first PyPI release:
35
+
36
+ ```bash
37
+ pipx install codeforerunner # recommended; isolated environment
38
+ pip install codeforerunner # alternative
39
+ ```
40
+
41
+ From source:
42
+
43
+ ```bash
44
+ git clone https://github.com/derek-palmer/codeForerunner
45
+ cd codeForerunner
46
+ python -m pip install -e .
47
+ ```
48
+
49
+ Then `forerunner --help` should print the subcommand list.
50
+
51
+ ## Prompt Layout
52
+
53
+ ```text
54
+ prompts/
55
+ ├── system/
56
+ │ └── base.md
57
+ ├── partials/
58
+ │ ├── context-format.md
59
+ │ ├── output-rules.md
60
+ │ └── stack-hints.md
61
+ └── tasks/
62
+ ├── scan.md
63
+ ├── init-agent-onboarding.md
64
+ ├── readme.md
65
+ ├── api-docs.md
66
+ ├── stack-docs.md
67
+ ├── diagrams.md
68
+ ├── flows.md
69
+ ├── version-audit.md
70
+ ├── check.md
71
+ └── review.md
72
+ ```
73
+
74
+ ## Quick Start
75
+
76
+ 1. Open `prompts/system/base.md` and use it as the agent system or project instruction.
77
+ 2. Assemble repo context using the shape in `prompts/partials/context-format.md`.
78
+ 3. For documentation generation, run `prompts/tasks/scan.md` first.
79
+ 4. For agent onboarding only, run `prompts/tasks/init-agent-onboarding.md` directly.
80
+ 5. Pass the scan result into one downstream documentation prompt, such as `prompts/tasks/readme.md` or `prompts/tasks/stack-docs.md`.
81
+ 6. Apply generated docs only after checking that every claim is grounded in provided files.
82
+
83
+ ## What The Prompts Do
84
+
85
+ | Prompt | Purpose |
86
+ | --- | --- |
87
+ | `prompts/system/base.md` | Defines the codeforerunner role, quality bar, Markdown rules, and accuracy constraints. |
88
+ | `prompts/tasks/scan.md` | Produces the first structured repo scan used by downstream tasks. |
89
+ | `prompts/tasks/init-agent-onboarding.md` | Generates or updates `AGENTS.md` from repo evidence plus files such as `CLAUDE.md`, `.cursor/rules/*`, `.cursorrules`, `.github/copilot-instructions.md`, and `opencode.json`. |
90
+ | `prompts/tasks/readme.md` | Generates or rewrites a top-level README from scan output and selected files. |
91
+ | `prompts/tasks/api-docs.md` | Documents public APIs when endpoints/interfaces are evident. |
92
+ | `prompts/tasks/stack-docs.md` | Documents stack-specific areas of a repo. |
93
+ | `prompts/tasks/diagrams.md` | Generates Mermaid architecture or flow diagrams. |
94
+ | `prompts/tasks/flows.md` | Documents user, request, job, or data flows. |
95
+ | `prompts/tasks/version-audit.md` | Audits pinned versions from manifests, lockfiles, Dockerfiles, workflows, or IaC. |
96
+ | `prompts/tasks/check.md` | Checks existing docs for staleness against a fresh scan. |
97
+ | `prompts/tasks/review.md` | Summarizes documentation impact for review. |
98
+
99
+ ## Docs And Spec
100
+
101
+ - `SPEC.md` tracks phases, invariants, and tasks so future PRs can make small status updates instead of broad rewrites.
102
+ - `docs/getting-started.md` explains manual prompt use.
103
+ - `docs/prompt-guide.md` explains how system, partial, and task prompts compose.
104
+ - `docs/editor-agent-setup.md` explains how to adapt prompts to local agents.
105
+ - `docs/roadmap.md` mirrors the `SPEC.md` phase status in human-readable form.
106
+ - `docs/agent-distribution-design.md` records the design that backs the Codex/Claude packages and `forerunner install`.
107
+
108
+ ## Configuration
109
+
110
+ `forerunner.config.yaml.example` documents the loaded schema. Copy it to `forerunner.config.yaml` to opt in; without that file, `forerunner check` is a silent no-op. The schema has top-level provider/model fields (`provider`, `model`, `api_key_env`, `output_dir`, `context_max_files`, `context_max_lines_per_file`, `approaching_eol_threshold_months`), `ignore_patterns`, `tasks.version_audit`, and `tasks.check`. `forerunner check` honors `tasks.check.enabled_rules` (allowlist of rule IDs, default all) and `tasks.check.ignore_paths` (fnmatch globs applied to scanned docs). Invalid YAML, unknown providers, unknown `api_key_env` providers, or unknown severities surface as a `ConfigError` and exit non-zero.
111
+
112
+ ### MCP Server
113
+
114
+ `forerunner mcp-server` speaks JSON-RPC 2.0 over stdio and exposes one tool per `prompts/tasks/*.md` (tool name = filename stem). Each `tools/call` returns the resolved `base + partials + task` bundle as text. A scan-first gate enforces SPEC V2: any tool other than `scan` or `init-agent-onboarding` returns an error until `scan` has been called in the same session. Point any MCP-compatible client at `forerunner mcp-server` as a stdio server (running from the target repo so `prompts/tasks/` resolves).
115
+
116
+ See `examples/mcp/` for Claude Desktop and mcp-cli wiring examples.
117
+
118
+ ## Roadmap
119
+
120
+ See `SPEC.md` for the canonical phase/task tracker and `docs/roadmap.md` for the human-readable roadmap.
@@ -0,0 +1,19 @@
1
+ codeforerunner/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
2
+ codeforerunner/check.py,sha256=kSrVoTVb9ALEBHvUerR2nZfroXco_yz6gMgXzdz45Ac,4905
3
+ codeforerunner/cli.py,sha256=anRbSooRqb5cS4EtgGJHVIOTK1ZgdACWb4WcD5Y0hqM,8478
4
+ codeforerunner/config.py,sha256=REs4FgmSSn7R2tLIwLJPL8VmSCGkTvw8J2SqBOggzho,6206
5
+ codeforerunner/doctor.py,sha256=AFHY8YbqTp726JcxLAYvrdeOLwr6WLPiNUUF2QYOwL8,10076
6
+ codeforerunner/installer.py,sha256=WJkWbZjP7x_CWQDs3cfx8zBr97riMgzmGFRNtruPlgI,10301
7
+ codeforerunner/mcp_server.py,sha256=d8eiBPGTLmf_k78tbH1hCfdyokXQajAgcM40loVXNHI,5737
8
+ codeforerunner/providers/__init__.py,sha256=ttMAbHWJIO8s-8H6Kb_EWf3LN5oMzlmX1D12RyGSmIg,962
9
+ codeforerunner/providers/anthropic.py,sha256=zaJDyzH1vPr7nSqUOv6avlTk3covpkWXLHae2-bS9io,2021
10
+ codeforerunner/providers/base.py,sha256=Jk9vBeRNxH9naUC6stN5jY1KHmhrqg2k2kMGOhbQTYk,752
11
+ codeforerunner/providers/google.py,sha256=wBpbte8hdX_Jnr_JBHvQarogT6T1hWR4aGF5O2exBCU,2109
12
+ codeforerunner/providers/ollama.py,sha256=bKhigdjHs3aw73iB5uQka9_n-XCjv1sh_zZfe1TrWTQ,1975
13
+ codeforerunner/providers/openai.py,sha256=OHWeZ11OuHPgQBMwqJnJWxNHyGIP5KL7B06w2ttgxlY,1952
14
+ codeforerunner-0.3.0.dist-info/licenses/LICENSE.md,sha256=iIhmJHib6GbdjcwiDMM-npiNRf3XgASom1WsOJivEdc,2915
15
+ codeforerunner-0.3.0.dist-info/METADATA,sha256=MsbTrzGM5GeGz_JtKhSONLHtwxbGwIz7A_EyK3vSd_c,7047
16
+ codeforerunner-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ codeforerunner-0.3.0.dist-info/entry_points.txt,sha256=3p8BbPlq-wfcXk42tsweKePRaGlZ1WXho1gOkuZGyIQ,55
18
+ codeforerunner-0.3.0.dist-info/top_level.txt,sha256=pV1rt0-NIpNEotKXpL_sF2060DHr-_0F86LWhUlvXis,15
19
+ codeforerunner-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ forerunner = codeforerunner.cli:main