codeforerunner 0.4.4__py3-none-any.whl → 0.4.5__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/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/."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -8,12 +8,21 @@ 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 as _resolve_bundle
12
-
13
- SCAN_EXEMPT_TASKS = frozenset({"scan", "init-agent-onboarding"})
11
+ from codeforerunner.bundle import find_prompts_root
12
+ from codeforerunner.prompt_session import Denial, PromptSession
13
+ from codeforerunner.tasks import refresh_tasks as _refresh_tasks
14
14
  SCAN_DONE_ENV = "FORERUNNER_SCAN_DONE"
15
15
 
16
16
 
17
+ def _scan_satisfied(repo_root: Path) -> bool:
18
+ """CLI scan-first signal: scan artifact present, env override set, or no config to gate."""
19
+ return (
20
+ (repo_root / ".forerunner" / "scan.md").is_file()
21
+ or bool(os.environ.get(SCAN_DONE_ENV))
22
+ or not (repo_root / "forerunner.config.yaml").is_file()
23
+ )
24
+
25
+
17
26
  def _get_bundle(args: argparse.Namespace) -> tuple[str, int]:
18
27
  """Resolve bundle for args.task. Returns (bundle_text, exit_code). exit_code != 0 on error."""
19
28
  try:
@@ -22,25 +31,22 @@ def _get_bundle(args: argparse.Namespace) -> tuple[str, int]:
22
31
  print(f"error: {e}", file=sys.stderr)
23
32
  return "", 2
24
33
 
25
- task_path = prompts_root / "tasks" / f"{args.task}.md"
26
- if not task_path.is_file():
27
- print(f"error: unknown task '{args.task}' (no {task_path})", file=sys.stderr)
28
- return "", 2
29
-
30
- repo_root = Path(args.repo) if args.repo else Path.cwd()
31
- if (
32
- args.task not in SCAN_EXEMPT_TASKS
33
- and (repo_root / "forerunner.config.yaml").is_file()
34
- and not os.environ.get(SCAN_DONE_ENV)
35
- ):
34
+ repo_root = Path(args.repo).resolve() if args.repo else Path.cwd()
35
+ session = PromptSession(prompts_root, _scan_satisfied(repo_root))
36
+ decision = session.can_run(args.task)
37
+ if not decision.allowed:
38
+ if decision.reason is Denial.UNKNOWN_TASK:
39
+ print(f"error: unknown task '{args.task}'", file=sys.stderr)
40
+ return "", 2
36
41
  print(
37
- f"warning: SPEC V2 scan-first — run `forerunner scan` first, "
38
- f"then export {SCAN_DONE_ENV}=1 to silence this warning.",
42
+ f"error: scan-first required — run `forerunner scan` first "
43
+ f"(writes .forerunner/scan.md). Set {SCAN_DONE_ENV}=1 to skip.",
39
44
  file=sys.stderr,
40
45
  )
46
+ return "", 1
41
47
 
42
48
  try:
43
- return _resolve_bundle(prompts_root, args.task), 0
49
+ return session.bundle_for(args.task), 0
44
50
  except FileNotFoundError as e:
45
51
  print(f"error: {e}", file=sys.stderr)
46
52
  return "", 2
@@ -73,12 +79,12 @@ def cmd_init(args: argparse.Namespace) -> int:
73
79
 
74
80
 
75
81
  def cmd_scan(args: argparse.Namespace) -> int:
76
- """Emit the scan prompt bundle and hint about FORERUNNER_SCAN_DONE."""
82
+ """Emit the scan prompt bundle and hint about scan artifact."""
77
83
  rc = _doc_for(args, "scan")
78
84
  if rc == 0:
79
85
  print(
80
- f"hint: export {SCAN_DONE_ENV}=1 in this shell to silence "
81
- "scan-first warnings on follow-up `forerunner doc`/`init` calls.",
86
+ "hint: write the scan result to .forerunner/scan.md to satisfy the "
87
+ f"scan-first gate on follow-up calls. Or set {SCAN_DONE_ENV}=1 to skip.",
82
88
  file=sys.stderr,
83
89
  )
84
90
  return rc
@@ -111,19 +117,19 @@ def cmd_mcp_server(args: argparse.Namespace) -> int:
111
117
  except FileNotFoundError as e:
112
118
  print(f"mcp_server: {e}", file=sys.stderr)
113
119
  return 2
114
- return mcp_server.serve(prompts_root)
120
+ repo_root = Path(args.repo).resolve() if args.repo else Path.cwd()
121
+ return mcp_server.serve(prompts_root, repo_root=repo_root)
115
122
 
116
123
 
117
124
  def cmd_refresh(args: argparse.Namespace) -> int:
118
125
  """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):
126
+ task_names = [t.name for t in _refresh_tasks()]
127
+ for i, task in enumerate(task_names):
122
128
  ns = argparse.Namespace(repo=getattr(args, "repo", None), task=task)
123
129
  rc = cmd_doc(ns)
124
130
  if rc != 0:
125
131
  return rc
126
- if i < len(tasks) - 1:
132
+ if i < len(task_names) - 1:
127
133
  sys.stdout.write("\n---\n\n")
128
134
  return 0
129
135
 
codeforerunner/config.py CHANGED
@@ -1,4 +1,4 @@
1
- """`forerunner.config.yaml` schema + loader. See SPEC.md §T25."""
1
+ """`forerunner.config.yaml` schema + loader."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
codeforerunner/doctor.py CHANGED
@@ -1,4 +1,4 @@
1
- """`forerunner doctor` — single-screen health report. See SPEC.md §T35."""
1
+ """`forerunner doctor` — single-screen health report."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -1,4 +1,4 @@
1
- """Idempotent skill installer. See SPEC.md §D.installer (cites V8, V10, V11, V12)."""
1
+ """Idempotent skill installer. Re-run safe (V12); body-parity enforced (V10)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -10,6 +10,8 @@ from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
  from typing import Iterable, Literal
12
12
 
13
+ from codeforerunner.tasks import installable_slugs as _installable_slugs
14
+
13
15
  MARKER_BEGIN = "<!-- forerunner:begin managed=codeforerunner.skill -->"
14
16
  MARKER_END = "<!-- forerunner:end -->"
15
17
 
@@ -21,22 +23,7 @@ EXIT_USAGE = 2
21
23
  EXIT_BODY_MISMATCH = 3
22
24
  EXIT_UNMANAGED_DEST = 4
23
25
 
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
- )
26
+ TASK_SKILL_SLUGS: tuple[str, ...] = _installable_slugs()
40
27
 
41
28
 
42
29
  @dataclass(frozen=True)
@@ -1,4 +1,4 @@
1
- """Minimal stdio MCP server exposing prompt bundles as tools. See SPEC.md §D.mcp.
1
+ """Minimal stdio MCP server exposing prompt bundles as tools.
2
2
 
3
3
  Hand-rolled JSON-RPC 2.0 over line-delimited stdio. Stdlib only.
4
4
  """
@@ -11,21 +11,15 @@ from pathlib import Path
11
11
  from typing import Any, Iterable
12
12
 
13
13
  from codeforerunner import __version__ as _pkg_version
14
- from codeforerunner.bundle import find_prompts_root, resolve_bundle
14
+ from codeforerunner.bundle import find_prompts_root
15
+ from codeforerunner.prompt_session import Denial, PromptSession
16
+ from codeforerunner.tasks import all_tasks as _all_tasks
15
17
 
16
18
  PROTOCOL_VERSION = "2025-03-26"
17
19
  SERVER_NAME = "codeforerunner"
18
20
  SERVER_VERSION = _pkg_version
19
21
 
20
22
 
21
- def _list_tasks(prompts_root: Path) -> list[Path]:
22
- """Return sorted list of task *.md paths under prompts_root/tasks/."""
23
- tasks_dir = prompts_root / "tasks"
24
- if not tasks_dir.is_dir():
25
- return []
26
- return sorted(tasks_dir.glob("*.md"))
27
-
28
-
29
23
  def _description_for(task_path: Path) -> str:
30
24
  """First non-empty markdown line, stripped of leading '#' and whitespace."""
31
25
  with task_path.open(encoding="utf-8") as f:
@@ -38,14 +32,14 @@ def _description_for(task_path: Path) -> str:
38
32
 
39
33
 
40
34
  def _tools(prompts_root: Path) -> list[dict[str, Any]]:
41
- """Build MCP tools/list payload from all task files in prompts_root."""
35
+ """Build MCP tools/list payload from registered tasks."""
42
36
  return [
43
37
  {
44
- "name": p.stem,
45
- "description": _description_for(p),
38
+ "name": task.name,
39
+ "description": _description_for(prompts_root / "tasks" / f"{task.name}.md"),
46
40
  "inputSchema": {"type": "object", "properties": {}, "required": []},
47
41
  }
48
- for p in _list_tasks(prompts_root)
42
+ for task in _all_tasks()
49
43
  ]
50
44
 
51
45
 
@@ -59,9 +53,6 @@ def _err(req_id: Any, code: int, message: str) -> dict[str, Any]:
59
53
  return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
60
54
 
61
55
 
62
- SCAN_EXEMPT_TOOLS = frozenset({"init-agent-onboarding", "scan"})
63
-
64
-
65
56
  def _handle(prompts_root: Path, msg: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
66
57
  """Dispatch a single JSON-RPC message; return response dict or None for notifications."""
67
58
  method = msg.get("method")
@@ -94,11 +85,11 @@ def _handle(prompts_root: Path, msg: dict[str, Any], state: dict[str, Any]) -> d
94
85
  name = params.get("name")
95
86
  if not isinstance(name, str) or "/" in name or "\\" in name or ".." in name:
96
87
  return _err(req_id, -32602, f"invalid tool name: {name!r}")
97
- task_path = prompts_root / "tasks" / f"{name}.md"
98
- tasks_root = (prompts_root / "tasks").resolve()
99
- if not task_path.resolve().is_relative_to(tasks_root) or not task_path.is_file():
100
- return _err(req_id, -32602, f"unknown tool: {name!r}")
101
- if name not in SCAN_EXEMPT_TOOLS and not state.get("scan_called"):
88
+ session = PromptSession(prompts_root, scan_satisfied=bool(state.get("scan_called")))
89
+ decision = session.can_run(name)
90
+ if not decision.allowed:
91
+ if decision.reason is Denial.UNKNOWN_TASK:
92
+ return _err(req_id, -32602, f"unknown tool: {name!r}")
102
93
  return _err(
103
94
  req_id,
104
95
  -32000,
@@ -107,7 +98,7 @@ def _handle(prompts_root: Path, msg: dict[str, Any], state: dict[str, Any]) -> d
107
98
  if name == "scan":
108
99
  state["scan_called"] = True
109
100
  try:
110
- text = resolve_bundle(prompts_root, name)
101
+ text = session.bundle_for(name)
111
102
  except Exception as e: # pragma: no cover - defensive
112
103
  return _err(req_id, -32603, f"internal error: {e}")
113
104
  return _ok(
@@ -118,9 +109,17 @@ def _handle(prompts_root: Path, msg: dict[str, Any], state: dict[str, Any]) -> d
118
109
  return _err(req_id, -32601, f"method not found: {method!r}")
119
110
 
120
111
 
121
- def serve(prompts_root: Path, stdin: Iterable[str] = sys.stdin, stdout=sys.stdout, stderr=sys.stderr) -> int:
112
+ def serve(
113
+ prompts_root: Path,
114
+ repo_root: Path | None = None,
115
+ stdin: Iterable[str] = sys.stdin,
116
+ stdout=sys.stdout,
117
+ stderr=sys.stderr,
118
+ ) -> int:
122
119
  """Run the JSON-RPC 2.0 MCP server loop over *stdin*/*stdout* until EOF."""
123
- state: dict[str, Any] = {"scan_called": False, "initialized": False}
120
+ root = repo_root if repo_root is not None else Path.cwd()
121
+ scan_artifact = root / ".forerunner" / "scan.md"
122
+ state: dict[str, Any] = {"scan_called": scan_artifact.is_file(), "initialized": False}
124
123
  for raw in stdin:
125
124
  line = raw.strip()
126
125
  if not line:
@@ -153,7 +152,7 @@ def main(argv: list[str] | None = None) -> int:
153
152
  except FileNotFoundError as e:
154
153
  print(f"mcp_server: {e}", file=sys.stderr)
155
154
  return 2
156
- return serve(prompts_root)
155
+ return serve(prompts_root, repo_root=Path.cwd().resolve())
157
156
 
158
157
 
159
158
  if __name__ == "__main__": # pragma: no cover
@@ -0,0 +1,53 @@
1
+ """Prompt Session — run-scoped owner of task lookup and scan-first enforcement.
2
+
3
+ CLI and MCP are thin adapters: they construct a session, ask whether a task
4
+ can run, and translate the structured Decision into their own surface
5
+ (stdout/exit codes for CLI, JSON-RPC for MCP). The session unifies the
6
+ scan-first *rule*; each adapter still computes its own scan-satisfied signal
7
+ and injects it (see docs/adr/0001).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from enum import Enum, auto
14
+ from pathlib import Path
15
+
16
+ from codeforerunner import tasks
17
+ from codeforerunner.bundle import resolve_bundle
18
+
19
+
20
+ class Denial(Enum):
21
+ UNKNOWN_TASK = auto()
22
+ SCAN_REQUIRED = auto()
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Decision:
27
+ allowed: bool
28
+ task: tasks.Task | None = None
29
+ reason: Denial | None = None
30
+ message: str | None = None
31
+
32
+
33
+ class PromptSession:
34
+ def __init__(self, prompts_root: Path, scan_satisfied: bool) -> None:
35
+ self._prompts_root = prompts_root
36
+ self._scan_done = scan_satisfied
37
+
38
+ def mark_scan_done(self) -> None:
39
+ """Record that scan ran in this session (MCP adapter, after scan tool)."""
40
+ self._scan_done = True
41
+
42
+ def can_run(self, name: str) -> Decision:
43
+ try:
44
+ task = tasks.get(name)
45
+ except KeyError:
46
+ return Decision(False, reason=Denial.UNKNOWN_TASK, message=f"unknown task: {name!r}")
47
+ if task.scan_exempt or self._scan_done:
48
+ return Decision(True, task=task)
49
+ return Decision(False, task=task, reason=Denial.SCAN_REQUIRED, message="scan-first required")
50
+
51
+ def bundle_for(self, name: str) -> str:
52
+ """Resolve the prompt bundle text for *name*. Call only after can_run allows."""
53
+ return resolve_bundle(self._prompts_root, name)
@@ -0,0 +1,121 @@
1
+ # Task: Architecture Review
2
+
3
+ Inspired by Matt Pocock's `/improve-codebase-architecture` skill:
4
+ https://github.com/mattpocock/skills/tree/main/skills/engineering/improve-codebase-architecture
5
+
6
+ Ranks repo-grounded Deepening Opportunities: architecture improvements that hide more behavior behind smaller interfaces, increasing leverage for callers and locality for maintainers.
7
+ Requires scan result as input.
8
+
9
+ ## Input
10
+
11
+ - Scan result from `prompts/tasks/scan.md`
12
+ - File tree
13
+ - Key module/package files relevant to the scan result
14
+ - Existing tests for the modules under review
15
+ - `CONTEXT.md` or `CONTEXT-MAP.md` if present
16
+ - Relevant `docs/adr/*.md` files if present
17
+ - Existing architecture docs only when they clarify current design
18
+
19
+ ## Review Focus
20
+
21
+ Look for architecture friction, not documentation drift:
22
+
23
+ 1. Modules that are shallow: interface complexity nearly matches implementation complexity
24
+ 2. Concepts that require bouncing across many files to understand
25
+ 3. Seams that leak implementation details into callers
26
+ 4. Pure helpers extracted for testability while real behavior remains hard to test
27
+ 5. Adapter seams with more abstraction than real variation
28
+ 6. Missing or weak tests caused by poor locality
29
+
30
+ Apply the deletion test to suspected shallow modules: if deleting the module makes complexity disappear, it was probably a pass-through; if complexity reappears across many callers, it was earning its keep.
31
+
32
+ ## Vocabulary
33
+
34
+ Use these architecture terms consistently:
35
+
36
+ - Module
37
+ - Interface
38
+ - Implementation
39
+ - Deep
40
+ - Shallow
41
+ - Seam
42
+ - Adapter
43
+ - Leverage
44
+ - Locality
45
+ - Deepening Opportunity
46
+ - Deletion test
47
+
48
+ Use repo vocabulary from `CONTEXT.md` when present. If the repo lacks `CONTEXT.md` or the vocabulary is incomplete, infer temporary terms from evidence and list them under `## Suggested Glossary Additions`; do not write or rewrite `CONTEXT.md`.
49
+
50
+ ## Candidate Format
51
+
52
+ For each Deepening Opportunity, include:
53
+
54
+ - **Files/modules**: concrete files or modules involved
55
+ - **Problem**: architecture friction observed
56
+ - **Evidence**: repo evidence supporting the finding
57
+ - **Proposed direction**: plain-English direction only
58
+ - **Benefits**: locality and leverage improvements
59
+ - **Testing impact**: what becomes easier to verify, without final test code
60
+ - **Risk / blast radius**: likely scope and migration risk
61
+ - **Recommendation strength**: `Strong`, `Worth exploring`, or `Speculative`
62
+
63
+ ## Rules
64
+
65
+ - Claims must derive from provided files. If evidence is absent, omit or document in `## Gaps`.
66
+ - Do not report stale README/API/diagram/doc drift; use `check`, `review`, `diagrams`, or `flows` for that.
67
+ - Do not propose final function signatures, dataclass fields, schema shapes, or file-by-file implementation plans.
68
+ - Do not mutate `CONTEXT.md` or create ADRs.
69
+ - Do not imply Matt Pocock endorses codeforerunner.
70
+ - Keep the highest-signal 3-7 candidates. Fewer is acceptable when evidence is thin.
71
+
72
+ ## Output Format
73
+
74
+ <!-- output: .forerunner/arch-review.md -->
75
+
76
+ # Architecture Review
77
+
78
+ > Inspired by Matt Pocock's `/improve-codebase-architecture` skill:
79
+ > https://github.com/mattpocock/skills/tree/main/skills/engineering/improve-codebase-architecture
80
+
81
+ ## Summary
82
+
83
+ One paragraph describing the main architecture pressure observed.
84
+
85
+ ## Top Recommendation
86
+
87
+ Name the highest-value Deepening Opportunity and why it should be first.
88
+
89
+ ## Deepening Opportunities
90
+
91
+ ### 1. [Candidate Name]
92
+
93
+ **Recommendation strength:** Strong | Worth exploring | Speculative
94
+
95
+ **Files/modules:** ...
96
+
97
+ **Problem:** ...
98
+
99
+ **Evidence:** ...
100
+
101
+ **Proposed direction:** ...
102
+
103
+ **Benefits:** ...
104
+
105
+ **Testing impact:** ...
106
+
107
+ **Risk / blast radius:** ...
108
+
109
+ ## Suggested Glossary Additions
110
+
111
+ Only include if `CONTEXT.md` is missing or incomplete. Suggest terms; do not write them.
112
+
113
+ ## Not Yet Decided
114
+
115
+ - Final interface shapes
116
+ - Migration sequence
117
+ - Exact tests
118
+
119
+ ## Gaps
120
+
121
+ List missing evidence that could materially change the review.
@@ -10,7 +10,11 @@ Prompt-first workflow for manual use today; future wrappers may orchestrate it.
10
10
  - Existing instruction files if present:
11
11
  - `AGENTS.md`
12
12
  - `CLAUDE.md`, `.cursor/rules/*`, `.cursorrules`, `.github/copilot-instructions.md`, `opencode.json`
13
- - Key docs describing current state (`README.md`, `SPEC.md`, roadmap docs)
13
+ - Existing domain vocabulary files if present:
14
+ - `CONTEXT.md`
15
+ - `CONTEXT-MAP.md`
16
+ - relevant `docs/adr/*.md`
17
+ - Key docs describing current state (`README.md`, `AGENTS.md`, roadmap docs)
14
18
  - Optional prior scan result from `prompts/tasks/scan.md`
15
19
 
16
20
  ## Objectives
@@ -18,6 +22,7 @@ Prompt-first workflow for manual use today; future wrappers may orchestrate it.
18
22
  1. Build or refine `AGENTS.md` so future agent sessions ramp quickly.
19
23
  2. Keep only high-signal, repo-specific guidance likely to prevent mistakes.
20
24
  3. Reconcile instruction drift across files without deleting valid custom constraints.
25
+ 4. Create or refine `CONTEXT.md` when stable repo-specific domain terms are evident.
21
26
 
22
27
  ## Method
23
28
 
@@ -26,6 +31,7 @@ Prompt-first workflow for manual use today; future wrappers may orchestrate it.
26
31
  3. Keep sections short and scannable.
27
32
  4. Remove stale or generic guidance.
28
33
  5. Preserve useful constraints that remain true.
34
+ 6. For `CONTEXT.md`, include only stable repo-specific vocabulary. Do not add generic technology terms unless this repo gives them product-specific meaning.
29
35
 
30
36
  ## Required Output Sections
31
37
 
@@ -39,6 +45,9 @@ Prompt-first workflow for manual use today; future wrappers may orchestrate it.
39
45
 
40
46
  - Never invent runnable surfaces (CLI, CI, hooks, Docker, package publish) if files do not exist.
41
47
  - Never copy broad best-practice text that is not repo-specific.
48
+ - Create or update `CONTEXT.md` only when stable repo-specific terms exist in tracked files or established conversation context.
49
+ - If only generic technical words are available, do not create `CONTEXT.md`; mention the absence under `## Gaps` if relevant.
50
+ - Keep `CONTEXT.md` as a glossary only: no implementation plan, scratch notes, or file-by-file design decisions.
42
51
  - Claims must derive from provided files. If evidence is absent, omit or document in `## Gaps`.
43
52
  - If uncertain, omit.
44
53
  - Keep naming consistent with repo conventions.
@@ -46,3 +55,7 @@ Prompt-first workflow for manual use today; future wrappers may orchestrate it.
46
55
  ## Output
47
56
 
48
57
  <!-- output: AGENTS.md -->
58
+
59
+ When stable domain vocabulary exists, also output:
60
+
61
+ <!-- output: CONTEXT.md -->
@@ -90,3 +90,5 @@ gaps:
90
90
  ```
91
91
 
92
92
  Wrap output in a fenced yaml block. No prose before or after.
93
+
94
+ <!-- output: .forerunner/scan.md -->
@@ -0,0 +1,30 @@
1
+ {
2
+ "canonical_skill_slug": "codeforerunner",
3
+ "refresh_sequence": [
4
+ "scan",
5
+ "check",
6
+ "readme",
7
+ "api-docs",
8
+ "stack-docs",
9
+ "diagrams",
10
+ "flows",
11
+ "version-audit",
12
+ "audit"
13
+ ],
14
+ "tasks": [
15
+ {"name": "scan", "scan_exempt": true, "skill_slug": "forerunner-scan"},
16
+ {"name": "init-agent-onboarding", "scan_exempt": true, "skill_slug": "forerunner-init"},
17
+ {"name": "readme", "scan_exempt": false, "skill_slug": "forerunner-readme"},
18
+ {"name": "api-docs", "scan_exempt": false, "skill_slug": "forerunner-api-docs"},
19
+ {"name": "arch-review", "scan_exempt": false, "skill_slug": "forerunner-arch-review"},
20
+ {"name": "audit", "scan_exempt": false, "skill_slug": "forerunner-audit"},
21
+ {"name": "changelog", "scan_exempt": false, "skill_slug": "forerunner-changelog"},
22
+ {"name": "check", "scan_exempt": false, "skill_slug": "forerunner-check"},
23
+ {"name": "diagrams", "scan_exempt": false, "skill_slug": "forerunner-diagrams"},
24
+ {"name": "flows", "scan_exempt": false, "skill_slug": "forerunner-flows"},
25
+ {"name": "refresh", "scan_exempt": false, "skill_slug": "forerunner-refresh"},
26
+ {"name": "review", "scan_exempt": false, "skill_slug": "forerunner-review"},
27
+ {"name": "stack-docs", "scan_exempt": false, "skill_slug": "forerunner-stack-docs"},
28
+ {"name": "version-audit", "scan_exempt": false, "skill_slug": "forerunner-version-audit"}
29
+ ]
30
+ }
@@ -0,0 +1,59 @@
1
+ """Task Registry — single source of truth for task identity and policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ import json
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Task:
12
+ name: str
13
+ scan_exempt: bool
14
+ skill_slug: str | None
15
+
16
+
17
+ def _load() -> tuple[list[Task], list[str], str]:
18
+ """Load and parse tasks.json. Returns (tasks, refresh_sequence, canonical_skill_slug)."""
19
+ data = json.loads(
20
+ importlib.resources.files("codeforerunner").joinpath("tasks.json").read_text(encoding="utf-8")
21
+ )
22
+ task_list = [
23
+ Task(
24
+ name=entry["name"],
25
+ scan_exempt=entry["scan_exempt"],
26
+ skill_slug=entry.get("skill_slug"),
27
+ )
28
+ for entry in data["tasks"]
29
+ ]
30
+ return task_list, data["refresh_sequence"], data["canonical_skill_slug"]
31
+
32
+
33
+ _TASKS, _REFRESH_SEQUENCE, _CANONICAL_SLUG = _load()
34
+ _BY_NAME: dict[str, Task] = {t.name: t for t in _TASKS}
35
+
36
+
37
+ def all_tasks() -> list[Task]:
38
+ return list(_TASKS)
39
+
40
+
41
+ def get(name: str) -> Task:
42
+ try:
43
+ return _BY_NAME[name]
44
+ except KeyError:
45
+ raise KeyError(f"unknown task: {name!r}") from None
46
+
47
+
48
+ def refresh_tasks() -> list[Task]:
49
+ return [_BY_NAME[name] for name in _REFRESH_SEQUENCE]
50
+
51
+
52
+ def scan_exempt_names() -> frozenset[str]:
53
+ return frozenset(t.name for t in _TASKS if t.scan_exempt)
54
+
55
+
56
+ def installable_slugs() -> tuple[str, ...]:
57
+ slugs = [_CANONICAL_SLUG]
58
+ slugs.extend(t.skill_slug for t in _TASKS if t.skill_slug and t.skill_slug != _CANONICAL_SLUG)
59
+ return tuple(slugs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeforerunner
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Model-agnostic repository documentation tooling (prompt-first; thin CLI).
5
5
  Author: Derek Palmer
6
6
  License-Expression: LicenseRef-Codeforerunner-SAL-0.1
@@ -29,7 +29,7 @@ Dynamic: license-file
29
29
 
30
30
  # codeForerunner
31
31
 
32
- [![Socket Badge](https://badge.socket.dev/npm/package/codeforerunner/0.4.3)](https://badge.socket.dev/npm/package/codeforerunner/0.4.3)
32
+ [![Socket Badge](https://badge.socket.dev/npm/package/codeforerunner/0.4.5)](https://socket.dev/npm/package/codeforerunner)
33
33
 
34
34
  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.
35
35
 
@@ -44,10 +44,15 @@ Install forerunner's prompt pack as skills into your agent CLI. Each documentati
44
44
  # One-liner (auto-detects Claude Code, Codex, Gemini CLI)
45
45
  curl -fsSL https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.sh | bash
46
46
 
47
+ # npm
48
+ npx -y codeforerunner
49
+ npm install -g codeforerunner
50
+
47
51
  # Windows
48
52
  irm https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.ps1 | iex
49
53
 
50
- # Via forerunner CLI (after pip install)
54
+ # Via Python CLI
55
+ pip install codeforerunner
51
56
  forerunner install --all claude
52
57
  forerunner install --all codex
53
58
  ```
@@ -70,6 +75,7 @@ Then in your agent:
70
75
  | `/forerunner-api-docs` | `api-docs` | Generate API reference docs |
71
76
  | `/forerunner-diagrams` | `diagrams` | Generate Mermaid architecture diagrams |
72
77
  | `/forerunner-flows` | `flows` | Document system flows |
78
+ | `/forerunner-arch-review` | `arch-review` | Rank architecture improvement candidates, inspired by Matt Pocock's [`/improve-codebase-architecture`](https://github.com/mattpocock/skills/tree/main/skills/engineering/improve-codebase-architecture) |
73
79
  | `/forerunner-stack-docs` | `stack-docs` | Stack-specific developer docs |
74
80
  | `/forerunner-version-audit` | `version-audit` | Audit pinned versions vs EOL |
75
81
  | `/forerunner-check` | `check` | Check docs for staleness |
@@ -118,6 +124,9 @@ pip install codeforerunner
118
124
  # Install skills into Claude Code
119
125
  curl -fsSL https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.sh | bash
120
126
 
127
+ # Or via npm
128
+ # npx -y codeforerunner
129
+
121
130
  # In Claude Code:
122
131
  # /forerunner-scan → scans your repo
123
132
  # /forerunner-readme → generates README.md
@@ -139,7 +148,7 @@ jobs:
139
148
  runs-on: ubuntu-latest
140
149
  steps:
141
150
  - uses: actions/checkout@v6.0.2
142
- - uses: derek-palmer/codeforerunner@v0.4.3
151
+ - uses: derek-palmer/codeforerunner@v0.4.5
143
152
  with:
144
153
  fail-on-drift: "true" # set "false" to warn-only
145
154
  ```
@@ -224,13 +233,13 @@ prompts/
224
233
  ├── scan.md api-docs.md audit.md
225
234
  ├── readme.md diagrams.md changelog.md
226
235
  ├── check.md flows.md version-audit.md
227
- ├── review.md stack-docs.md refresh.md
236
+ ├── review.md stack-docs.md arch-review.md
237
+ ├── refresh.md
228
238
  └── init-agent-onboarding.md
229
239
  ```
230
240
 
231
- ## Docs and spec
241
+ ## Docs
232
242
 
233
- - `SPEC.md` — canonical phase/task tracker
234
243
  - `docs/getting-started.md` — manual prompt use
235
244
  - `docs/prompt-guide.md` — how system, partial, and task prompts compose
236
245
  - `docs/editor-agent-setup.md` — adapting prompts to local agents
@@ -1,31 +1,35 @@
1
1
  codeforerunner/__init__.py,sha256=lDClMqfAXApyQaPIWGp01yvS0zWJC-7sXTCRKPqit9M,292
2
2
  codeforerunner/bundle.py,sha256=EfQGozjsS8g20607sGwuUwZ9yk379dg5UhNpIla2mKU,2132
3
3
  codeforerunner/check.py,sha256=UrHyzdfRZpH80iXPANbjhkTKN9Wes9IZnDS-Ms3sTxo,8750
4
- codeforerunner/cli.py,sha256=aAOQNViteDto-J6VyJ8mLkuv3kUHo2U0aTtCINDJVqc,8318
5
- codeforerunner/config.py,sha256=izAnUQQzEfpHcROtvnC3QuLfUvy4DV4d7DKZu6b6YEo,5500
6
- codeforerunner/doctor.py,sha256=lSbXXN_R-eQ8pR341PaZ42lFakSgkkyREN-wMHF8_Tw,10707
7
- codeforerunner/installer.py,sha256=e-__Lky415pAxI-BWqSNG3CdO4UZpHq31-jg3V-ojn4,14527
8
- codeforerunner/mcp_server.py,sha256=FB-L6-eN-htA74BnnOPO2YntOs9WahpJuUxdlYzDEwk,5603
4
+ codeforerunner/cli.py,sha256=Ex7LCiqKikYtN-9wbZ_t4QdbgQKJkAsEp-puOxwn_p8,8643
5
+ codeforerunner/config.py,sha256=LiN_6qkSgfKwdH_hK0HYhA5HlAqOfgmCDS9NLiJRNQg,5481
6
+ codeforerunner/doctor.py,sha256=Hd141wT26ZoTVmxzZW9DJjBxZou4DurMSARB5AAVv4o,10688
7
+ codeforerunner/installer.py,sha256=X0NoOSh1bqVRO012auGn-WuUix2RBD7wL0N2ew9vJbY,14173
8
+ codeforerunner/mcp_server.py,sha256=PB4qwB_oncMQY41243InxXYzkTUUSfewhKSGJibi7Gw,5538
9
+ codeforerunner/prompt_session.py,sha256=8ulbvtJuROos7CfZ66a9qW4n3uJ6_wNavRbkrldGtvE,1794
10
+ codeforerunner/tasks.json,sha256=vgMW4OdfYE4_Al_8vDp38MAAx8uQF_uUYckcme3HQRM,1589
11
+ codeforerunner/tasks.py,sha256=DeLu3LAPcIbAQDra_xAi8mZyQicfEMPj3d64MhrTJss,1571
9
12
  codeforerunner/prompts/partials/context-format.md,sha256=WNfkr4kf2Awj0R8wLOrFotEiYCe6hfKTq5eA3Rt5_Xw,817
10
13
  codeforerunner/prompts/partials/output-rules.md,sha256=vfIAX-ImxCa-MVAeNH896uSIO7-cKbJd0KohkgHIiD8,1731
11
14
  codeforerunner/prompts/partials/stack-hints.md,sha256=8E2qELhk-hve2ULSdmiFK48LE4Aprhmuasqr6A1K2QU,2001
12
15
  codeforerunner/prompts/system/base.md,sha256=MKxVGjn9eSwW86LWuXdDmoMaEqkDcrHPc6AbKKMx48A,1870
13
16
  codeforerunner/prompts/tasks/api-docs.md,sha256=T1tAvHXXcuPTJ33fZoGxy4QFuCy54xTDFw22BYqfhLs,1052
17
+ codeforerunner/prompts/tasks/arch-review.md,sha256=EDQTpqss_B1qGkVSbde-MIwQjZNwExLndS7wv-XTnjE,3900
14
18
  codeforerunner/prompts/tasks/audit.md,sha256=EVaxDbLlWwZvSIODBTQqxUZk3PyCuoRJwbowedBZsBk,1896
15
19
  codeforerunner/prompts/tasks/changelog.md,sha256=zEy3HFFjsg_X4K93yOgKUfhpunrbqJrgxtvu4XUoekg,1503
16
20
  codeforerunner/prompts/tasks/check.md,sha256=TWEjrAqq14-yKGPL39x0RlaTTYfMZ8lG4VMSMsZ_4YA,2722
17
21
  codeforerunner/prompts/tasks/diagrams.md,sha256=Hjo5PS5HwH0VAwjQrAfZGhOjnOco1VjBZ_Z8Rdt1exo,2129
18
22
  codeforerunner/prompts/tasks/flows.md,sha256=JDfI9-zuSI-1rhStbpzb5Bb05iEMWFB0dAffsAwnI08,1592
19
- codeforerunner/prompts/tasks/init-agent-onboarding.md,sha256=O8T39Gx5DgcLczltZiiOsTWorupA0-aIeC6_xH0M4aQ,1697
23
+ codeforerunner/prompts/tasks/init-agent-onboarding.md,sha256=J-HJ-6TcPdMUGC5E76QiNLD61XmnU-fWHZTanuFtzME,2504
20
24
  codeforerunner/prompts/tasks/readme.md,sha256=Fq9CTAryb7Kv4bPMIgcyGFgmmzh7KtnMRa9tSyWt0yU,1156
21
25
  codeforerunner/prompts/tasks/refresh.md,sha256=-7vRYazNLNegKC2wdJBfR1j9Brw_tLVEbaVx9BVbu0o,1435
22
26
  codeforerunner/prompts/tasks/review.md,sha256=IRdIXAKvv0JMOE5WtrnlO1Cd4LHXtcJqb1C9yc1biVA,1741
23
- codeforerunner/prompts/tasks/scan.md,sha256=hYXf-IL1kgpBPHJapRrwTu88cLZ7y3bCmAq9HY0yG3U,2300
27
+ codeforerunner/prompts/tasks/scan.md,sha256=IoZXX-bt9ODra7cSB-3Y6IUgws6YTDKwKNX_18ChjIU,2338
24
28
  codeforerunner/prompts/tasks/stack-docs.md,sha256=Dy-JSXpSmHSyhR5shQBXKa_F0PqnjPcmtljthYZpaiM,1923
25
29
  codeforerunner/prompts/tasks/version-audit.md,sha256=oK-pcoxt_VcvDOlj1Sz9OlEhXlcViLPn54r-qP5WfiA,5833
26
- codeforerunner-0.4.4.dist-info/licenses/LICENSE.md,sha256=iIhmJHib6GbdjcwiDMM-npiNRf3XgASom1WsOJivEdc,2915
27
- codeforerunner-0.4.4.dist-info/METADATA,sha256=wbBCRgBBLt2i5MGtQ7HDH62EOoAuUAWWNbX2YPZHWdM,9456
28
- codeforerunner-0.4.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
- codeforerunner-0.4.4.dist-info/entry_points.txt,sha256=3p8BbPlq-wfcXk42tsweKePRaGlZ1WXho1gOkuZGyIQ,55
30
- codeforerunner-0.4.4.dist-info/top_level.txt,sha256=pV1rt0-NIpNEotKXpL_sF2060DHr-_0F86LWhUlvXis,15
31
- codeforerunner-0.4.4.dist-info/RECORD,,
30
+ codeforerunner-0.4.5.dist-info/licenses/LICENSE.md,sha256=iIhmJHib6GbdjcwiDMM-npiNRf3XgASom1WsOJivEdc,2915
31
+ codeforerunner-0.4.5.dist-info/METADATA,sha256=3Cg7P0koLSlnj8Cqr8NEIGKnmqCLiUior9EvAO31mO0,9767
32
+ codeforerunner-0.4.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
33
+ codeforerunner-0.4.5.dist-info/entry_points.txt,sha256=3p8BbPlq-wfcXk42tsweKePRaGlZ1WXho1gOkuZGyIQ,55
34
+ codeforerunner-0.4.5.dist-info/top_level.txt,sha256=pV1rt0-NIpNEotKXpL_sF2060DHr-_0F86LWhUlvXis,15
35
+ codeforerunner-0.4.5.dist-info/RECORD,,