codeforerunner 0.3.0__py3-none-any.whl → 0.3.1__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/__init__.py +6 -1
- codeforerunner/bundle.py +58 -0
- codeforerunner/cli.py +38 -48
- codeforerunner/installer.py +1 -3
- codeforerunner/mcp_server.py +18 -50
- codeforerunner/prompts/partials/context-format.md +29 -0
- codeforerunner/prompts/partials/output-rules.md +39 -0
- codeforerunner/prompts/partials/stack-hints.md +43 -0
- codeforerunner/prompts/system/base.md +38 -0
- codeforerunner/prompts/tasks/api-docs.md +47 -0
- codeforerunner/prompts/tasks/audit.md +44 -0
- codeforerunner/prompts/tasks/changelog.md +42 -0
- codeforerunner/prompts/tasks/check.md +61 -0
- codeforerunner/prompts/tasks/diagrams.md +66 -0
- codeforerunner/prompts/tasks/flows.md +55 -0
- codeforerunner/prompts/tasks/init-agent-onboarding.md +48 -0
- codeforerunner/prompts/tasks/readme.md +34 -0
- codeforerunner/prompts/tasks/review.md +48 -0
- codeforerunner/prompts/tasks/scan.md +92 -0
- codeforerunner/prompts/tasks/stack-docs.md +50 -0
- codeforerunner/prompts/tasks/version-audit.md +114 -0
- {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/METADATA +14 -1
- codeforerunner-0.3.1.dist-info/RECORD +36 -0
- codeforerunner-0.3.0.dist-info/RECORD +0 -19
- {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/WHEEL +0 -0
- {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/entry_points.txt +0 -0
- {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
- {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/top_level.txt +0 -0
codeforerunner/__init__.py
CHANGED
codeforerunner/bundle.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Shared prompt resolution used by cli.py and mcp_server.py."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _package_prompts() -> Path:
|
|
8
|
+
return Path(__file__).parent / "prompts"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def find_prompts_root(repo_arg: str | Path | None = None) -> Path:
|
|
12
|
+
"""Return the prompts root directory (parent of tasks/).
|
|
13
|
+
|
|
14
|
+
Resolution order:
|
|
15
|
+
1. {repo_arg}/prompts/ if given and contains tasks/
|
|
16
|
+
2. Walk up from cwd looking for prompts/tasks/ (checkout compat)
|
|
17
|
+
3. Package-bundled prompts (always available after pip install)
|
|
18
|
+
"""
|
|
19
|
+
if repo_arg is not None:
|
|
20
|
+
p = Path(repo_arg) / "prompts"
|
|
21
|
+
if (p / "tasks").is_dir():
|
|
22
|
+
return p
|
|
23
|
+
raise FileNotFoundError(
|
|
24
|
+
f"no prompts/tasks/ found under {str(repo_arg)!r}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
here = Path.cwd().resolve()
|
|
28
|
+
for candidate in [here, *here.parents]:
|
|
29
|
+
if (candidate / "prompts" / "tasks").is_dir():
|
|
30
|
+
return candidate / "prompts"
|
|
31
|
+
|
|
32
|
+
pkg = _package_prompts()
|
|
33
|
+
if (pkg / "tasks").is_dir():
|
|
34
|
+
return pkg
|
|
35
|
+
|
|
36
|
+
raise FileNotFoundError(
|
|
37
|
+
"could not find prompts/tasks/; specify --repo or reinstall the package"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_bundle(prompts_root: Path, task: str) -> str:
|
|
42
|
+
"""Concatenate system/base.md + sorted partials/*.md + tasks/<task>.md."""
|
|
43
|
+
task_path = prompts_root / "tasks" / f"{task}.md"
|
|
44
|
+
if not task_path.is_file():
|
|
45
|
+
raise FileNotFoundError(f"unknown task {task!r} (no {task_path})")
|
|
46
|
+
|
|
47
|
+
parts: list[str] = []
|
|
48
|
+
base = prompts_root / "system" / "base.md"
|
|
49
|
+
if base.is_file():
|
|
50
|
+
parts.append(f"<!-- system: base.md -->\n{base.read_text(encoding='utf-8').rstrip()}")
|
|
51
|
+
|
|
52
|
+
partials_dir = prompts_root / "partials"
|
|
53
|
+
if partials_dir.is_dir():
|
|
54
|
+
for p in sorted(partials_dir.glob("*.md")):
|
|
55
|
+
parts.append(f"<!-- partial: {p.name} -->\n{p.read_text(encoding='utf-8').rstrip()}")
|
|
56
|
+
|
|
57
|
+
parts.append(f"<!-- task: {task_path.name} -->\n{task_path.read_text(encoding='utf-8').rstrip()}")
|
|
58
|
+
return "\n\n".join(parts) + "\n"
|
codeforerunner/cli.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Thin CLI orchestration. Product logic lives in
|
|
1
|
+
"""Thin CLI orchestration. Product logic lives in prompts/. See SPEC.md §D.cli."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,36 +8,29 @@ import sys
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Sequence
|
|
10
10
|
|
|
11
|
+
from codeforerunner.bundle import find_prompts_root, resolve_bundle
|
|
12
|
+
|
|
11
13
|
SCAN_EXEMPT_TASKS = frozenset({"scan", "init-agent-onboarding"})
|
|
12
14
|
SCAN_DONE_ENV = "FORERUNNER_SCAN_DONE"
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
def _repo_root(start: Path | None = None) -> Path:
|
|
16
|
-
"""Walk up from cwd (or `start`) to a directory containing `prompts/tasks`."""
|
|
17
|
-
here = (start or Path.cwd()).resolve()
|
|
18
|
-
for candidate in [here, *here.parents]:
|
|
19
|
-
if (candidate / "prompts" / "tasks").is_dir():
|
|
20
|
-
return candidate
|
|
21
|
-
raise FileNotFoundError(
|
|
22
|
-
"could not locate codeforerunner repo root (no prompts/tasks/ found upward)"
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _read(path: Path) -> str:
|
|
27
|
-
return path.read_text(encoding="utf-8")
|
|
28
|
-
|
|
29
|
-
|
|
30
17
|
def cmd_doc(args: argparse.Namespace) -> int:
|
|
31
|
-
"""Resolve
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
"""Resolve base + partials + task bundle to stdout."""
|
|
19
|
+
try:
|
|
20
|
+
prompts_root = find_prompts_root(args.repo)
|
|
21
|
+
except FileNotFoundError as e:
|
|
22
|
+
print(f"error: {e}", file=sys.stderr)
|
|
23
|
+
return 2
|
|
24
|
+
|
|
25
|
+
task_path = prompts_root / "tasks" / f"{args.task}.md"
|
|
34
26
|
if not task_path.is_file():
|
|
35
27
|
print(f"error: unknown task '{args.task}' (no {task_path})", file=sys.stderr)
|
|
36
28
|
return 2
|
|
37
29
|
|
|
30
|
+
repo_root = Path(args.repo) if args.repo else Path.cwd()
|
|
38
31
|
if (
|
|
39
32
|
args.task not in SCAN_EXEMPT_TASKS
|
|
40
|
-
and (
|
|
33
|
+
and (repo_root / "forerunner.config.yaml").is_file()
|
|
41
34
|
and not os.environ.get(SCAN_DONE_ENV)
|
|
42
35
|
):
|
|
43
36
|
print(
|
|
@@ -46,18 +39,11 @@ def cmd_doc(args: argparse.Namespace) -> int:
|
|
|
46
39
|
file=sys.stderr,
|
|
47
40
|
)
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
partials_dir = root / "prompts" / "partials"
|
|
55
|
-
if partials_dir.is_dir():
|
|
56
|
-
for p in sorted(partials_dir.glob("*.md")):
|
|
57
|
-
parts.append(f"<!-- partial: {p.name} -->\n{_read(p).rstrip()}")
|
|
58
|
-
|
|
59
|
-
parts.append(f"<!-- task: {task_path.name} -->\n{_read(task_path).rstrip()}")
|
|
60
|
-
sys.stdout.write("\n\n".join(parts) + "\n")
|
|
42
|
+
try:
|
|
43
|
+
sys.stdout.write(resolve_bundle(prompts_root, args.task))
|
|
44
|
+
except FileNotFoundError as e:
|
|
45
|
+
print(f"error: {e}", file=sys.stderr)
|
|
46
|
+
return 2
|
|
61
47
|
return 0
|
|
62
48
|
|
|
63
49
|
|
|
@@ -89,11 +75,8 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
|
|
89
75
|
|
|
90
76
|
|
|
91
77
|
def cmd_check(args: argparse.Namespace) -> int:
|
|
92
|
-
"""Run check rules when
|
|
93
|
-
|
|
94
|
-
root = _repo_root(Path(args.repo) if args.repo else None)
|
|
95
|
-
except FileNotFoundError:
|
|
96
|
-
root = Path.cwd()
|
|
78
|
+
"""Run check rules when forerunner.config.yaml present. Silent no-op otherwise."""
|
|
79
|
+
root = Path(args.repo).resolve() if args.repo else Path.cwd()
|
|
97
80
|
from codeforerunner import check as _check
|
|
98
81
|
from codeforerunner.config import ConfigError, load_from_repo
|
|
99
82
|
try:
|
|
@@ -112,8 +95,12 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|
|
112
95
|
|
|
113
96
|
def cmd_mcp_server(args: argparse.Namespace) -> int:
|
|
114
97
|
from codeforerunner import mcp_server
|
|
115
|
-
|
|
116
|
-
|
|
98
|
+
try:
|
|
99
|
+
prompts_root = find_prompts_root(args.repo)
|
|
100
|
+
except FileNotFoundError as e:
|
|
101
|
+
print(f"mcp_server: {e}", file=sys.stderr)
|
|
102
|
+
return 2
|
|
103
|
+
return mcp_server.serve(prompts_root)
|
|
117
104
|
|
|
118
105
|
|
|
119
106
|
def cmd_generate(args: argparse.Namespace) -> int:
|
|
@@ -121,8 +108,8 @@ def cmd_generate(args: argparse.Namespace) -> int:
|
|
|
121
108
|
from codeforerunner import providers as _providers
|
|
122
109
|
from codeforerunner.config import load_from_repo
|
|
123
110
|
|
|
124
|
-
|
|
125
|
-
cfg = load_from_repo(
|
|
111
|
+
repo_root = Path(args.repo).resolve() if args.repo else Path.cwd()
|
|
112
|
+
cfg = load_from_repo(repo_root)
|
|
126
113
|
|
|
127
114
|
provider_name = args.provider or (cfg.provider if cfg else "anthropic")
|
|
128
115
|
model = args.model or (cfg.model if cfg else None)
|
|
@@ -133,7 +120,6 @@ def cmd_generate(args: argparse.Namespace) -> int:
|
|
|
133
120
|
import io as _io
|
|
134
121
|
buf = _io.StringIO()
|
|
135
122
|
ns = argparse.Namespace(repo=getattr(args, "repo", None), task=args.task)
|
|
136
|
-
# Temporarily redirect stdout to capture cmd_doc output.
|
|
137
123
|
real_stdout = sys.stdout
|
|
138
124
|
sys.stdout = buf
|
|
139
125
|
try:
|
|
@@ -146,10 +132,7 @@ def cmd_generate(args: argparse.Namespace) -> int:
|
|
|
146
132
|
env_var = (cfg.api_key_env.get(provider_name) if cfg else None) or provider.default_env_var
|
|
147
133
|
api_key = os.environ.get(env_var)
|
|
148
134
|
if api_key is None and provider_name != "ollama":
|
|
149
|
-
print(
|
|
150
|
-
f"error: missing API key; set ${env_var}",
|
|
151
|
-
file=sys.stderr,
|
|
152
|
-
)
|
|
135
|
+
print(f"error: missing API key; set ${env_var}", file=sys.stderr)
|
|
153
136
|
return 3
|
|
154
137
|
|
|
155
138
|
try:
|
|
@@ -168,7 +151,7 @@ def cmd_generate(args: argparse.Namespace) -> int:
|
|
|
168
151
|
|
|
169
152
|
def cmd_doctor(args: argparse.Namespace) -> int:
|
|
170
153
|
from codeforerunner import doctor
|
|
171
|
-
root =
|
|
154
|
+
root = Path(args.repo).resolve() if args.repo else Path.cwd()
|
|
172
155
|
findings = doctor.run(root)
|
|
173
156
|
sys.stdout.write(doctor.format_report(findings) + "\n")
|
|
174
157
|
return 1 if any(f.severity == "error" for f in findings) else 0
|
|
@@ -179,7 +162,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
179
162
|
prog="forerunner",
|
|
180
163
|
description="Prompt-first repo documentation tooling. Thin CLI; product logic in prompts/.",
|
|
181
164
|
)
|
|
182
|
-
p.add_argument("--repo", help="path to repo root
|
|
165
|
+
p.add_argument("--repo", default=argparse.SUPPRESS, help="path to repo root")
|
|
183
166
|
from codeforerunner import __version__ as _version
|
|
184
167
|
p.add_argument("--version", action="version", version=f"forerunner {_version}")
|
|
185
168
|
sub = p.add_subparsers(dest="cmd", required=True, metavar="<cmd>")
|
|
@@ -209,6 +192,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
209
192
|
s_check.set_defaults(func=cmd_check)
|
|
210
193
|
|
|
211
194
|
s_mcp = sub.add_parser("mcp-server", help="serve prompt bundles as MCP tools over stdio")
|
|
195
|
+
s_mcp.add_argument(
|
|
196
|
+
"--repo",
|
|
197
|
+
default=argparse.SUPPRESS,
|
|
198
|
+
help="path containing prompts/tasks/ (default: package-bundled prompts)",
|
|
199
|
+
)
|
|
212
200
|
s_mcp.set_defaults(func=cmd_mcp_server)
|
|
213
201
|
|
|
214
202
|
s_doctor = sub.add_parser("doctor", help="health report: skill parity + marketplace + installed dests")
|
|
@@ -229,6 +217,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
229
217
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
230
218
|
parser = build_parser()
|
|
231
219
|
args = parser.parse_args(argv)
|
|
220
|
+
if not hasattr(args, "repo"):
|
|
221
|
+
args.repo = None
|
|
232
222
|
return args.func(args)
|
|
233
223
|
|
|
234
224
|
|
codeforerunner/installer.py
CHANGED
|
@@ -291,9 +291,7 @@ def add_subparser(sub: argparse._SubParsersAction) -> None:
|
|
|
291
291
|
|
|
292
292
|
|
|
293
293
|
def _cli_entry(args: argparse.Namespace) -> int:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
root = _repo_root(Path(args.repo) if args.repo else None)
|
|
294
|
+
root = Path(args.repo).resolve() if args.repo else Path.cwd()
|
|
297
295
|
return install(
|
|
298
296
|
agent=args.agent,
|
|
299
297
|
repo_root=root,
|
codeforerunner/mcp_server.py
CHANGED
|
@@ -10,54 +10,23 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Iterable
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
SERVER_VERSION = "0.2.0"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _repo_root(start: Path | None = None) -> Path:
|
|
19
|
-
here = (start or Path.cwd()).resolve()
|
|
20
|
-
for candidate in [here, *here.parents]:
|
|
21
|
-
if (candidate / "prompts" / "tasks").is_dir():
|
|
22
|
-
return candidate
|
|
23
|
-
raise FileNotFoundError(
|
|
24
|
-
"could not locate codeforerunner repo root (no prompts/tasks/ found upward)"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _read(path: Path) -> str:
|
|
29
|
-
return path.read_text(encoding="utf-8")
|
|
13
|
+
from codeforerunner import __version__ as _pkg_version
|
|
14
|
+
from codeforerunner.bundle import find_prompts_root, resolve_bundle
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
task_path = repo / "prompts" / "tasks" / f"{task}.md"
|
|
35
|
-
if not task_path.is_file():
|
|
36
|
-
raise FileNotFoundError(f"unknown task '{task}' (no {task_path})")
|
|
37
|
-
|
|
38
|
-
parts: list[str] = []
|
|
39
|
-
base = repo / "prompts" / "system" / "base.md"
|
|
40
|
-
if base.is_file():
|
|
41
|
-
parts.append(f"<!-- system: base.md -->\n{_read(base).rstrip()}")
|
|
42
|
-
|
|
43
|
-
partials_dir = repo / "prompts" / "partials"
|
|
44
|
-
if partials_dir.is_dir():
|
|
45
|
-
for p in sorted(partials_dir.glob("*.md")):
|
|
46
|
-
parts.append(f"<!-- partial: {p.name} -->\n{_read(p).rstrip()}")
|
|
47
|
-
|
|
48
|
-
parts.append(f"<!-- task: {task_path.name} -->\n{_read(task_path).rstrip()}")
|
|
49
|
-
return "\n\n".join(parts) + "\n"
|
|
16
|
+
PROTOCOL_VERSION = "2025-03-26"
|
|
17
|
+
SERVER_NAME = "codeforerunner"
|
|
18
|
+
SERVER_VERSION = _pkg_version
|
|
50
19
|
|
|
51
20
|
|
|
52
|
-
def _list_tasks(
|
|
53
|
-
tasks_dir =
|
|
21
|
+
def _list_tasks(prompts_root: Path) -> list[Path]:
|
|
22
|
+
tasks_dir = prompts_root / "tasks"
|
|
54
23
|
if not tasks_dir.is_dir():
|
|
55
24
|
return []
|
|
56
25
|
return sorted(tasks_dir.glob("*.md"))
|
|
57
26
|
|
|
58
27
|
|
|
59
28
|
def _description_for(task_path: Path) -> str:
|
|
60
|
-
"""First non-empty markdown line, stripped of leading '#'
|
|
29
|
+
"""First non-empty markdown line, stripped of leading '#' and whitespace."""
|
|
61
30
|
for raw in task_path.read_text(encoding="utf-8").splitlines():
|
|
62
31
|
line = raw.strip()
|
|
63
32
|
if not line:
|
|
@@ -66,14 +35,14 @@ def _description_for(task_path: Path) -> str:
|
|
|
66
35
|
return task_path.stem
|
|
67
36
|
|
|
68
37
|
|
|
69
|
-
def _tools(
|
|
38
|
+
def _tools(prompts_root: Path) -> list[dict[str, Any]]:
|
|
70
39
|
return [
|
|
71
40
|
{
|
|
72
41
|
"name": p.stem,
|
|
73
42
|
"description": _description_for(p),
|
|
74
43
|
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
75
44
|
}
|
|
76
|
-
for p in _list_tasks(
|
|
45
|
+
for p in _list_tasks(prompts_root)
|
|
77
46
|
]
|
|
78
47
|
|
|
79
48
|
|
|
@@ -88,12 +57,11 @@ def _err(req_id: Any, code: int, message: str) -> dict[str, Any]:
|
|
|
88
57
|
SCAN_EXEMPT_TOOLS = frozenset({"init-agent-onboarding", "scan"})
|
|
89
58
|
|
|
90
59
|
|
|
91
|
-
def _handle(
|
|
60
|
+
def _handle(prompts_root: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
|
|
92
61
|
method = msg.get("method")
|
|
93
62
|
req_id = msg.get("id")
|
|
94
63
|
params = msg.get("params") or {}
|
|
95
64
|
|
|
96
|
-
# Notifications: no id, no response.
|
|
97
65
|
if method == "notifications/initialized":
|
|
98
66
|
return None
|
|
99
67
|
if req_id is None and isinstance(method, str) and method.startswith("notifications/"):
|
|
@@ -110,11 +78,11 @@ def _handle(repo: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str,
|
|
|
110
78
|
)
|
|
111
79
|
|
|
112
80
|
if method == "tools/list":
|
|
113
|
-
return _ok(req_id, {"tools": _tools(
|
|
81
|
+
return _ok(req_id, {"tools": _tools(prompts_root)})
|
|
114
82
|
|
|
115
83
|
if method == "tools/call":
|
|
116
84
|
name = params.get("name")
|
|
117
|
-
task_path =
|
|
85
|
+
task_path = prompts_root / "tasks" / f"{name}.md"
|
|
118
86
|
if not isinstance(name, str) or not task_path.is_file():
|
|
119
87
|
return _err(req_id, -32602, f"unknown tool: {name!r}")
|
|
120
88
|
if name not in SCAN_EXEMPT_TOOLS and not state.get("scan_called"):
|
|
@@ -126,7 +94,7 @@ def _handle(repo: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str,
|
|
|
126
94
|
if name == "scan":
|
|
127
95
|
state["scan_called"] = True
|
|
128
96
|
try:
|
|
129
|
-
text = resolve_bundle(
|
|
97
|
+
text = resolve_bundle(prompts_root, name)
|
|
130
98
|
except Exception as e: # pragma: no cover - defensive
|
|
131
99
|
return _err(req_id, -32603, f"internal error: {e}")
|
|
132
100
|
return _ok(
|
|
@@ -137,7 +105,7 @@ def _handle(repo: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str,
|
|
|
137
105
|
return _err(req_id, -32601, f"method not found: {method!r}")
|
|
138
106
|
|
|
139
107
|
|
|
140
|
-
def serve(
|
|
108
|
+
def serve(prompts_root: Path, stdin: Iterable[str] = sys.stdin, stdout=sys.stdout, stderr=sys.stderr) -> int:
|
|
141
109
|
state: dict[str, Any] = {"scan_called": False}
|
|
142
110
|
for raw in stdin:
|
|
143
111
|
line = raw.strip()
|
|
@@ -153,7 +121,7 @@ def serve(repo: Path, stdin: Iterable[str] = sys.stdin, stdout=sys.stdout, stder
|
|
|
153
121
|
continue
|
|
154
122
|
|
|
155
123
|
try:
|
|
156
|
-
resp = _handle(
|
|
124
|
+
resp = _handle(prompts_root, msg, state)
|
|
157
125
|
except Exception as e: # pragma: no cover - defensive
|
|
158
126
|
print(f"mcp_server: handler error: {e}", file=stderr)
|
|
159
127
|
resp = _err(msg.get("id"), -32603, f"internal error: {e}")
|
|
@@ -166,11 +134,11 @@ def serve(repo: Path, stdin: Iterable[str] = sys.stdin, stdout=sys.stdout, stder
|
|
|
166
134
|
|
|
167
135
|
def main(argv: list[str] | None = None) -> int:
|
|
168
136
|
try:
|
|
169
|
-
|
|
137
|
+
prompts_root = find_prompts_root()
|
|
170
138
|
except FileNotFoundError as e:
|
|
171
139
|
print(f"mcp_server: {e}", file=sys.stderr)
|
|
172
140
|
return 2
|
|
173
|
-
return serve(
|
|
141
|
+
return serve(prompts_root)
|
|
174
142
|
|
|
175
143
|
|
|
176
144
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Context Format — Partial
|
|
2
|
+
|
|
3
|
+
Context is passed as a single block immediately before the task prompt:
|
|
4
|
+
|
|
5
|
+
<context>
|
|
6
|
+
<repo_root>/path/to/repo</repo_root>
|
|
7
|
+
|
|
8
|
+
<file_tree>
|
|
9
|
+
.
|
|
10
|
+
├── src/
|
|
11
|
+
│ └── index.ts
|
|
12
|
+
└── package.json
|
|
13
|
+
</file_tree>
|
|
14
|
+
|
|
15
|
+
<files>
|
|
16
|
+
<file path="package.json">
|
|
17
|
+
{ "name": "my-app" }
|
|
18
|
+
</file>
|
|
19
|
+
</files>
|
|
20
|
+
</context>
|
|
21
|
+
|
|
22
|
+
## Rules for Context Assembly
|
|
23
|
+
|
|
24
|
+
1. File tree is always included — full tree respecting .gitignore and .forerunnerignore
|
|
25
|
+
2. File contents are selective — include only files relevant to the current task
|
|
26
|
+
3. File size limit — truncate at 300 lines; append <!-- truncated --> if cut
|
|
27
|
+
4. Binary files — never include contents; list in tree only
|
|
28
|
+
5. Secrets — never include .env files or credential files
|
|
29
|
+
6. Order — config/manifest files first, then entry points, then supporting modules
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Output Rules — Partial
|
|
2
|
+
|
|
3
|
+
## Markdown Standards
|
|
4
|
+
- Use ATX-style headers (#, ##, ###) — never underline style
|
|
5
|
+
- Fenced code blocks must always include a language identifier
|
|
6
|
+
- Bold (**text**) for key terms on first use only
|
|
7
|
+
- Tables must have header rows
|
|
8
|
+
|
|
9
|
+
## File Output Format
|
|
10
|
+
When writing to a specific file, the first line must be:
|
|
11
|
+
<!-- output: path/to/output/file.md -->
|
|
12
|
+
Followed immediately by the file content. No preamble.
|
|
13
|
+
|
|
14
|
+
## Accuracy Rules
|
|
15
|
+
- Never document a function, endpoint, or behavior not present in the provided code
|
|
16
|
+
- Version numbers must come from lock files or manifests — never guess
|
|
17
|
+
- Environment variables must come from .env.example or explicit code references
|
|
18
|
+
|
|
19
|
+
## Diagram Rules (Mermaid)
|
|
20
|
+
- All diagrams use Mermaid syntax in a fenced ```mermaid block
|
|
21
|
+
- Node IDs: no spaces, no special characters except _ and -
|
|
22
|
+
- Use graph TD for top-down, graph LR for left-right pipelines
|
|
23
|
+
- Use sequenceDiagram for request/response flows
|
|
24
|
+
- Use erDiagram for data models
|
|
25
|
+
- Label all edges with action verbs
|
|
26
|
+
- Max ~20 nodes per section diagram, ~40 for master
|
|
27
|
+
|
|
28
|
+
## Length Guidelines
|
|
29
|
+
| Document type | Target length |
|
|
30
|
+
|-------------------------|-----------------------------|
|
|
31
|
+
| README | 150-400 lines |
|
|
32
|
+
| API docs (per endpoint) | 20-60 lines |
|
|
33
|
+
| Stack doc | 100-300 lines |
|
|
34
|
+
| Master diagram | 30-60 lines of Mermaid |
|
|
35
|
+
| Section diagram | 10-25 lines of Mermaid |
|
|
36
|
+
| Flow doc | 50-150 lines |
|
|
37
|
+
| Version audit | scales with component count |
|
|
38
|
+
| Check report | 20-80 lines |
|
|
39
|
+
| Review summary | 10-30 lines |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Stack Detection Hints — Partial
|
|
2
|
+
|
|
3
|
+
## Detection Signals
|
|
4
|
+
|
|
5
|
+
### JavaScript / TypeScript
|
|
6
|
+
- package.json, tsconfig.json, node_modules/
|
|
7
|
+
- Frameworks: next.config.* -> Next.js, vite.config.* -> Vite, react in deps -> React
|
|
8
|
+
|
|
9
|
+
### Python
|
|
10
|
+
- pyproject.toml, setup.py, requirements.txt, Pipfile, uv.lock
|
|
11
|
+
- Frameworks: fastapi -> FastAPI, flask -> Flask, django -> Django, airflow -> Airflow
|
|
12
|
+
|
|
13
|
+
### Infrastructure / IaC
|
|
14
|
+
- *.tf -> Terraform
|
|
15
|
+
- .github/workflows/*.yml -> GitHub Actions
|
|
16
|
+
- docker-compose.yml, Dockerfile -> Docker
|
|
17
|
+
- kubernetes/, k8s/, *.yaml with kind: -> Kubernetes
|
|
18
|
+
|
|
19
|
+
### ETL / Data Pipelines
|
|
20
|
+
- dags/ -> Airflow
|
|
21
|
+
- dbt_project.yml -> dbt
|
|
22
|
+
- Prefect, Dagster, Luigi config files
|
|
23
|
+
|
|
24
|
+
### Go
|
|
25
|
+
- go.mod, go.sum; entry: main.go, cmd/
|
|
26
|
+
|
|
27
|
+
### Rust
|
|
28
|
+
- Cargo.toml, Cargo.lock; entry: src/main.rs, src/lib.rs
|
|
29
|
+
|
|
30
|
+
## Documentation Style by Stack
|
|
31
|
+
| Stack | README style | Diagram priority | Key sections |
|
|
32
|
+
|-------------------|-----------------------|-----------------------------|----------------------------------------|
|
|
33
|
+
| React/Next.js | User-facing + dev | Component tree, routes | Setup, env vars, routing, components |
|
|
34
|
+
| Python API | Developer-first | Endpoint flow, data model | Install, endpoints, auth, models |
|
|
35
|
+
| Terraform module | Ops-focused | Infrastructure topology | Inputs, outputs, resources, examples |
|
|
36
|
+
| ETL pipeline | Data engineer-focused | Data flow, DAG structure | Sources, transforms, targets, schedule |
|
|
37
|
+
| CLI tool | User-first | Command flow | Install, commands, flags, examples |
|
|
38
|
+
| Monorepo | Package-first | Package dependency graph | Packages, shared libs, dev workflow |
|
|
39
|
+
|
|
40
|
+
## Monorepo Detection
|
|
41
|
+
If the file tree contains multiple package.json, pyproject.toml, or equivalent manifest files
|
|
42
|
+
at different directory levels, treat as a monorepo. Document each package independently
|
|
43
|
+
and add a top-level dependency graph.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# codeforerunner — Base System Prompt
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
|
|
5
|
+
You are codeforerunner, an expert technical documentation agent. Your sole purpose is to analyze code repositories and produce accurate, well-structured, developer-friendly documentation.
|
|
6
|
+
|
|
7
|
+
You are not a general assistant. You are focused exclusively on understanding codebases and producing documentation that is:
|
|
8
|
+
- Accurate to what the code actually does — never hallucinate APIs, behavior, or integrations
|
|
9
|
+
- Concise and scannable, not verbose
|
|
10
|
+
- Consistently formatted using Markdown
|
|
11
|
+
- Targeted at developers onboarding to this repo
|
|
12
|
+
|
|
13
|
+
## Core Principles
|
|
14
|
+
|
|
15
|
+
### 1. Ground Truth is the Code
|
|
16
|
+
Always derive documentation from the provided file tree and file contents. If something is not evident from the provided context, say so explicitly — do not infer or invent.
|
|
17
|
+
|
|
18
|
+
### 2. Be Stack-Aware
|
|
19
|
+
Identify the technology stack, runtime, and ecosystem from the files provided. Adjust terminology, conventions, and documentation style to match the detected stack.
|
|
20
|
+
|
|
21
|
+
### 3. Output is Always Markdown
|
|
22
|
+
All outputs are valid Markdown. Use fenced code blocks with language identifiers. Use tables for comparisons. Use headers hierarchically.
|
|
23
|
+
|
|
24
|
+
### 4. Never Pad
|
|
25
|
+
Do not add motivational language, marketing copy, generic disclaimers, or filler sentences. Every sentence must carry information.
|
|
26
|
+
|
|
27
|
+
### 5. Scope Boundaries
|
|
28
|
+
Only document what is in scope for the current task.
|
|
29
|
+
|
|
30
|
+
### 6. Acknowledge Gaps
|
|
31
|
+
If context is insufficient, produce the best output possible and append a ## Gaps section.
|
|
32
|
+
|
|
33
|
+
## Behavior Constraints
|
|
34
|
+
- Do not ask clarifying questions mid-task unless explicitly instructed
|
|
35
|
+
- Do not produce partial outputs with placeholder notes
|
|
36
|
+
- Do not wrap output in meta-commentary
|
|
37
|
+
- Begin your response with the documentation content directly
|
|
38
|
+
- If a file path is specified as the output target, begin with: <!-- output: path/to/file.md -->
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Task: Generate API Documentation
|
|
2
|
+
|
|
3
|
+
Generates endpoint-level API documentation.
|
|
4
|
+
Requires scan result. Best results when all route files are in context.
|
|
5
|
+
|
|
6
|
+
## Input
|
|
7
|
+
- Scan result
|
|
8
|
+
- Route/handler files (all)
|
|
9
|
+
- Auth middleware files
|
|
10
|
+
- Request/response model/schema files
|
|
11
|
+
|
|
12
|
+
## Per-Endpoint Format
|
|
13
|
+
|
|
14
|
+
### `METHOD /path/to/endpoint`
|
|
15
|
+
|
|
16
|
+
**Description:** What this endpoint does.
|
|
17
|
+
**Auth:** Required / None / Optional
|
|
18
|
+
|
|
19
|
+
**Request**
|
|
20
|
+
| Field | Location | Type | Required | Description |
|
|
21
|
+
|---|---|---|---|---|
|
|
22
|
+
|
|
23
|
+
**Request Example**
|
|
24
|
+
```http
|
|
25
|
+
POST /api/users
|
|
26
|
+
Authorization: Bearer <token>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Response**
|
|
30
|
+
| Status | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| 200 | Success |
|
|
33
|
+
| 401 | Unauthorized |
|
|
34
|
+
|
|
35
|
+
**Response Example**
|
|
36
|
+
```json
|
|
37
|
+
{ "id": "uuid" }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Rules
|
|
41
|
+
- Document every endpoint, do not skip any
|
|
42
|
+
- Group by resource/router
|
|
43
|
+
- Include a summary table at the top: method, path, auth, one-line description
|
|
44
|
+
- Claims must derive from provided files. Document every endpoint with available evidence; put unevidenced fields or unknown details in `## Gaps`.
|
|
45
|
+
|
|
46
|
+
## Output
|
|
47
|
+
<!-- output: docs/api.md -->
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Task: Security and Dependency Audit
|
|
2
|
+
|
|
3
|
+
Produces a structured security and dependency audit report for the repository.
|
|
4
|
+
Requires scan result as input.
|
|
5
|
+
|
|
6
|
+
## Input
|
|
7
|
+
- Scan result
|
|
8
|
+
- Manifest/lockfiles: package.json, package-lock.json, pyproject.toml, poetry.lock, go.mod, go.sum, Cargo.toml, Cargo.lock, requirements*.txt, Gemfile.lock
|
|
9
|
+
- CI/CD workflow files
|
|
10
|
+
- Dockerfile / compose files if present
|
|
11
|
+
- .env.example or documented env vars
|
|
12
|
+
|
|
13
|
+
## Instructions
|
|
14
|
+
|
|
15
|
+
1. **Dependency inventory** — list all direct dependencies with pinned version (or "unpinned" if range only)
|
|
16
|
+
2. **Known-vulnerable versions** — flag any dependency version with a published CVE you know of; note CVE ID and severity
|
|
17
|
+
3. **Outdated pins** — flag dependencies pinned to versions that are end-of-life or significantly behind latest stable
|
|
18
|
+
4. **Supply-chain risks** — flag: unpinned versions in CI (`uses: action@main`), `curl | bash` install patterns, unverified download steps
|
|
19
|
+
5. **Secrets surface** — enumerate all env vars the repo reads; flag any that look like secrets hardcoded in non-.env files
|
|
20
|
+
6. **Auth / input validation** — from entry-point and API handler files, flag: missing input sanitisation, direct string interpolation into shell commands or SQL, unauthenticated endpoints
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
- Claims must derive from provided files. If evidence is absent, omit or document in `## Gaps`.
|
|
24
|
+
- Do not fabricate CVE IDs. If you are not certain a CVE applies, note the concern without a CVE reference.
|
|
25
|
+
- Severity: CRITICAL / HIGH / MEDIUM / LOW / INFO.
|
|
26
|
+
- Only report findings with evidence; do not speculate.
|
|
27
|
+
|
|
28
|
+
## Output Format
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
## Dependency Inventory
|
|
32
|
+
| Package | Version | Pinned? |
|
|
33
|
+
|---------|---------|---------|
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
## Findings
|
|
37
|
+
### [SEVERITY] Title
|
|
38
|
+
- **Evidence**: file:line or package@version
|
|
39
|
+
- **Detail**: what the risk is
|
|
40
|
+
- **Remediation**: concrete fix
|
|
41
|
+
|
|
42
|
+
## Gaps
|
|
43
|
+
- Items not assessable from provided files
|
|
44
|
+
```
|