codeforerunner 0.4.3__py3-none-any.whl → 0.4.4__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/bundle.py CHANGED
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
 
7
7
  def _package_prompts() -> Path:
8
+ """Return the path to the bundled prompts directory inside the package."""
8
9
  return Path(__file__).parent / "prompts"
9
10
 
10
11
 
codeforerunner/check.py CHANGED
@@ -21,6 +21,8 @@ class Violation:
21
21
 
22
22
  @dataclass(frozen=True)
23
23
  class _Rule:
24
+ """Drift detection rule: pattern to match, trigger files, and violation message."""
25
+
24
26
  id: str
25
27
  pattern: re.Pattern
26
28
  triggers: tuple[str, ...]
@@ -124,6 +126,7 @@ _CHANGELOG_FILENAME = "CHANGELOG.md"
124
126
 
125
127
 
126
128
  def _trigger_exists(repo: Path, patterns: tuple[str, ...]) -> bool:
129
+ """Return True if any pattern matches an existing file in repo."""
127
130
  for pat in patterns:
128
131
  if "*" in pat:
129
132
  parent = repo / Path(pat).parent
@@ -137,6 +140,7 @@ def _trigger_exists(repo: Path, patterns: tuple[str, ...]) -> bool:
137
140
 
138
141
 
139
142
  def _scanned_docs(repo: Path) -> list[Path]:
143
+ """Collect README.md and all *.md files under docs/ from repo."""
140
144
  docs: list[Path] = []
141
145
  readme = repo / "README.md"
142
146
  if readme.is_file():
@@ -148,6 +152,7 @@ def _scanned_docs(repo: Path) -> list[Path]:
148
152
 
149
153
 
150
154
  def _path_ignored(repo: Path, doc: Path, ignore_patterns: tuple[str, ...]) -> bool:
155
+ """Return True if doc's repo-relative path matches any ignore pattern."""
151
156
  if not ignore_patterns:
152
157
  return False
153
158
  try:
@@ -158,6 +163,7 @@ def _path_ignored(repo: Path, doc: Path, ignore_patterns: tuple[str, ...]) -> bo
158
163
 
159
164
 
160
165
  def _current_version(repo: Path) -> str | None:
166
+ """Extract the package version from pyproject.toml, or None if absent/unparseable."""
161
167
  pyproject = repo / "pyproject.toml"
162
168
  if not pyproject.is_file():
163
169
  return None
@@ -175,6 +181,7 @@ def _check_version_drift(
175
181
  ignore_patterns: tuple[str, ...],
176
182
  enabled: set[str] | None,
177
183
  ) -> list[Violation]:
184
+ """Scan docs for pinned version strings that don't match pyproject.toml."""
178
185
  if enabled is not None and "RV1-version-drift" not in enabled:
179
186
  return []
180
187
  current = _current_version(repo)
codeforerunner/cli.py CHANGED
@@ -56,6 +56,7 @@ def cmd_doc(args: argparse.Namespace) -> int:
56
56
 
57
57
 
58
58
  def _doc_for(args: argparse.Namespace, task: str) -> int:
59
+ """Emit bundle for *task* by delegating to cmd_doc with a synthetic Namespace."""
59
60
  ns = argparse.Namespace(repo=getattr(args, "repo", None), task=task)
60
61
  return cmd_doc(ns)
61
62
 
@@ -113,85 +114,17 @@ def cmd_mcp_server(args: argparse.Namespace) -> int:
113
114
  return mcp_server.serve(prompts_root)
114
115
 
115
116
 
116
- def cmd_generate(args: argparse.Namespace) -> int:
117
- """Resolve the bundle for <task> and send it to the configured provider.
118
-
119
- With --prompt-only (or when no provider/key/Ollama is reachable), outputs
120
- the assembled prompt bundle to stdout for the calling agent to process.
121
- """
122
- from codeforerunner import providers as _providers
123
- from codeforerunner.config import load_from_repo
124
-
125
- repo_root = Path(args.repo).resolve() if args.repo else Path.cwd()
126
- cfg = load_from_repo(repo_root)
127
-
128
- ns = argparse.Namespace(repo=getattr(args, "repo", None), task=args.task)
129
-
130
- # --prompt-only: output the bundle and stop; the calling agent is the model.
131
- if getattr(args, "prompt_only", False):
132
- return cmd_doc(ns)
133
-
134
- bundle, rc = _get_bundle(ns)
135
- if rc != 0:
136
- return rc
137
-
138
-
139
- explicit_provider = args.provider or (cfg.provider if cfg else None)
140
- provider_name = explicit_provider or "anthropic"
141
- model = args.model or (cfg.model if cfg else None)
142
- provider_cls = _providers.get(provider_name)
143
- provider = provider_cls()
144
- model = model or provider.default_model
145
-
146
- env_var = (cfg.api_key_env.get(provider_name) if cfg else None) or provider.default_env_var
147
- api_key = os.environ.get(env_var)
148
- if api_key is None and provider_name != "ollama":
149
- if explicit_provider is None and _providers.ollama_available():
150
- provider_name = "ollama"
151
- provider_cls = _providers.get("ollama")
152
- provider = provider_cls()
153
- if not args.model:
154
- model = provider.default_model
155
- print("info: no API key; falling back to Ollama (local mode)", file=sys.stderr)
156
- elif explicit_provider is None:
157
- # Skill-mode auto-detect: no provider configured, no Ollama — output
158
- # the prompt bundle for the calling agent to process directly.
159
- sys.stdout.write(bundle)
160
- if sys.stdout.isatty():
161
- print(
162
- "\ninfo: no provider configured and Ollama not running.\n"
163
- " Prompt bundle written above — paste into your agent,\n"
164
- " or run: forerunner generate --prompt-only "
165
- f"{args.task}",
166
- file=sys.stderr,
167
- )
168
- return 0
169
- else:
170
- print(f"error: missing API key; set ${env_var}", file=sys.stderr)
171
- return 3
172
-
173
- if getattr(args, "stream", False):
174
- try:
175
- for chunk in provider.stream(prompt=bundle, model=model, api_key=api_key):
176
- sys.stdout.write(chunk)
177
- sys.stdout.flush()
178
- except _providers.ProviderError as e:
179
- print(f"error: {provider_name} provider failed: {e}", file=sys.stderr)
180
- return 4
181
- sys.stdout.write("\n")
182
- return 0
183
-
184
- try:
185
- result = provider.complete(prompt=bundle, model=model, api_key=api_key)
186
- except _providers.ProviderError as e:
187
- print(f"error: {provider_name} provider failed: {e}", file=sys.stderr)
188
- return 4
189
-
190
- sys.stdout.write(result.text.rstrip() + "\n")
191
- print(
192
- f"# {provider_name} {result.model} {result.usage or ''}".rstrip(),
193
- file=sys.stderr,
194
- )
117
+ def cmd_refresh(args: argparse.Namespace) -> int:
118
+ """Emit scan + check + all doc-task bundles to stdout for a full doc refresh."""
119
+ tasks = ["scan", "check", "readme", "api-docs", "stack-docs",
120
+ "diagrams", "flows", "version-audit", "audit"]
121
+ for i, task in enumerate(tasks):
122
+ ns = argparse.Namespace(repo=getattr(args, "repo", None), task=task)
123
+ rc = cmd_doc(ns)
124
+ if rc != 0:
125
+ return rc
126
+ if i < len(tasks) - 1:
127
+ sys.stdout.write("\n---\n\n")
195
128
  return 0
196
129
 
197
130
 
@@ -270,18 +203,8 @@ def build_parser() -> argparse.ArgumentParser:
270
203
  )
271
204
  s_doctor.set_defaults(func=cmd_doctor)
272
205
 
273
- s_gen = sub.add_parser("generate", help="resolve bundle for <task> and call the configured provider")
274
- s_gen.add_argument("task", help="task basename under prompts/tasks/")
275
- s_gen.add_argument("--provider", help="override config provider")
276
- s_gen.add_argument("--model", help="override config model")
277
- s_gen.add_argument("--stream", action="store_true", help="stream output token-by-token")
278
- s_gen.add_argument(
279
- "--prompt-only",
280
- dest="prompt_only",
281
- action="store_true",
282
- help="output the assembled prompt bundle to stdout; do not call a model (skill mode)",
283
- )
284
- s_gen.set_defaults(func=cmd_generate)
206
+ s_refresh = sub.add_parser("refresh", help="output all doc-refresh bundles in sequence (scan + check + all tasks)")
207
+ s_refresh.set_defaults(func=cmd_refresh)
285
208
 
286
209
  from codeforerunner import installer
287
210
  installer.add_subparser(sub)
codeforerunner/config.py CHANGED
@@ -3,13 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
+ from pathlib import Path
6
7
  from typing import Any
7
8
 
8
9
  import yaml
9
10
 
10
11
  CONFIG_FILENAME = "forerunner.config.yaml"
11
12
 
12
- _KNOWN_PROVIDERS = {"anthropic", "openai", "google", "ollama"}
13
13
  _KNOWN_SEVERITIES = {"HIGH", "MEDIUM", "LOW"}
14
14
 
15
15
 
@@ -40,16 +40,14 @@ class VersionAuditConfig:
40
40
  class ForerunnerConfig:
41
41
  """Top-level forerunner.config.yaml configuration."""
42
42
 
43
- provider: str = "anthropic"
44
- model: str = "claude-opus-4-7"
45
43
  approaching_eol_threshold_months: int = 6
46
44
  ignore_patterns: tuple[str, ...] = ()
47
- api_key_env: dict[str, str] = field(default_factory=dict)
48
45
  check: CheckConfig = field(default_factory=CheckConfig)
49
46
  version_audit: VersionAuditConfig = field(default_factory=VersionAuditConfig)
50
47
 
51
48
 
52
49
  def _require_type(value: Any, expected: type, field_name: str) -> Any:
50
+ """Raise ConfigError if value is not an instance of expected."""
53
51
  if not isinstance(value, expected):
54
52
  raise ConfigError(
55
53
  f"{field_name}: expected {expected.__name__}, got {type(value).__name__}"
@@ -58,7 +56,8 @@ def _require_type(value: Any, expected: type, field_name: str) -> Any:
58
56
 
59
57
 
60
58
  def _coerce_str_tuple(value: Any, field_name: str) -> tuple[str, ...]:
61
- if value is None: # pragma: no cover - callers supply defaults, never pass None
59
+ """Coerce a list of strings to a tuple, raising ConfigError on bad input."""
60
+ if value is None:
62
61
  return ()
63
62
  if not isinstance(value, list):
64
63
  raise ConfigError(f"{field_name}: expected list, got {type(value).__name__}")
@@ -70,30 +69,8 @@ def _coerce_str_tuple(value: Any, field_name: str) -> tuple[str, ...]:
70
69
  return tuple(out)
71
70
 
72
71
 
73
- def _parse_api_key_env(raw: Any) -> dict[str, str]:
74
- if raw is None:
75
- return {}
76
- if not isinstance(raw, dict):
77
- raise ConfigError(f"api_key_env: expected dict, got {type(raw).__name__}")
78
- out: dict[str, str] = {}
79
- for k, v in raw.items():
80
- if not isinstance(k, str): # pragma: no cover - YAML keys are always strings
81
- raise ConfigError(
82
- f"api_key_env: keys must be strings, got {type(k).__name__}"
83
- )
84
- if k not in _KNOWN_PROVIDERS:
85
- raise ConfigError(
86
- f"api_key_env: unknown provider '{k}' (expected one of {sorted(_KNOWN_PROVIDERS)})"
87
- )
88
- if not isinstance(v, str) or not v:
89
- raise ConfigError(
90
- f"api_key_env[{k}]: expected non-empty string, got {type(v).__name__}"
91
- )
92
- out[k] = v
93
- return out
94
-
95
-
96
72
  def _parse_check(raw: Any) -> CheckConfig:
73
+ """Parse the tasks.check mapping into a CheckConfig."""
97
74
  if raw is None:
98
75
  return CheckConfig()
99
76
  _require_type(raw, dict, "tasks.check")
@@ -120,6 +97,7 @@ def _parse_check(raw: Any) -> CheckConfig:
120
97
 
121
98
 
122
99
  def _to_int(value: Any, field_name: str) -> int:
100
+ """Convert value to int, raising ConfigError on failure."""
123
101
  try:
124
102
  return int(value)
125
103
  except (TypeError, ValueError) as e:
@@ -127,6 +105,7 @@ def _to_int(value: Any, field_name: str) -> int:
127
105
 
128
106
 
129
107
  def _parse_version_audit(raw: Any) -> VersionAuditConfig:
108
+ """Parse the tasks.version_audit mapping into a VersionAuditConfig."""
130
109
  if raw is None:
131
110
  return VersionAuditConfig()
132
111
  _require_type(raw, dict, "tasks.version_audit")
@@ -143,24 +122,21 @@ def parse(raw: dict[str, Any] | None) -> ForerunnerConfig:
143
122
  return ForerunnerConfig()
144
123
  _require_type(raw, dict, "<root>")
145
124
 
146
- provider = raw.get("provider", "anthropic")
147
- _require_type(provider, str, "provider")
148
- if provider not in _KNOWN_PROVIDERS:
125
+ tasks_raw = raw.get("tasks")
126
+ tasks = tasks_raw if tasks_raw is not None else {}
127
+ _require_type(tasks, dict, "tasks")
128
+
129
+ eol_months = _to_int(
130
+ raw.get("approaching_eol_threshold_months", 6), "approaching_eol_threshold_months"
131
+ )
132
+ if eol_months <= 0:
149
133
  raise ConfigError(
150
- f"provider: unknown '{provider}' (expected one of {sorted(_KNOWN_PROVIDERS)})"
134
+ f"approaching_eol_threshold_months: must be a positive integer, got {eol_months}"
151
135
  )
152
136
 
153
- tasks = raw.get("tasks") or {}
154
- _require_type(tasks, dict, "tasks")
155
-
156
137
  return ForerunnerConfig(
157
- provider=provider,
158
- model=_require_type(raw.get("model", "claude-opus-4-7"), str, "model"),
159
- approaching_eol_threshold_months=_to_int(
160
- raw.get("approaching_eol_threshold_months", 6), "approaching_eol_threshold_months"
161
- ),
138
+ approaching_eol_threshold_months=eol_months,
162
139
  ignore_patterns=_coerce_str_tuple(raw.get("ignore_patterns", []), "ignore_patterns"),
163
- api_key_env=_parse_api_key_env(raw.get("api_key_env")),
164
140
  check=_parse_check(tasks.get("check")),
165
141
  version_audit=_parse_version_audit(tasks.get("version_audit")),
166
142
  )
codeforerunner/doctor.py CHANGED
@@ -23,13 +23,6 @@ MARKETPLACE_REL = Path("plugins/codex/marketplace.json")
23
23
  MARKER_BEGIN = "<!-- forerunner:begin managed=codeforerunner.skill -->"
24
24
  MARKER_END = "<!-- forerunner:end -->"
25
25
 
26
- _DEFAULT_PROVIDER_ENV = {
27
- "anthropic": "ANTHROPIC_API_KEY",
28
- "openai": "OPENAI_API_KEY",
29
- "google": "GOOGLE_API_KEY",
30
- "ollama": "OLLAMA_HOST",
31
- }
32
-
33
26
 
34
27
  @dataclass(frozen=True)
35
28
  class Finding:
@@ -41,6 +34,7 @@ class Finding:
41
34
 
42
35
 
43
36
  def _installed_skill_destinations() -> list[Path]:
37
+ """Return default install paths for the codeforerunner skill across supported agents."""
44
38
  home = Path(os.path.expanduser("~"))
45
39
  return [
46
40
  home / ".codex/skills/codeforerunner/SKILL.md",
@@ -49,10 +43,12 @@ def _installed_skill_destinations() -> list[Path]:
49
43
 
50
44
 
51
45
  def _installed_marketplace_destination() -> Path:
46
+ """Return default install path for the Codex marketplace manifest."""
52
47
  return Path(os.path.expanduser("~")) / ".codex/marketplaces/codeforerunner.json"
53
48
 
54
49
 
55
50
  def _load_script_module(repo: Path, relpath: str, module_name: str):
51
+ """Load a Python script from the repo as a module with a unique name to avoid cache collisions."""
56
52
  # L3: unique name prevents stale cached module on repeated calls
57
53
  unique_name = f"{module_name}_{uuid.uuid4().hex}"
58
54
  script_path = repo / relpath
@@ -66,6 +62,7 @@ def _load_script_module(repo: Path, relpath: str, module_name: str):
66
62
 
67
63
 
68
64
  def _check_skill_body_parity(repo: Path, run_scripts: bool = False) -> list[Finding]:
65
+ """Verify that all distributed skill copies match the canonical body."""
69
66
  if not run_scripts:
70
67
  return [
71
68
  Finding(
@@ -118,6 +115,7 @@ def _check_skill_body_parity(repo: Path, run_scripts: bool = False) -> list[Find
118
115
 
119
116
 
120
117
  def _check_codex_marketplace(repo: Path, run_scripts: bool = False) -> list[Finding]:
118
+ """Validate the Codex marketplace manifest using the repo validation script."""
121
119
  if not run_scripts:
122
120
  return [
123
121
  Finding(
@@ -145,6 +143,7 @@ def _check_codex_marketplace(repo: Path, run_scripts: bool = False) -> list[Find
145
143
 
146
144
 
147
145
  def _check_installed_destinations(repo: Path) -> list[Finding]:
146
+ """Check whether installed skill and marketplace files are present and managed."""
148
147
  findings: list[Finding] = []
149
148
 
150
149
  for dest in _installed_skill_destinations():
@@ -229,6 +228,7 @@ def _check_installed_destinations(repo: Path) -> list[Finding]:
229
228
 
230
229
 
231
230
  def _check_config_loadable(repo: Path) -> list[Finding]:
231
+ """Try parsing forerunner.config.yaml; report error finding on ConfigError."""
232
232
  cfg_path = repo / CONFIG_FILENAME
233
233
  if not cfg_path.is_file():
234
234
  return [
@@ -245,89 +245,6 @@ def _check_config_loadable(repo: Path) -> list[Finding]:
245
245
  return [Finding("ok", "config-loadable", f"{CONFIG_FILENAME} parses cleanly")]
246
246
 
247
247
 
248
- def _skill_mode_active() -> bool:
249
- """Return True if any installed skill destination has managed markers (agent is the model)."""
250
- for dest in _installed_skill_destinations():
251
- if dest.exists():
252
- try:
253
- text = dest.read_text(encoding="utf-8")
254
- if MARKER_BEGIN in text and MARKER_END in text:
255
- return True
256
- except OSError:
257
- pass
258
- return False
259
-
260
-
261
- def _check_provider_api_key(repo: Path) -> list[Finding]:
262
- from codeforerunner.providers.ollama import is_available as _ollama_available
263
-
264
- cfg_path = repo / CONFIG_FILENAME
265
- if not cfg_path.is_file():
266
- if _ollama_available():
267
- return [
268
- Finding(
269
- "ok",
270
- "provider-api-key",
271
- "no config; Ollama running — generate will use local mode automatically",
272
- )
273
- ]
274
- if _skill_mode_active():
275
- return [
276
- Finding(
277
- "ok",
278
- "provider-api-key",
279
- "no config; skill mode active — the installed agent is the model, no API key needed",
280
- )
281
- ]
282
- return [
283
- Finding(
284
- "ok",
285
- "provider-api-key",
286
- f"no {CONFIG_FILENAME}; set an API key in config, start Ollama, or use skill mode (`forerunner generate --prompt-only`)",
287
- )
288
- ]
289
- try:
290
- cfg = load_from_repo(repo)
291
- except ConfigError:
292
- # config-loadable check will surface this; skip here
293
- return [
294
- Finding(
295
- "ok",
296
- "provider-api-key",
297
- "config unparseable; skipped (see config-loadable)",
298
- )
299
- ]
300
- if cfg is None: # pragma: no cover - defensive
301
- return [
302
- Finding(
303
- "ok",
304
- "provider-api-key",
305
- f"no {CONFIG_FILENAME}; provider key not checked",
306
- )
307
- ]
308
- provider = cfg.provider
309
- if provider == "ollama":
310
- return [
311
- Finding(
312
- "ok",
313
- "provider-api-key",
314
- "running in local mode (Ollama; no API key needed)",
315
- )
316
- ]
317
- env_var = cfg.api_key_env.get(provider) or _DEFAULT_PROVIDER_ENV.get(provider, "")
318
- if os.environ.get(env_var):
319
- return [Finding("ok", "provider-api-key", f"{provider}: {env_var} is set")]
320
- # Emit the same warning regardless of skill mode: an explicit provider in config
321
- # means the user intends to use that provider, so a missing key is always a real problem.
322
- return [
323
- Finding(
324
- "warn",
325
- "provider-api-key",
326
- f"{provider}: ${env_var} is not set; `forerunner generate` will refuse to run",
327
- )
328
- ]
329
-
330
-
331
248
  _STARTER_CONFIG = """\
332
249
  # forerunner.config.yaml — generated by `forerunner doctor --fix`
333
250
  # See https://github.com/derek-palmer/codeforerunner for docs.
@@ -359,7 +276,6 @@ def run(repo: Path, run_scripts: bool = False) -> list[Finding]:
359
276
  findings.extend(_check_codex_marketplace(repo, run_scripts=run_scripts))
360
277
  findings.extend(_check_installed_destinations(repo))
361
278
  findings.extend(_check_config_loadable(repo))
362
- findings.extend(_check_provider_api_key(repo))
363
279
  return findings
364
280
 
365
281
 
@@ -48,6 +48,7 @@ class Target:
48
48
 
49
49
 
50
50
  def _home() -> Path:
51
+ """Return the current user's home directory as a Path."""
51
52
  return Path(os.path.expanduser("~"))
52
53
 
53
54
 
@@ -164,10 +165,12 @@ def extract_frontmatter(text: str) -> str:
164
165
 
165
166
 
166
167
  def _hash(s: str) -> str:
168
+ """Return SHA-256 hex digest of a UTF-8 encoded string."""
167
169
  return hashlib.sha256(s.encode("utf-8")).hexdigest()
168
170
 
169
171
 
170
172
  def _hash_bytes(b: bytes) -> str:
173
+ """Return SHA-256 hex digest of raw bytes."""
171
174
  return hashlib.sha256(b).hexdigest()
172
175
 
173
176
 
@@ -383,6 +386,7 @@ def add_subparser(sub: argparse._SubParsersAction) -> None:
383
386
 
384
387
 
385
388
  def _cli_entry(args: argparse.Namespace) -> int:
389
+ """Dispatch `forerunner install` subcommand from parsed CLI args."""
386
390
  root = Path(args.repo).resolve() if args.repo else Path.cwd()
387
391
 
388
392
  if getattr(args, "all", False):
@@ -19,6 +19,7 @@ SERVER_VERSION = _pkg_version
19
19
 
20
20
 
21
21
  def _list_tasks(prompts_root: Path) -> list[Path]:
22
+ """Return sorted list of task *.md paths under prompts_root/tasks/."""
22
23
  tasks_dir = prompts_root / "tasks"
23
24
  if not tasks_dir.is_dir():
24
25
  return []
@@ -37,6 +38,7 @@ def _description_for(task_path: Path) -> str:
37
38
 
38
39
 
39
40
  def _tools(prompts_root: Path) -> list[dict[str, Any]]:
41
+ """Build MCP tools/list payload from all task files in prompts_root."""
40
42
  return [
41
43
  {
42
44
  "name": p.stem,
@@ -48,10 +50,12 @@ def _tools(prompts_root: Path) -> list[dict[str, Any]]:
48
50
 
49
51
 
50
52
  def _ok(req_id: Any, result: Any) -> dict[str, Any]:
53
+ """Return a JSON-RPC 2.0 success response."""
51
54
  return {"jsonrpc": "2.0", "id": req_id, "result": result}
52
55
 
53
56
 
54
57
  def _err(req_id: Any, code: int, message: str) -> dict[str, Any]:
58
+ """Return a JSON-RPC 2.0 error response."""
55
59
  return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
56
60
 
57
61
 
@@ -59,6 +63,7 @@ SCAN_EXEMPT_TOOLS = frozenset({"init-agent-onboarding", "scan"})
59
63
 
60
64
 
61
65
  def _handle(prompts_root: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
66
+ """Dispatch a single JSON-RPC message; return response dict or None for notifications."""
62
67
  method = msg.get("method")
63
68
  req_id = msg.get("id")
64
69
  params = msg.get("params") or {}
@@ -0,0 +1,23 @@
1
+ # Task: Refresh All Documentation
2
+
3
+ Runs a full documentation refresh cycle: scan, check staleness, then generate or update every stale or missing doc in one pass.
4
+
5
+ This prompt is the batch form (all bundles concatenated). When running via the `/forerunner-refresh` skill, the agent calls `forerunner doc <task>` for each step individually so it can process each result before moving to the next.
6
+
7
+ ## Steps (execute in order)
8
+
9
+ 1. **Scan** — Execute the scan task bundle. Capture the YAML output. All downstream tasks depend on it.
10
+ 2. **Check** — Execute the check task bundle using the scan result. Identify every doc with `STALE` or `MISSING` status.
11
+ 3. **Generate / update** — For each stale or missing doc, run the corresponding task bundle in this order:
12
+ `readme` → `api-docs` → `stack-docs` → `diagrams` → `flows` → `version-audit` → `audit`
13
+ Skip any task whose check status is `CURRENT`.
14
+ Note: `changelog` and `review` are on-demand tasks excluded from automated refresh.
15
+
16
+ ## Rules
17
+
18
+ - The scan result from step 1 is the input to all downstream tasks.
19
+ - The check report from step 2 determines which tasks run.
20
+ - Stop and report if scan fails (non-zero exit or empty output).
21
+ - Write each artifact to its task-defined output path.
22
+ - Append a `## Gaps` section to any doc where evidence is insufficient — never silently omit content.
23
+ - Report a summary of what was updated, skipped, and any gaps found.