codeforerunner 0.3.1__py3-none-any.whl → 0.4.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.
codeforerunner/check.py CHANGED
@@ -1,9 +1,9 @@
1
- """Drift detection for docs that claim files don't exist when they do."""
1
+ """Drift detection for docs vs repo state."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import fnmatch
5
5
  import re
6
- from dataclasses import dataclass
6
+ from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
8
 
9
9
  from codeforerunner.config import CheckConfig
@@ -17,8 +17,18 @@ class Violation:
17
17
  message: str
18
18
 
19
19
 
20
- _RULES = [
21
- (
20
+ @dataclass(frozen=True)
21
+ class _Rule:
22
+ id: str
23
+ pattern: re.Pattern
24
+ triggers: tuple[str, ...]
25
+ message: str
26
+ invert: bool = False # True = fire when triggers ABSENT (doc claims feature exists but file gone)
27
+
28
+
29
+ _RULES: list[_Rule] = [
30
+ # Normal rules: fire when trigger EXISTS and phrase matches (doc denies a thing that's present)
31
+ _Rule(
22
32
  "R1-no-cli",
23
33
  re.compile(
24
34
  r"(?i)no\s+CLI\s+exists"
@@ -29,56 +39,87 @@ _RULES = [
29
39
  ("src/codeforerunner/cli.py",),
30
40
  "doc claims no CLI exists, but src/codeforerunner/cli.py is present",
31
41
  ),
32
- (
42
+ _Rule(
33
43
  "R2-no-pre-commit",
34
44
  re.compile(r"(?i)no\s+pre[- ]commit(\s+hook)?"),
35
45
  (".pre-commit-hooks.yaml",),
36
46
  "doc claims no pre-commit hook, but .pre-commit-hooks.yaml is present",
37
47
  ),
38
- (
48
+ _Rule(
39
49
  "R3-no-ci",
40
50
  re.compile(r"(?i)no\s+CI(\s+workflow)?"),
41
51
  (".github/workflows/*.yml",),
42
52
  "doc claims no CI workflow, but .github/workflows/*.yml is present",
43
53
  ),
44
- (
54
+ _Rule(
45
55
  "R4-no-installer",
46
56
  re.compile(r"(?i)no\s+installer"),
47
57
  ("src/codeforerunner/installer.py",),
48
58
  "doc claims no installer, but src/codeforerunner/installer.py is present",
49
59
  ),
50
- (
60
+ _Rule(
51
61
  "R5-no-python-package",
52
62
  re.compile(r"(?i)no\s+Python\s+package"),
53
63
  ("pyproject.toml",),
54
64
  "doc claims no Python package, but pyproject.toml is present",
55
65
  ),
56
- (
66
+ _Rule(
57
67
  "R6-no-docker",
58
68
  re.compile(r"(?i)no\s+Docker(\s+image)?|no\s+Dockerfile"),
59
69
  ("Dockerfile", "compose.yml", "docker-compose.yml"),
60
70
  "doc claims no Docker, but Dockerfile/compose file is present",
61
71
  ),
62
- (
72
+ _Rule(
63
73
  "R6b-no-makefile",
64
74
  re.compile(r"(?i)no\s+Makefile"),
65
75
  ("Makefile",),
66
76
  "doc claims no Makefile, but Makefile is present",
67
77
  ),
68
- (
78
+ _Rule(
69
79
  "R7-no-mcp",
70
80
  re.compile(r"(?i)no\s+MCP(\s+server)?"),
71
81
  ("src/codeforerunner/mcp_server.py",),
72
82
  "doc claims no MCP server, but src/codeforerunner/mcp_server.py is present",
73
83
  ),
74
- (
84
+ _Rule(
75
85
  "R8-no-marketplace",
76
86
  re.compile(r"(?i)no\s+marketplace(\s+manifest)?"),
77
87
  ("plugins/codex/marketplace.json",),
78
88
  "doc claims no marketplace, but plugins/codex/marketplace.json is present",
79
89
  ),
90
+ # Inverse rules: fire when trigger ABSENT and phrase matches (doc claims thing exists but file gone)
91
+ _Rule(
92
+ "RI1-missing-cli",
93
+ re.compile(
94
+ r"(?i)\bforerunner\s+(?:init|scan|doc|check|generate|doctor)\b"
95
+ ),
96
+ ("src/codeforerunner/cli.py",),
97
+ "doc references forerunner CLI commands, but src/codeforerunner/cli.py is absent",
98
+ invert=True,
99
+ ),
100
+ _Rule(
101
+ "RI5-missing-python-package",
102
+ re.compile(r"(?i)\bpipx?\s+install\s+codeforerunner\b"),
103
+ ("pyproject.toml",),
104
+ "doc claims package is installable via pip/pipx, but pyproject.toml is absent",
105
+ invert=True,
106
+ ),
107
+ _Rule(
108
+ "RI7-missing-mcp",
109
+ re.compile(r"(?i)\bforerunner\s+mcp-server\b"),
110
+ ("src/codeforerunner/mcp_server.py",),
111
+ "doc references forerunner mcp-server, but src/codeforerunner/mcp_server.py is absent",
112
+ invert=True,
113
+ ),
80
114
  ]
81
115
 
116
+ _VERSION_PIN_RE = re.compile(
117
+ r"(?:codeforerunner==|codeforerunner@v)"
118
+ r"(\d+\.\d+\.\d+)"
119
+ )
120
+ _PYPROJECT_VERSION_RE = re.compile(r'^version\s*=\s*"(\d+\.\d+\.\d+)"', re.MULTILINE)
121
+ _CHANGELOG_FILENAME = "CHANGELOG.md"
122
+
82
123
 
83
124
  def _trigger_exists(repo: Path, patterns: tuple[str, ...]) -> bool:
84
125
  for pat in patterns:
@@ -114,6 +155,54 @@ def _path_ignored(repo: Path, doc: Path, ignore_patterns: tuple[str, ...]) -> bo
114
155
  return any(fnmatch.fnmatch(rel, pat) for pat in ignore_patterns)
115
156
 
116
157
 
158
+ def _current_version(repo: Path) -> str | None:
159
+ pyproject = repo / "pyproject.toml"
160
+ if not pyproject.is_file():
161
+ return None
162
+ try:
163
+ text = pyproject.read_text(encoding="utf-8")
164
+ except OSError:
165
+ return None
166
+ m = _PYPROJECT_VERSION_RE.search(text)
167
+ return m.group(1) if m else None
168
+
169
+
170
+ def _check_version_drift(
171
+ repo: Path,
172
+ docs: list[Path],
173
+ ignore_patterns: tuple[str, ...],
174
+ enabled: set[str] | None,
175
+ ) -> list[Violation]:
176
+ if enabled is not None and "RV1-version-drift" not in enabled:
177
+ return []
178
+ current = _current_version(repo)
179
+ if current is None:
180
+ return []
181
+ violations: list[Violation] = []
182
+ for doc in docs:
183
+ if doc.name == _CHANGELOG_FILENAME:
184
+ continue
185
+ if _path_ignored(repo, doc, ignore_patterns):
186
+ continue
187
+ try:
188
+ text = doc.read_text(encoding="utf-8")
189
+ except (OSError, UnicodeDecodeError):
190
+ continue
191
+ for lineno, line in enumerate(text.splitlines(), start=1):
192
+ for m in _VERSION_PIN_RE.finditer(line):
193
+ pinned = m.group(1)
194
+ if pinned != current:
195
+ violations.append(
196
+ Violation(
197
+ path=doc,
198
+ line=lineno,
199
+ rule_id="RV1-version-drift",
200
+ message=f"version pin {pinned!r} does not match current {current!r}",
201
+ )
202
+ )
203
+ return violations
204
+
205
+
117
206
  def run(repo: Path, config: CheckConfig | None = None) -> list[Violation]:
118
207
  """Scan repo docs for drift; return list of violations.
119
208
 
@@ -124,16 +213,18 @@ def run(repo: Path, config: CheckConfig | None = None) -> list[Violation]:
124
213
  enabled = set(config.enabled_rules) if (config and config.enabled_rules is not None) else None
125
214
  ignore_patterns = config.ignore_paths if config else ()
126
215
 
127
- active_rules = [
128
- (rid, rx, msg)
129
- for rid, rx, triggers, msg in _RULES
130
- if _trigger_exists(repo, triggers) and (enabled is None or rid in enabled)
131
- ]
132
- if not active_rules:
133
- return []
216
+ docs = _scanned_docs(repo)
217
+
218
+ active_rules: list[_Rule] = []
219
+ for rule in _RULES:
220
+ if enabled is not None and rule.id not in enabled:
221
+ continue
222
+ trigger_found = _trigger_exists(repo, rule.triggers)
223
+ if (not rule.invert and trigger_found) or (rule.invert and not trigger_found):
224
+ active_rules.append(rule)
134
225
 
135
226
  violations: list[Violation] = []
136
- for doc in _scanned_docs(repo):
227
+ for doc in docs:
137
228
  if _path_ignored(repo, doc, ignore_patterns):
138
229
  continue
139
230
  try:
@@ -141,11 +232,13 @@ def run(repo: Path, config: CheckConfig | None = None) -> list[Violation]:
141
232
  except (OSError, UnicodeDecodeError):
142
233
  continue
143
234
  for lineno, line in enumerate(text.splitlines(), start=1):
144
- for rid, rx, msg in active_rules:
145
- if rx.search(line):
235
+ for rule in active_rules:
236
+ if rule.pattern.search(line):
146
237
  violations.append(
147
- Violation(path=doc, line=lineno, rule_id=rid, message=msg)
238
+ Violation(path=doc, line=lineno, rule_id=rule.id, message=rule.message)
148
239
  )
240
+
241
+ violations.extend(_check_version_drift(repo, docs, ignore_patterns, enabled))
149
242
  return violations
150
243
 
151
244
 
codeforerunner/cli.py CHANGED
@@ -111,7 +111,8 @@ def cmd_generate(args: argparse.Namespace) -> int:
111
111
  repo_root = Path(args.repo).resolve() if args.repo else Path.cwd()
112
112
  cfg = load_from_repo(repo_root)
113
113
 
114
- provider_name = args.provider or (cfg.provider if cfg else "anthropic")
114
+ explicit_provider = args.provider or (cfg.provider if cfg else None)
115
+ provider_name = explicit_provider or "anthropic"
115
116
  model = args.model or (cfg.model if cfg else None)
116
117
  provider_cls = _providers.get(provider_name)
117
118
  provider = provider_cls()
@@ -132,8 +133,30 @@ def cmd_generate(args: argparse.Namespace) -> int:
132
133
  env_var = (cfg.api_key_env.get(provider_name) if cfg else None) or provider.default_env_var
133
134
  api_key = os.environ.get(env_var)
134
135
  if api_key is None and provider_name != "ollama":
135
- print(f"error: missing API key; set ${env_var}", file=sys.stderr)
136
- return 3
136
+ if explicit_provider is None and _providers.ollama_available():
137
+ provider_name = "ollama"
138
+ provider_cls = _providers.get("ollama")
139
+ provider = provider_cls()
140
+ if not args.model:
141
+ model = provider.default_model
142
+ print("info: no API key; falling back to Ollama (local mode)", file=sys.stderr)
143
+ else:
144
+ msg = f"error: missing API key; set ${env_var}"
145
+ if explicit_provider is None:
146
+ msg += "\nhint: start Ollama for keyless local generation (https://ollama.com)"
147
+ print(msg, file=sys.stderr)
148
+ return 3
149
+
150
+ if getattr(args, "stream", False):
151
+ try:
152
+ for chunk in provider.stream(prompt=buf.getvalue(), model=model, api_key=api_key):
153
+ sys.stdout.write(chunk)
154
+ sys.stdout.flush()
155
+ except _providers.ProviderError as e:
156
+ print(f"error: {provider_name} provider failed: {e}", file=sys.stderr)
157
+ return 4
158
+ sys.stdout.write("\n")
159
+ return 0
137
160
 
138
161
  try:
139
162
  result = provider.complete(prompt=buf.getvalue(), model=model, api_key=api_key)
@@ -151,7 +174,15 @@ def cmd_generate(args: argparse.Namespace) -> int:
151
174
 
152
175
  def cmd_doctor(args: argparse.Namespace) -> int:
153
176
  from codeforerunner import doctor
177
+ from codeforerunner.config import CONFIG_FILENAME
154
178
  root = Path(args.repo).resolve() if args.repo else Path.cwd()
179
+ if getattr(args, "fix", False):
180
+ cfg_path = root / CONFIG_FILENAME
181
+ if not cfg_path.is_file():
182
+ cfg_path.write_text(doctor.starter_config(), encoding="utf-8")
183
+ print(f"wrote {cfg_path}", file=sys.stderr)
184
+ else:
185
+ print(f"{cfg_path} already exists; skipping --fix", file=sys.stderr)
155
186
  findings = doctor.run(root)
156
187
  sys.stdout.write(doctor.format_report(findings) + "\n")
157
188
  return 1 if any(f.severity == "error" for f in findings) else 0
@@ -200,12 +231,18 @@ def build_parser() -> argparse.ArgumentParser:
200
231
  s_mcp.set_defaults(func=cmd_mcp_server)
201
232
 
202
233
  s_doctor = sub.add_parser("doctor", help="health report: skill parity + marketplace + installed dests")
234
+ s_doctor.add_argument(
235
+ "--fix",
236
+ action="store_true",
237
+ help="write a starter forerunner.config.yaml if absent",
238
+ )
203
239
  s_doctor.set_defaults(func=cmd_doctor)
204
240
 
205
241
  s_gen = sub.add_parser("generate", help="resolve bundle for <task> and call the configured provider")
206
242
  s_gen.add_argument("task", help="task basename under prompts/tasks/")
207
243
  s_gen.add_argument("--provider", help="override config provider")
208
244
  s_gen.add_argument("--model", help="override config model")
245
+ s_gen.add_argument("--stream", action="store_true", help="stream output token-by-token")
209
246
  s_gen.set_defaults(func=cmd_generate)
210
247
 
211
248
  from codeforerunner import installer
codeforerunner/doctor.py CHANGED
@@ -225,13 +225,23 @@ def _check_config_loadable(repo: Path) -> list[Finding]:
225
225
 
226
226
 
227
227
  def _check_provider_api_key(repo: Path) -> list[Finding]:
228
+ from codeforerunner.providers.ollama import is_available as _ollama_available
229
+
228
230
  cfg_path = repo / CONFIG_FILENAME
229
231
  if not cfg_path.is_file():
232
+ if _ollama_available():
233
+ return [
234
+ Finding(
235
+ "ok",
236
+ "provider-api-key",
237
+ "no config; Ollama running — generate will use local mode automatically",
238
+ )
239
+ ]
230
240
  return [
231
241
  Finding(
232
242
  "ok",
233
243
  "provider-api-key",
234
- f"no {CONFIG_FILENAME}; provider key not checked",
244
+ f"no {CONFIG_FILENAME}; set an API key in config or start Ollama for keyless local generation",
235
245
  )
236
246
  ]
237
247
  try:
@@ -259,7 +269,7 @@ def _check_provider_api_key(repo: Path) -> list[Finding]:
259
269
  Finding(
260
270
  "ok",
261
271
  "provider-api-key",
262
- "ollama needs no API key (OLLAMA_HOST optional)",
272
+ "running in local mode (Ollama; no API key needed)",
263
273
  )
264
274
  ]
265
275
  env_var = cfg.api_key_env.get(provider) or _DEFAULT_PROVIDER_ENV.get(provider, "")
@@ -274,6 +284,26 @@ def _check_provider_api_key(repo: Path) -> list[Finding]:
274
284
  ]
275
285
 
276
286
 
287
+ _STARTER_CONFIG = """\
288
+ # forerunner.config.yaml — generated by `forerunner doctor --fix`
289
+ # See https://github.com/derek-palmer/codeforerunner for docs.
290
+
291
+ enabled_rules:
292
+ - R1-no-cli
293
+ - R2-no-pre-commit
294
+ - R3-no-ci
295
+ - R4-no-installer
296
+ - R5-no-python-package
297
+ - R7-no-mcp
298
+ - R8-no-marketplace
299
+ ignore_paths: []
300
+ """
301
+
302
+
303
+ def starter_config() -> str:
304
+ return _STARTER_CONFIG
305
+
306
+
277
307
  def run(repo: Path) -> list[Finding]:
278
308
  repo = repo.resolve()
279
309
  findings: list[Finding] = []
@@ -21,6 +21,23 @@ EXIT_USAGE = 2
21
21
  EXIT_BODY_MISMATCH = 3
22
22
  EXIT_UNMANAGED_DEST = 4
23
23
 
24
+ # Per-task skill slugs (source: skills/<slug>/SKILL.md → plugins/codeforerunner/skills/<slug>/SKILL.md)
25
+ TASK_SKILL_SLUGS: tuple[str, ...] = (
26
+ "codeforerunner",
27
+ "forerunner-scan",
28
+ "forerunner-readme",
29
+ "forerunner-api-docs",
30
+ "forerunner-audit",
31
+ "forerunner-changelog",
32
+ "forerunner-check",
33
+ "forerunner-diagrams",
34
+ "forerunner-flows",
35
+ "forerunner-init",
36
+ "forerunner-review",
37
+ "forerunner-stack-docs",
38
+ "forerunner-version-audit",
39
+ )
40
+
24
41
 
25
42
  @dataclass(frozen=True)
26
43
  class Target:
@@ -44,9 +61,65 @@ def resolve_target(agent: str, override: Path | None) -> Target:
44
61
  return Target(agent, home / ".codex/skills/codeforerunner/SKILL.md")
45
62
  if agent == "claude":
46
63
  return Target(agent, home / ".claude/plugins/codeforerunner/skills/codeforerunner/SKILL.md")
64
+ if agent == "gemini":
65
+ raise ValueError(
66
+ "gemini install is handled via `gemini extensions install`; "
67
+ "run `./install.sh --only gemini` instead"
68
+ )
47
69
  raise ValueError(f"unknown agent '{agent}' (expected: codex, claude, generic)")
48
70
 
49
71
 
72
+ def resolve_skill_target(agent: str, slug: str) -> Target:
73
+ """Return install target for a per-task skill slug."""
74
+ home = _home()
75
+ if agent == "codex":
76
+ return Target(agent, home / f".codex/skills/{slug}/SKILL.md")
77
+ if agent == "claude":
78
+ return Target(agent, home / f".claude/plugins/codeforerunner/skills/{slug}/SKILL.md")
79
+ raise ValueError(f"install_all not supported for agent '{agent}' (expected: codex, claude)")
80
+
81
+
82
+ def install_all_skills(
83
+ *,
84
+ agent: str,
85
+ repo_root: Path,
86
+ check_only: bool,
87
+ out=None,
88
+ err=None,
89
+ ) -> int:
90
+ """Install all per-task skills for the given agent. Returns 0 on full success."""
91
+ out = out or sys.stdout
92
+ err = err or sys.stderr
93
+ any_error = False
94
+ for slug in TASK_SKILL_SLUGS:
95
+ src_path = repo_root / "plugins" / "codeforerunner" / "skills" / slug / "SKILL.md"
96
+ if not src_path.is_file():
97
+ print(f"warning: skill source not found: {src_path}", file=err)
98
+ continue
99
+ try:
100
+ target = resolve_skill_target(agent, slug)
101
+ except ValueError as e:
102
+ print(f"error: {e}", file=err)
103
+ return EXIT_USAGE
104
+ # For per-task skills use simple copy (no body-parity check against canonical)
105
+ dest = target.path
106
+ prefix = "would " if check_only else ""
107
+ if dest.exists():
108
+ src_trimmed = src_path.read_bytes().rstrip()
109
+ dest_trimmed = dest.read_bytes().rstrip()
110
+ if src_trimmed == dest_trimmed:
111
+ print(f"skip: {dest} (up-to-date)", file=out)
112
+ continue
113
+ action = "update"
114
+ else:
115
+ action = "create"
116
+ print(f"{prefix}{action}: {dest}", file=out)
117
+ if not check_only:
118
+ dest.parent.mkdir(parents=True, exist_ok=True)
119
+ dest.write_bytes(src_path.read_bytes())
120
+ return EXIT_OK if not any_error else EXIT_BODY_MISMATCH
121
+
122
+
50
123
  def resolve_marketplace_target(agent: str, override: Path | None) -> Target:
51
124
  if agent == "generic":
52
125
  if override is None:
@@ -277,8 +350,11 @@ def install(
277
350
 
278
351
 
279
352
  def add_subparser(sub: argparse._SubParsersAction) -> None:
280
- p = sub.add_parser("install", help="install skill into agent-specific directory (D.installer)")
281
- p.add_argument("agent", choices=["codex", "claude", "generic"])
353
+ p = sub.add_parser("install", help="install skill(s) into agent-specific directories (D.installer)")
354
+ p.add_argument("agent", choices=["codex", "claude", "generic"], nargs="?",
355
+ help="target agent (omit with --all to install to all detected agents)")
356
+ p.add_argument("--all", action="store_true",
357
+ help="install all per-task skills for the specified agent")
282
358
  p.add_argument("--check", action="store_true", help="dry-run: print plan, write nothing")
283
359
  p.add_argument("--path", type=Path, help="dest path override (required for generic)")
284
360
  p.add_argument("--source", type=Path, help="source skill file (default: agent/codeforerunner.skill.md)")
@@ -292,6 +368,19 @@ def add_subparser(sub: argparse._SubParsersAction) -> None:
292
368
 
293
369
  def _cli_entry(args: argparse.Namespace) -> int:
294
370
  root = Path(args.repo).resolve() if args.repo else Path.cwd()
371
+
372
+ if getattr(args, "all", False):
373
+ agent = args.agent or "claude"
374
+ return install_all_skills(
375
+ agent=agent,
376
+ repo_root=root,
377
+ check_only=args.check,
378
+ )
379
+
380
+ if not args.agent:
381
+ print("error: specify an agent or use --all", file=sys.stderr)
382
+ return EXIT_USAGE
383
+
295
384
  return install(
296
385
  agent=args.agent,
297
386
  repo_root=root,
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from codeforerunner.providers.anthropic import AnthropicProvider
6
6
  from codeforerunner.providers.base import CompletionResult, Provider, ProviderError
7
7
  from codeforerunner.providers.google import GoogleProvider
8
- from codeforerunner.providers.ollama import OllamaProvider
8
+ from codeforerunner.providers.ollama import OllamaProvider, is_available as ollama_available
9
9
  from codeforerunner.providers.openai import OpenAIProvider
10
10
 
11
11
  __all__ = [
@@ -18,6 +18,7 @@ __all__ = [
18
18
  "ProviderError",
19
19
  "REGISTRY",
20
20
  "get",
21
+ "ollama_available",
21
22
  ]
22
23
 
23
24
  REGISTRY: dict[str, type] = {
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import urllib.error
7
7
  import urllib.request
8
+ from typing import Iterator
8
9
 
9
10
  from codeforerunner.providers.base import CompletionResult, ProviderError
10
11
 
@@ -12,7 +13,7 @@ from codeforerunner.providers.base import CompletionResult, ProviderError
12
13
  class AnthropicProvider:
13
14
  name = "anthropic"
14
15
  default_env_var = "ANTHROPIC_API_KEY"
15
- default_model = "claude-opus-4-5"
16
+ default_model = "claude-opus-4-7"
16
17
 
17
18
  endpoint = "https://api.anthropic.com/v1/messages"
18
19
 
@@ -59,3 +60,59 @@ class AnthropicProvider:
59
60
  return CompletionResult(
60
61
  text=text, model=data.get("model", model), usage=data.get("usage")
61
62
  )
63
+
64
+ def stream(
65
+ self,
66
+ *,
67
+ prompt: str,
68
+ model: str | None = None,
69
+ api_key: str | None = None,
70
+ ) -> Iterator[str]:
71
+ if not api_key:
72
+ raise ProviderError(f"missing API key (set ${self.default_env_var})")
73
+ model = model or self.default_model
74
+ body = json.dumps(
75
+ {
76
+ "model": model,
77
+ "max_tokens": 4096,
78
+ "stream": True,
79
+ "messages": [{"role": "user", "content": prompt}],
80
+ }
81
+ ).encode("utf-8")
82
+ req = urllib.request.Request(
83
+ self.endpoint,
84
+ data=body,
85
+ method="POST",
86
+ headers={
87
+ "x-api-key": api_key,
88
+ "anthropic-version": "2023-06-01",
89
+ "content-type": "application/json",
90
+ },
91
+ )
92
+ try:
93
+ resp = urllib.request.urlopen(req)
94
+ except urllib.error.HTTPError as e:
95
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
96
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
97
+ except urllib.error.URLError as e:
98
+ raise ProviderError(f"network error: {e.reason}") from e
99
+ try:
100
+ for raw_line in resp:
101
+ line = raw_line.decode("utf-8").rstrip("\n")
102
+ if not line.startswith("data: "):
103
+ continue
104
+ payload = line[6:]
105
+ if payload.strip() == "[DONE]":
106
+ break
107
+ try:
108
+ event = json.loads(payload)
109
+ except json.JSONDecodeError:
110
+ continue
111
+ if event.get("type") == "content_block_delta":
112
+ delta = event.get("delta", {})
113
+ if delta.get("type") == "text_delta":
114
+ text = delta.get("text", "")
115
+ if text:
116
+ yield text
117
+ finally:
118
+ resp.close()
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
- from typing import Protocol
6
+ from typing import Iterator, Protocol
7
7
 
8
8
 
9
9
  @dataclass(frozen=True)
@@ -26,6 +26,14 @@ class Provider(Protocol):
26
26
  api_key: str | None = None,
27
27
  ) -> CompletionResult: ...
28
28
 
29
+ def stream(
30
+ self,
31
+ *,
32
+ prompt: str,
33
+ model: str | None = None,
34
+ api_key: str | None = None,
35
+ ) -> Iterator[str]: ...
36
+
29
37
 
30
38
  class ProviderError(Exception):
31
39
  """Raised on provider HTTP failures, missing keys, or malformed responses."""
@@ -6,6 +6,7 @@ import json
6
6
  import urllib.error
7
7
  import urllib.parse
8
8
  import urllib.request
9
+ from typing import Iterator
9
10
 
10
11
  from codeforerunner.providers.base import CompletionResult, ProviderError
11
12
 
@@ -18,6 +19,10 @@ class GoogleProvider:
18
19
  endpoint_template = (
19
20
  "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
20
21
  )
22
+ stream_endpoint_template = (
23
+ "https://generativelanguage.googleapis.com/v1beta/models/{model}:streamGenerateContent"
24
+ "?key={key}&alt=sse"
25
+ )
21
26
 
22
27
  def complete(
23
28
  self,
@@ -60,3 +65,48 @@ class GoogleProvider:
60
65
  model=data.get("modelVersion", model),
61
66
  usage=data.get("usageMetadata"),
62
67
  )
68
+
69
+ def stream(
70
+ self,
71
+ *,
72
+ prompt: str,
73
+ model: str | None = None,
74
+ api_key: str | None = None,
75
+ ) -> Iterator[str]:
76
+ if not api_key:
77
+ raise ProviderError(f"missing API key (set ${self.default_env_var})")
78
+ model = model or self.default_model
79
+ url = self.stream_endpoint_template.format(
80
+ model=urllib.parse.quote(model, safe=""),
81
+ key=urllib.parse.quote(api_key, safe=""),
82
+ )
83
+ body = json.dumps(
84
+ {"contents": [{"parts": [{"text": prompt}]}]}
85
+ ).encode("utf-8")
86
+ req = urllib.request.Request(
87
+ url,
88
+ data=body,
89
+ method="POST",
90
+ headers={"content-type": "application/json"},
91
+ )
92
+ try:
93
+ resp = urllib.request.urlopen(req)
94
+ except urllib.error.HTTPError as e:
95
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
96
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
97
+ except urllib.error.URLError as e:
98
+ raise ProviderError(f"network error: {e.reason}") from e
99
+ try:
100
+ for raw_line in resp:
101
+ line = raw_line.decode("utf-8").rstrip("\n")
102
+ if not line.startswith("data: "):
103
+ continue
104
+ try:
105
+ event = json.loads(line[6:])
106
+ text = event["candidates"][0]["content"]["parts"][0]["text"]
107
+ if text:
108
+ yield text
109
+ except (json.JSONDecodeError, KeyError, IndexError, TypeError):
110
+ continue
111
+ finally:
112
+ resp.close()
@@ -6,12 +6,23 @@ import json
6
6
  import os
7
7
  import urllib.error
8
8
  import urllib.request
9
+ from typing import Iterator
9
10
 
10
11
  from codeforerunner.providers.base import CompletionResult, ProviderError
11
12
 
12
13
  DEFAULT_HOST = "http://localhost:11434"
13
14
 
14
15
 
16
+ def is_available(host: str | None = None) -> bool:
17
+ """Return True if an Ollama instance is reachable at the configured host."""
18
+ base = (host or os.environ.get("OLLAMA_HOST") or DEFAULT_HOST).rstrip("/")
19
+ try:
20
+ urllib.request.urlopen(f"{base}/api/tags", timeout=2)
21
+ return True
22
+ except Exception:
23
+ return False
24
+
25
+
15
26
  class OllamaProvider:
16
27
  name = "ollama"
17
28
  default_env_var = "OLLAMA_HOST"
@@ -54,3 +65,47 @@ class OllamaProvider:
54
65
  usage_keys = ("prompt_eval_count", "eval_count", "total_duration")
55
66
  usage = {k: data[k] for k in usage_keys if k in data} or None
56
67
  return CompletionResult(text=text, model=data.get("model", model), usage=usage)
68
+
69
+ def stream(
70
+ self,
71
+ *,
72
+ prompt: str,
73
+ model: str | None = None,
74
+ api_key: str | None = None,
75
+ ) -> Iterator[str]:
76
+ base = api_key or os.environ.get(self.default_env_var) or DEFAULT_HOST
77
+ base = base.rstrip("/")
78
+ model = model or self.default_model
79
+ url = f"{base}/api/generate"
80
+ body = json.dumps(
81
+ {"model": model, "prompt": prompt, "stream": True}
82
+ ).encode("utf-8")
83
+ req = urllib.request.Request(
84
+ url,
85
+ data=body,
86
+ method="POST",
87
+ headers={"content-type": "application/json"},
88
+ )
89
+ try:
90
+ resp = urllib.request.urlopen(req)
91
+ except urllib.error.HTTPError as e:
92
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
93
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
94
+ except urllib.error.URLError as e:
95
+ raise ProviderError(f"network error: {e.reason}") from e
96
+ try:
97
+ for raw_line in resp:
98
+ line = raw_line.decode("utf-8").strip()
99
+ if not line:
100
+ continue
101
+ try:
102
+ event = json.loads(line)
103
+ except json.JSONDecodeError:
104
+ continue
105
+ text = event.get("response", "")
106
+ if text:
107
+ yield text
108
+ if event.get("done", False):
109
+ break
110
+ finally:
111
+ resp.close()
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import urllib.error
7
7
  import urllib.request
8
+ from typing import Iterator
8
9
 
9
10
  from codeforerunner.providers.base import CompletionResult, ProviderError
10
11
 
@@ -57,3 +58,56 @@ class OpenAIProvider:
57
58
  return CompletionResult(
58
59
  text=text, model=data.get("model", model), usage=data.get("usage")
59
60
  )
61
+
62
+ def stream(
63
+ self,
64
+ *,
65
+ prompt: str,
66
+ model: str | None = None,
67
+ api_key: str | None = None,
68
+ ) -> Iterator[str]:
69
+ if not api_key:
70
+ raise ProviderError(f"missing API key (set ${self.default_env_var})")
71
+ model = model or self.default_model
72
+ body = json.dumps(
73
+ {
74
+ "model": model,
75
+ "stream": True,
76
+ "messages": [{"role": "user", "content": prompt}],
77
+ }
78
+ ).encode("utf-8")
79
+ req = urllib.request.Request(
80
+ self.endpoint,
81
+ data=body,
82
+ method="POST",
83
+ headers={
84
+ "Authorization": f"Bearer {api_key}",
85
+ "content-type": "application/json",
86
+ },
87
+ )
88
+ try:
89
+ resp = urllib.request.urlopen(req)
90
+ except urllib.error.HTTPError as e:
91
+ snippet = (e.read() or b"")[:500].decode("utf-8", errors="replace")
92
+ raise ProviderError(f"HTTP {e.code}: {snippet}") from e
93
+ except urllib.error.URLError as e:
94
+ raise ProviderError(f"network error: {e.reason}") from e
95
+ try:
96
+ for raw_line in resp:
97
+ line = raw_line.decode("utf-8").rstrip("\n")
98
+ if not line.startswith("data: "):
99
+ continue
100
+ payload = line[6:]
101
+ if payload.strip() == "[DONE]":
102
+ break
103
+ try:
104
+ event = json.loads(payload)
105
+ except json.JSONDecodeError:
106
+ continue
107
+ choices = event.get("choices", [])
108
+ if choices:
109
+ text = choices[0].get("delta", {}).get("content")
110
+ if text:
111
+ yield text
112
+ finally:
113
+ resp.close()
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: codeforerunner
3
+ Version: 0.4.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
+ Keywords: repository-documentation,developer-tools,agent-tooling,code-generation,prompt-engineering,mcp,llm,documentation
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Documentation
19
+ Classifier: Topic :: Software Development :: Documentation
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE.md
25
+ Requires-Dist: PyYAML>=6.0
26
+ Dynamic: license-file
27
+
28
+ ![codeForerunner — your codebase gets a Forerunner; your docs finally see the light](images/readme_banner.png)
29
+
30
+ # codeForerunner
31
+
32
+ Model-agnostic repository documentation tooling. Ships a prompt pack for codebase analysis and doc generation, a thin Python CLI, an MCP server, drift-detection rules that keep docs honest — and native slash-command skills for Claude Code, Codex, Gemini CLI, and other agent CLIs.
33
+
34
+ ## Two modes
35
+
36
+ ### Mode A — Agent skill (recommended, no API key required)
37
+
38
+ Install forerunner's prompt pack as skills into your agent CLI. Each documentation task becomes a slash command (`/forerunner-readme`, `/forerunner-check`, etc.) available inside Claude Code, Codex, Gemini CLI, and other agents. Authentication is handled by your existing agent subscription — no separate API key needed.
39
+
40
+ ```bash
41
+ # From a cloned repo
42
+ ./install.sh
43
+
44
+ # One-liner (auto-detects Claude Code, Codex, Gemini CLI)
45
+ curl -fsSL https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.sh | bash
46
+
47
+ # Windows
48
+ irm https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.ps1 | iex
49
+
50
+ # Via forerunner CLI (after pip install)
51
+ forerunner install --all claude
52
+ forerunner install --all codex
53
+ ```
54
+
55
+ Then in your agent:
56
+
57
+ ```
58
+ /forerunner-scan ← scan the repo first
59
+ /forerunner-readme ← generate README
60
+ /forerunner-check ← detect doc drift
61
+ ```
62
+
63
+ ### Mode B — Direct API (needs API key or Ollama)
64
+
65
+ Install the Python CLI and call your provider directly. Works without any agent CLI installed.
66
+
67
+ ```bash
68
+ pipx install codeforerunner # recommended
69
+ pip install codeforerunner # alternative
70
+ ```
71
+
72
+ Configure a provider (or start Ollama for keyless local generation):
73
+
74
+ ```bash
75
+ export ANTHROPIC_API_KEY=sk-...
76
+ forerunner generate readme --stream
77
+ ```
78
+
79
+ If no API key and no `--provider` flag, forerunner auto-detects Ollama at `localhost:11434` and falls back to local mode.
80
+
81
+ ## Slash commands
82
+
83
+ | Command | Task | Purpose |
84
+ |---------|------|---------|
85
+ | `/forerunner-scan` | `scan` | Collect repo evidence (run first) |
86
+ | `/forerunner-readme` | `readme` | Generate or refresh README.md |
87
+ | `/forerunner-api-docs` | `api-docs` | Generate API reference docs |
88
+ | `/forerunner-diagrams` | `diagrams` | Generate Mermaid architecture diagrams |
89
+ | `/forerunner-flows` | `flows` | Document system flows |
90
+ | `/forerunner-stack-docs` | `stack-docs` | Stack-specific developer docs |
91
+ | `/forerunner-version-audit` | `version-audit` | Audit pinned versions vs EOL |
92
+ | `/forerunner-check` | `check` | Check docs for staleness |
93
+ | `/forerunner-review` | `review` | Doc-impact summary for PR review |
94
+ | `/forerunner-audit` | `audit` | Security and dependency audit |
95
+ | `/forerunner-changelog` | `changelog` | Generate changelog from git log |
96
+ | `/forerunner-init` | `init-agent-onboarding` | Bootstrap or refresh AGENTS.md |
97
+
98
+ Slash command availability depends on the agent CLI. Claude Code, Codex, and Gemini CLI support all 12 commands after install.
99
+
100
+ ## Skill install options
101
+
102
+ | Flag | Effect |
103
+ |------|--------|
104
+ | `./install.sh` | Auto-detect all agents, install all skills |
105
+ | `./install.sh --only claude` | Claude Code only |
106
+ | `./install.sh --only codex` | Codex only |
107
+ | `./install.sh --only gemini` | Gemini CLI only |
108
+ | `./install.sh --dry-run` | Preview, write nothing |
109
+ | `./install.sh --list` | Show detected agents + skill list |
110
+ | `./install.sh --uninstall` | Remove all installed skills |
111
+
112
+ ## CLI
113
+
114
+ ```bash
115
+ pip install codeforerunner
116
+ ```
117
+
118
+ | Command | Purpose |
119
+ |---------|---------|
120
+ | `forerunner init` | Resolve agent-onboarding bundle to stdout. |
121
+ | `forerunner scan` | Resolve scan bundle to stdout. |
122
+ | `forerunner doc <task>` | Resolve `base + partials + task` bundle to stdout. |
123
+ | `forerunner check` | Run drift-detection rules; no-op without `forerunner.config.yaml`. |
124
+ | `forerunner generate <task>` | Call configured provider directly. Add `--stream` for token-by-token output. Falls back to Ollama automatically when no API key is configured. |
125
+ | `forerunner doctor` | Health report: skill parity, config, provider key, local-mode status. Add `--fix` to write a starter config. |
126
+ | `forerunner mcp-server` | Serve prompt bundles as MCP tools over stdio (JSON-RPC 2.0). |
127
+ | `forerunner install <agent>` | Install canonical skill into agent-specific directory. Add `--all` for all per-task skills. |
128
+
129
+ ## Prompt pack
130
+
131
+ Prompts are bundled inside the package at `src/codeforerunner/prompts/`.
132
+
133
+ ```text
134
+ prompts/
135
+ ├── system/base.md
136
+ ├── partials/
137
+ │ ├── context-format.md
138
+ │ ├── output-rules.md
139
+ │ └── stack-hints.md
140
+ └── tasks/
141
+ ├── scan.md api-docs.md audit.md
142
+ ├── readme.md diagrams.md changelog.md
143
+ ├── check.md flows.md version-audit.md
144
+ ├── review.md stack-docs.md
145
+ └── init-agent-onboarding.md
146
+ ```
147
+
148
+ ## Quick start (agent skill mode)
149
+
150
+ ```bash
151
+ # Install skills into Claude Code
152
+ curl -fsSL https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.sh | bash
153
+
154
+ # In Claude Code:
155
+ # /forerunner-scan → scans your repo
156
+ # /forerunner-readme → generates README.md
157
+ # /forerunner-check → checks for doc drift
158
+ ```
159
+
160
+ ## Quick start (direct API mode)
161
+
162
+ ```bash
163
+ # 1. Install and configure
164
+ pip install codeforerunner
165
+ export ANTHROPIC_API_KEY=sk-...
166
+
167
+ # 2. Run a task
168
+ forerunner generate readme --stream
169
+
170
+ # 3. Enable drift detection
171
+ forerunner doctor --fix # writes forerunner.config.yaml
172
+ forerunner check # run any time or as pre-commit hook
173
+ ```
174
+
175
+ ## GitHub Action
176
+
177
+ ```yaml
178
+ - uses: derek-palmer/codeforerunner@v0.3.2
179
+ ```
180
+
181
+ No-op when `forerunner.config.yaml` is absent.
182
+
183
+ ## Configuration
184
+
185
+ Copy `forerunner.config.yaml.example` to `forerunner.config.yaml` to opt in to drift rules. Generate a starter config with:
186
+
187
+ ```bash
188
+ forerunner doctor --fix
189
+ ```
190
+
191
+ ### Config fields
192
+
193
+ ```yaml
194
+ provider: anthropic # anthropic | openai | google | ollama
195
+ model: claude-opus-4-7
196
+ api_key_env:
197
+ anthropic: ANTHROPIC_API_KEY
198
+
199
+ tasks:
200
+ check:
201
+ enabled_rules:
202
+ - R1-no-cli
203
+ - R2-no-pre-commit
204
+ - R3-no-ci
205
+ - R4-no-installer
206
+ - R5-no-python-package
207
+ - R7-no-mcp
208
+ - R8-no-marketplace
209
+ - RI1-missing-cli
210
+ - RI5-missing-python-package
211
+ - RI7-missing-mcp
212
+ - RV1-version-drift
213
+ ignore_paths:
214
+ - docs/legacy/**/*.md
215
+ ```
216
+
217
+ ### Drift rules
218
+
219
+ | Rule | Fires when |
220
+ |------|-----------|
221
+ | `R1-no-cli` | Doc denies having a CLI, but `cli.py` is present |
222
+ | `R2-no-pre-commit` | Doc denies having pre-commit hooks, but `.pre-commit-hooks.yaml` present |
223
+ | `R3-no-ci` | Doc denies having CI, but `.github/workflows/*.yml` present |
224
+ | `R4-no-installer` | Doc denies having an installer, but `installer.py` present |
225
+ | `R5-no-python-package` | Doc denies having a Python package, but `pyproject.toml` present |
226
+ | `R6-no-docker` | Doc denies having Docker, but `Dockerfile`/`compose.yml` present |
227
+ | `R7-no-mcp` | Doc denies having an MCP server, but `mcp_server.py` present |
228
+ | `R8-no-marketplace` | Doc denies having a marketplace, but `marketplace.json` present |
229
+ | `RI1-missing-cli` | Doc references `forerunner` subcommands but `cli.py` absent |
230
+ | `RI5-missing-python-package` | Doc shows `pip install codeforerunner` but `pyproject.toml` absent |
231
+ | `RI7-missing-mcp` | Doc references `forerunner mcp-server` but `mcp_server.py` absent |
232
+ | `RV1-version-drift` | Doc pins `codeforerunner==X.Y.Z` differing from current version |
233
+
234
+ ## MCP Server
235
+
236
+ `forerunner mcp-server` speaks JSON-RPC 2.0 over stdio and exposes one tool per `prompts/tasks/*.md`. A scan-first gate enforces SPEC V2: any tool except `scan` or `init-agent-onboarding` returns an error until `scan` has been called in the same session.
237
+
238
+ See `examples/mcp/` for Claude Desktop and mcp-cli wiring examples.
239
+
240
+ ## Providers
241
+
242
+ `forerunner generate` supports four providers. When no provider is explicitly configured and no API key is found, forerunner probes `localhost:11434` and falls back to Ollama automatically.
243
+
244
+ | Provider | Env var | Default model |
245
+ |----------|---------|---------------|
246
+ | `anthropic` | `ANTHROPIC_API_KEY` | `claude-opus-4-7` |
247
+ | `openai` | `OPENAI_API_KEY` | `gpt-4o` |
248
+ | `google` | `GOOGLE_API_KEY` | `gemini-2.5-pro` |
249
+ | `ollama` | `OLLAMA_HOST` (optional) | `llama3` |
250
+
251
+ ## Docs and spec
252
+
253
+ - `SPEC.md` — canonical phase/task tracker
254
+ - `docs/getting-started.md` — manual prompt use
255
+ - `docs/prompt-guide.md` — how system, partial, and task prompts compose
256
+ - `docs/editor-agent-setup.md` — adapting prompts to local agents
257
+ - `docs/roadmap.md` — human-readable roadmap
258
+ - `docs/agent-distribution-design.md` — packaging and installer design
@@ -1,10 +1,10 @@
1
1
  codeforerunner/__init__.py,sha256=4ItVS_FzLddTK77jExpkV3QJ1nHl2Bh-QIujM7Hg_5w,205
2
2
  codeforerunner/bundle.py,sha256=wWlhNaja8HPLzN-9pxiSrFD8X0mGi-c1HM9HRyVhmzk,2020
3
- codeforerunner/check.py,sha256=kSrVoTVb9ALEBHvUerR2nZfroXco_yz6gMgXzdz45Ac,4905
4
- codeforerunner/cli.py,sha256=3eJtUhfOj_ZMPAYPHvlQ7P-eBs2NRIpzewipqvOpy9w,7981
3
+ codeforerunner/check.py,sha256=5sJVwMMSvVCrRmwBIunYbn2pINroUjONrVJAs5ov3O4,8188
4
+ codeforerunner/cli.py,sha256=fBojCOPEEzQdXxYB0Intjhax90l7Rz7sfsgeFpozP58,9633
5
5
  codeforerunner/config.py,sha256=REs4FgmSSn7R2tLIwLJPL8VmSCGkTvw8J2SqBOggzho,6206
6
- codeforerunner/doctor.py,sha256=AFHY8YbqTp726JcxLAYvrdeOLwr6WLPiNUUF2QYOwL8,10076
7
- codeforerunner/installer.py,sha256=9Ze_hyjvow-if-NMovk9CzVDYVZSR3vyLO6s2vhOs6g,10227
6
+ codeforerunner/doctor.py,sha256=4VE7nCGdJKpzWtqK3ut-SbrOENdfLNLuvA_wxn-5fcM,10859
7
+ codeforerunner/installer.py,sha256=60CMYbV8di-0iLE8jbsLGScdgU5VSJTOO_PhoBtqYYY,13395
8
8
  codeforerunner/mcp_server.py,sha256=oIfuAR7e_rH--B1aLOATVflyWAyGpkyeeXI4SAI4eTg,4657
9
9
  codeforerunner/prompts/partials/context-format.md,sha256=WNfkr4kf2Awj0R8wLOrFotEiYCe6hfKTq5eA3Rt5_Xw,817
10
10
  codeforerunner/prompts/partials/output-rules.md,sha256=vfIAX-ImxCa-MVAeNH896uSIO7-cKbJd0KohkgHIiD8,1731
@@ -22,15 +22,15 @@ codeforerunner/prompts/tasks/review.md,sha256=IRdIXAKvv0JMOE5WtrnlO1Cd4LHXtcJqb1
22
22
  codeforerunner/prompts/tasks/scan.md,sha256=hYXf-IL1kgpBPHJapRrwTu88cLZ7y3bCmAq9HY0yG3U,2300
23
23
  codeforerunner/prompts/tasks/stack-docs.md,sha256=Dy-JSXpSmHSyhR5shQBXKa_F0PqnjPcmtljthYZpaiM,1923
24
24
  codeforerunner/prompts/tasks/version-audit.md,sha256=oK-pcoxt_VcvDOlj1Sz9OlEhXlcViLPn54r-qP5WfiA,5833
25
- codeforerunner/providers/__init__.py,sha256=ttMAbHWJIO8s-8H6Kb_EWf3LN5oMzlmX1D12RyGSmIg,962
26
- codeforerunner/providers/anthropic.py,sha256=zaJDyzH1vPr7nSqUOv6avlTk3covpkWXLHae2-bS9io,2021
27
- codeforerunner/providers/base.py,sha256=Jk9vBeRNxH9naUC6stN5jY1KHmhrqg2k2kMGOhbQTYk,752
28
- codeforerunner/providers/google.py,sha256=wBpbte8hdX_Jnr_JBHvQarogT6T1hWR4aGF5O2exBCU,2109
29
- codeforerunner/providers/ollama.py,sha256=bKhigdjHs3aw73iB5uQka9_n-XCjv1sh_zZfe1TrWTQ,1975
30
- codeforerunner/providers/openai.py,sha256=OHWeZ11OuHPgQBMwqJnJWxNHyGIP5KL7B06w2ttgxlY,1952
31
- codeforerunner-0.3.1.dist-info/licenses/LICENSE.md,sha256=iIhmJHib6GbdjcwiDMM-npiNRf3XgASom1WsOJivEdc,2915
32
- codeforerunner-0.3.1.dist-info/METADATA,sha256=koodiwqaG0vZYUyvQgkhSy7ggb0s6ldTEJazVtCyrec,7737
33
- codeforerunner-0.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
34
- codeforerunner-0.3.1.dist-info/entry_points.txt,sha256=3p8BbPlq-wfcXk42tsweKePRaGlZ1WXho1gOkuZGyIQ,55
35
- codeforerunner-0.3.1.dist-info/top_level.txt,sha256=pV1rt0-NIpNEotKXpL_sF2060DHr-_0F86LWhUlvXis,15
36
- codeforerunner-0.3.1.dist-info/RECORD,,
25
+ codeforerunner/providers/__init__.py,sha256=hoLODdqQ-beA7-MVFR6aoE29ZUSzxGVPLhwNXNN1xw4,1020
26
+ codeforerunner/providers/anthropic.py,sha256=kECeFMCeSJHcsUPvF93ECv52wXmLR6H21rFNwPzRhaM,4049
27
+ codeforerunner/providers/base.py,sha256=MMrOUVOXHWP1td-TndxhLhDyDPJZGExZCeFopZUSRCo,923
28
+ codeforerunner/providers/google.py,sha256=OWEE0FNupFWmZCeilIrgYUYDHH1iWvIwHEEsHYQoFFY,3979
29
+ codeforerunner/providers/ollama.py,sha256=Q8ACojaeiBgPQgDFxP7KKM5r4Ccu6dDspNFza1vbOzw,3871
30
+ codeforerunner/providers/openai.py,sha256=999ZzIVh0cqW4xDnzK_NACqfJxNziHwpVjwmw9_jjRw,3825
31
+ codeforerunner-0.4.0.dist-info/licenses/LICENSE.md,sha256=iIhmJHib6GbdjcwiDMM-npiNRf3XgASom1WsOJivEdc,2915
32
+ codeforerunner-0.4.0.dist-info/METADATA,sha256=hCzFSOjHkJ6iNZLYkgvHDpgjlM5P_D0OWP9zTW66pLc,9873
33
+ codeforerunner-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
34
+ codeforerunner-0.4.0.dist-info/entry_points.txt,sha256=3p8BbPlq-wfcXk42tsweKePRaGlZ1WXho1gOkuZGyIQ,55
35
+ codeforerunner-0.4.0.dist-info/top_level.txt,sha256=pV1rt0-NIpNEotKXpL_sF2060DHr-_0F86LWhUlvXis,15
36
+ codeforerunner-0.4.0.dist-info/RECORD,,
@@ -1,133 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: codeforerunner
3
- Version: 0.3.1
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
- Keywords: repository-documentation,developer-tools,agent-tooling,code-generation,prompt-engineering,mcp,llm,documentation
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Environment :: Console
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
- Classifier: Topic :: Documentation
19
- Classifier: Topic :: Software Development :: Documentation
20
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
- Classifier: Topic :: Utilities
22
- Requires-Python: >=3.11
23
- Description-Content-Type: text/markdown
24
- License-File: LICENSE.md
25
- Requires-Dist: PyYAML>=6.0
26
- Dynamic: license-file
27
-
28
- ![codeForerunner — your codebase gets a Forerunner; your docs finally see the light](images/readme_banner.png)
29
-
30
- # codeForerunner
31
-
32
- 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.
33
-
34
- 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.
35
-
36
- ## Current State
37
-
38
- - Core product: Markdown prompts in `prompts/`.
39
- - Agent package artifacts: Codex plugin files under `plugins/codeforerunner/` and Claude Code plugin files under `.claude-plugin/` plus `skills/codeforerunner/`.
40
- - 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.
41
- - 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.
42
- - Current config: `forerunner.config.yaml.example` documents the schema now parsed by `src/codeforerunner/config.py`; see "Configuration" below.
43
- - Not currently present: Docker image, Makefile, published PyPI release.
44
-
45
- ## Install
46
-
47
- After the first PyPI release:
48
-
49
- ```bash
50
- pipx install codeforerunner # recommended; isolated environment
51
- pip install codeforerunner # alternative
52
- ```
53
-
54
- From source:
55
-
56
- ```bash
57
- git clone https://github.com/derek-palmer/codeForerunner
58
- cd codeForerunner
59
- python -m pip install -e .
60
- ```
61
-
62
- Then `forerunner --help` should print the subcommand list.
63
-
64
- ## Prompt Layout
65
-
66
- ```text
67
- prompts/
68
- ├── system/
69
- │ └── base.md
70
- ├── partials/
71
- │ ├── context-format.md
72
- │ ├── output-rules.md
73
- │ └── stack-hints.md
74
- └── tasks/
75
- ├── scan.md
76
- ├── init-agent-onboarding.md
77
- ├── readme.md
78
- ├── api-docs.md
79
- ├── stack-docs.md
80
- ├── diagrams.md
81
- ├── flows.md
82
- ├── version-audit.md
83
- ├── check.md
84
- └── review.md
85
- ```
86
-
87
- ## Quick Start
88
-
89
- 1. Open `prompts/system/base.md` and use it as the agent system or project instruction.
90
- 2. Assemble repo context using the shape in `prompts/partials/context-format.md`.
91
- 3. For documentation generation, run `prompts/tasks/scan.md` first.
92
- 4. For agent onboarding only, run `prompts/tasks/init-agent-onboarding.md` directly.
93
- 5. Pass the scan result into one downstream documentation prompt, such as `prompts/tasks/readme.md` or `prompts/tasks/stack-docs.md`.
94
- 6. Apply generated docs only after checking that every claim is grounded in provided files.
95
-
96
- ## What The Prompts Do
97
-
98
- | Prompt | Purpose |
99
- | --- | --- |
100
- | `prompts/system/base.md` | Defines the codeforerunner role, quality bar, Markdown rules, and accuracy constraints. |
101
- | `prompts/tasks/scan.md` | Produces the first structured repo scan used by downstream tasks. |
102
- | `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`. |
103
- | `prompts/tasks/readme.md` | Generates or rewrites a top-level README from scan output and selected files. |
104
- | `prompts/tasks/api-docs.md` | Documents public APIs when endpoints/interfaces are evident. |
105
- | `prompts/tasks/stack-docs.md` | Documents stack-specific areas of a repo. |
106
- | `prompts/tasks/diagrams.md` | Generates Mermaid architecture or flow diagrams. |
107
- | `prompts/tasks/flows.md` | Documents user, request, job, or data flows. |
108
- | `prompts/tasks/version-audit.md` | Audits pinned versions from manifests, lockfiles, Dockerfiles, workflows, or IaC. |
109
- | `prompts/tasks/check.md` | Checks existing docs for staleness against a fresh scan. |
110
- | `prompts/tasks/review.md` | Summarizes documentation impact for review. |
111
-
112
- ## Docs And Spec
113
-
114
- - `SPEC.md` tracks phases, invariants, and tasks so future PRs can make small status updates instead of broad rewrites.
115
- - `docs/getting-started.md` explains manual prompt use.
116
- - `docs/prompt-guide.md` explains how system, partial, and task prompts compose.
117
- - `docs/editor-agent-setup.md` explains how to adapt prompts to local agents.
118
- - `docs/roadmap.md` mirrors the `SPEC.md` phase status in human-readable form.
119
- - `docs/agent-distribution-design.md` records the design that backs the Codex/Claude packages and `forerunner install`.
120
-
121
- ## Configuration
122
-
123
- `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.
124
-
125
- ### MCP Server
126
-
127
- `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).
128
-
129
- See `examples/mcp/` for Claude Desktop and mcp-cli wiring examples.
130
-
131
- ## Roadmap
132
-
133
- See `SPEC.md` for the canonical phase/task tracker and `docs/roadmap.md` for the human-readable roadmap.