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 +116 -23
- codeforerunner/cli.py +40 -3
- codeforerunner/doctor.py +32 -2
- codeforerunner/installer.py +91 -2
- codeforerunner/providers/__init__.py +2 -1
- codeforerunner/providers/anthropic.py +58 -1
- codeforerunner/providers/base.py +9 -1
- codeforerunner/providers/google.py +50 -0
- codeforerunner/providers/ollama.py +55 -0
- codeforerunner/providers/openai.py +54 -0
- codeforerunner-0.4.0.dist-info/METADATA +258 -0
- {codeforerunner-0.3.1.dist-info → codeforerunner-0.4.0.dist-info}/RECORD +16 -16
- codeforerunner-0.3.1.dist-info/METADATA +0 -133
- {codeforerunner-0.3.1.dist-info → codeforerunner-0.4.0.dist-info}/WHEEL +0 -0
- {codeforerunner-0.3.1.dist-info → codeforerunner-0.4.0.dist-info}/entry_points.txt +0 -0
- {codeforerunner-0.3.1.dist-info → codeforerunner-0.4.0.dist-info}/licenses/LICENSE.md +0 -0
- {codeforerunner-0.3.1.dist-info → codeforerunner-0.4.0.dist-info}/top_level.txt +0 -0
codeforerunner/check.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""Drift detection for docs
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
|
145
|
-
if
|
|
235
|
+
for rule in active_rules:
|
|
236
|
+
if rule.pattern.search(line):
|
|
146
237
|
violations.append(
|
|
147
|
-
Violation(path=doc, line=lineno, rule_id=
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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};
|
|
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
|
-
"
|
|
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] = []
|
codeforerunner/installer.py
CHANGED
|
@@ -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
|
|
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-
|
|
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()
|
codeforerunner/providers/base.py
CHANGED
|
@@ -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
|
+

|
|
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=
|
|
4
|
-
codeforerunner/cli.py,sha256=
|
|
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=
|
|
7
|
-
codeforerunner/installer.py,sha256=
|
|
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=
|
|
26
|
-
codeforerunner/providers/anthropic.py,sha256=
|
|
27
|
-
codeforerunner/providers/base.py,sha256=
|
|
28
|
-
codeforerunner/providers/google.py,sha256=
|
|
29
|
-
codeforerunner/providers/ollama.py,sha256=
|
|
30
|
-
codeforerunner/providers/openai.py,sha256=
|
|
31
|
-
codeforerunner-0.
|
|
32
|
-
codeforerunner-0.
|
|
33
|
-
codeforerunner-0.
|
|
34
|
-
codeforerunner-0.
|
|
35
|
-
codeforerunner-0.
|
|
36
|
-
codeforerunner-0.
|
|
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
|
-

|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|