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.
Files changed (28) hide show
  1. codeforerunner/__init__.py +6 -1
  2. codeforerunner/bundle.py +58 -0
  3. codeforerunner/cli.py +38 -48
  4. codeforerunner/installer.py +1 -3
  5. codeforerunner/mcp_server.py +18 -50
  6. codeforerunner/prompts/partials/context-format.md +29 -0
  7. codeforerunner/prompts/partials/output-rules.md +39 -0
  8. codeforerunner/prompts/partials/stack-hints.md +43 -0
  9. codeforerunner/prompts/system/base.md +38 -0
  10. codeforerunner/prompts/tasks/api-docs.md +47 -0
  11. codeforerunner/prompts/tasks/audit.md +44 -0
  12. codeforerunner/prompts/tasks/changelog.md +42 -0
  13. codeforerunner/prompts/tasks/check.md +61 -0
  14. codeforerunner/prompts/tasks/diagrams.md +66 -0
  15. codeforerunner/prompts/tasks/flows.md +55 -0
  16. codeforerunner/prompts/tasks/init-agent-onboarding.md +48 -0
  17. codeforerunner/prompts/tasks/readme.md +34 -0
  18. codeforerunner/prompts/tasks/review.md +48 -0
  19. codeforerunner/prompts/tasks/scan.md +92 -0
  20. codeforerunner/prompts/tasks/stack-docs.md +50 -0
  21. codeforerunner/prompts/tasks/version-audit.md +114 -0
  22. {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/METADATA +14 -1
  23. codeforerunner-0.3.1.dist-info/RECORD +36 -0
  24. codeforerunner-0.3.0.dist-info/RECORD +0 -19
  25. {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/WHEEL +0 -0
  26. {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/entry_points.txt +0 -0
  27. {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
  28. {codeforerunner-0.3.0.dist-info → codeforerunner-0.3.1.dist-info}/top_level.txt +0 -0
@@ -1 +1,6 @@
1
- __version__ = "0.2.0"
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("codeforerunner")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.0" # running from source without install
@@ -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 `prompts/`. See SPEC.md §D.cli."""
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 `prompts/system/base.md` + `prompts/partials/*.md` + `prompts/tasks/<task>.md` to stdout."""
32
- root = _repo_root(Path(args.repo) if args.repo else None)
33
- task_path = root / "prompts" / "tasks" / f"{args.task}.md"
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 (root / "forerunner.config.yaml").is_file()
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
- parts: list[str] = []
50
- base = root / "prompts" / "system" / "base.md"
51
- if base.is_file():
52
- parts.append(f"<!-- system: base.md -->\n{_read(base).rstrip()}")
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 `forerunner.config.yaml` present. Silent no-op otherwise."""
93
- try:
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
- root = _repo_root(Path(args.repo) if args.repo else None)
116
- return mcp_server.serve(root)
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
- root = _repo_root(Path(args.repo) if args.repo else None)
125
- cfg = load_from_repo(root)
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 = _repo_root(Path(args.repo) if args.repo else None)
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 (defaults to cwd ancestor with prompts/tasks/)")
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
 
@@ -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
- from codeforerunner.cli import _repo_root # local import to avoid cycle
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,
@@ -10,54 +10,23 @@ import sys
10
10
  from pathlib import Path
11
11
  from typing import Any, Iterable
12
12
 
13
- PROTOCOL_VERSION = "2024-11-05"
14
- SERVER_NAME = "codeforerunner"
15
- SERVER_VERSION = "0.2.0"
16
-
17
-
18
- def _repo_root(start: Path | None = None) -> Path:
19
- here = (start or Path.cwd()).resolve()
20
- for candidate in [here, *here.parents]:
21
- if (candidate / "prompts" / "tasks").is_dir():
22
- return candidate
23
- raise FileNotFoundError(
24
- "could not locate codeforerunner repo root (no prompts/tasks/ found upward)"
25
- )
26
-
27
-
28
- def _read(path: Path) -> str:
29
- return path.read_text(encoding="utf-8")
13
+ from codeforerunner import __version__ as _pkg_version
14
+ from codeforerunner.bundle import find_prompts_root, resolve_bundle
30
15
 
31
-
32
- def resolve_bundle(repo: Path, task: str) -> str:
33
- """Concatenate system/base.md + sorted partials/*.md + tasks/<task>.md with marker comments."""
34
- task_path = repo / "prompts" / "tasks" / f"{task}.md"
35
- if not task_path.is_file():
36
- raise FileNotFoundError(f"unknown task '{task}' (no {task_path})")
37
-
38
- parts: list[str] = []
39
- base = repo / "prompts" / "system" / "base.md"
40
- if base.is_file():
41
- parts.append(f"<!-- system: base.md -->\n{_read(base).rstrip()}")
42
-
43
- partials_dir = repo / "prompts" / "partials"
44
- if partials_dir.is_dir():
45
- for p in sorted(partials_dir.glob("*.md")):
46
- parts.append(f"<!-- partial: {p.name} -->\n{_read(p).rstrip()}")
47
-
48
- parts.append(f"<!-- task: {task_path.name} -->\n{_read(task_path).rstrip()}")
49
- return "\n\n".join(parts) + "\n"
16
+ PROTOCOL_VERSION = "2025-03-26"
17
+ SERVER_NAME = "codeforerunner"
18
+ SERVER_VERSION = _pkg_version
50
19
 
51
20
 
52
- def _list_tasks(repo: Path) -> list[Path]:
53
- tasks_dir = repo / "prompts" / "tasks"
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 '#' chars and whitespace."""
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(repo: Path) -> list[dict[str, Any]]:
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(repo)
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(repo: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
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(repo)})
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 = repo / "prompts" / "tasks" / f"{name}.md"
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(repo, name)
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(repo: Path, stdin: Iterable[str] = sys.stdin, stdout=sys.stdout, stderr=sys.stderr) -> int:
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(repo, msg, state)
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
- repo = _repo_root()
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(repo)
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
+ ```