codeforerunner 0.4.4__tar.gz → 0.4.6__tar.gz

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 (64) hide show
  1. {codeforerunner-0.4.4/src/codeforerunner.egg-info → codeforerunner-0.4.6}/PKG-INFO +16 -7
  2. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/README.md +15 -6
  3. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/pyproject.toml +2 -2
  4. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/cli.py +54 -45
  5. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/config.py +1 -1
  6. codeforerunner-0.4.6/src/codeforerunner/distribution.py +56 -0
  7. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/doctor.py +25 -44
  8. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/installer.py +128 -126
  9. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/mcp_server.py +30 -29
  10. codeforerunner-0.4.6/src/codeforerunner/prompt_session.py +93 -0
  11. codeforerunner-0.4.6/src/codeforerunner/prompts/tasks/arch-review.md +121 -0
  12. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/init-agent-onboarding.md +14 -1
  13. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/scan.md +2 -0
  14. codeforerunner-0.4.6/src/codeforerunner/release_surfaces.json +77 -0
  15. codeforerunner-0.4.6/src/codeforerunner/release_surfaces.py +115 -0
  16. codeforerunner-0.4.6/src/codeforerunner/skill_parity.py +95 -0
  17. codeforerunner-0.4.6/src/codeforerunner/tasks.json +30 -0
  18. codeforerunner-0.4.6/src/codeforerunner/tasks.py +59 -0
  19. {codeforerunner-0.4.4 → codeforerunner-0.4.6/src/codeforerunner.egg-info}/PKG-INFO +16 -7
  20. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner.egg-info/SOURCES.txt +16 -0
  21. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_check.py +3 -2
  22. codeforerunner-0.4.6/tests/test_check_versions.py +126 -0
  23. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_cli.py +57 -4
  24. codeforerunner-0.4.6/tests/test_distribution.py +54 -0
  25. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_doctor.py +1 -1
  26. codeforerunner-0.4.6/tests/test_inspect_npm_package.py +122 -0
  27. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_installer.py +78 -0
  28. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_mcp_server.py +88 -26
  29. codeforerunner-0.4.6/tests/test_package_metadata.py +35 -0
  30. codeforerunner-0.4.6/tests/test_prompt_session.py +80 -0
  31. codeforerunner-0.4.6/tests/test_release_surfaces.py +87 -0
  32. codeforerunner-0.4.6/tests/test_skill_parity.py +100 -0
  33. codeforerunner-0.4.6/tests/test_tasks.py +85 -0
  34. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_workflows_yaml.py +76 -1
  35. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/LICENSE.md +0 -0
  36. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/setup.cfg +0 -0
  37. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/__init__.py +0 -0
  38. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/bundle.py +0 -0
  39. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/check.py +0 -0
  40. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/partials/context-format.md +0 -0
  41. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/partials/output-rules.md +0 -0
  42. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/partials/stack-hints.md +0 -0
  43. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/system/base.md +0 -0
  44. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/api-docs.md +0 -0
  45. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/audit.md +0 -0
  46. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/changelog.md +0 -0
  47. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/check.md +0 -0
  48. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/diagrams.md +0 -0
  49. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/flows.md +0 -0
  50. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/readme.md +0 -0
  51. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/refresh.md +0 -0
  52. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/review.md +0 -0
  53. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/stack-docs.md +0 -0
  54. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner/prompts/tasks/version-audit.md +0 -0
  55. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner.egg-info/dependency_links.txt +0 -0
  56. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner.egg-info/entry_points.txt +0 -0
  57. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner.egg-info/requires.txt +0 -0
  58. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/src/codeforerunner.egg-info/top_level.txt +0 -0
  59. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_bundle.py +0 -0
  60. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_check_config_integration.py +0 -0
  61. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_config.py +0 -0
  62. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_examples.py +0 -0
  63. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_hooks_manifest.py +0 -0
  64. {codeforerunner-0.4.4 → codeforerunner-0.4.6}/tests/test_validate_codex_marketplace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeforerunner
3
- Version: 0.4.4
3
+ Version: 0.4.6
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.6)](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.6
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  # codeForerunner
4
4
 
5
- [![Socket Badge](https://badge.socket.dev/npm/package/codeforerunner/0.4.3)](https://badge.socket.dev/npm/package/codeforerunner/0.4.3)
5
+ [![Socket Badge](https://badge.socket.dev/npm/package/codeforerunner/0.4.6)](https://socket.dev/npm/package/codeforerunner)
6
6
 
7
7
  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.
8
8
 
@@ -17,10 +17,15 @@ Install forerunner's prompt pack as skills into your agent CLI. Each documentati
17
17
  # One-liner (auto-detects Claude Code, Codex, Gemini CLI)
18
18
  curl -fsSL https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.sh | bash
19
19
 
20
+ # npm
21
+ npx -y codeforerunner
22
+ npm install -g codeforerunner
23
+
20
24
  # Windows
21
25
  irm https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.ps1 | iex
22
26
 
23
- # Via forerunner CLI (after pip install)
27
+ # Via Python CLI
28
+ pip install codeforerunner
24
29
  forerunner install --all claude
25
30
  forerunner install --all codex
26
31
  ```
@@ -43,6 +48,7 @@ Then in your agent:
43
48
  | `/forerunner-api-docs` | `api-docs` | Generate API reference docs |
44
49
  | `/forerunner-diagrams` | `diagrams` | Generate Mermaid architecture diagrams |
45
50
  | `/forerunner-flows` | `flows` | Document system flows |
51
+ | `/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) |
46
52
  | `/forerunner-stack-docs` | `stack-docs` | Stack-specific developer docs |
47
53
  | `/forerunner-version-audit` | `version-audit` | Audit pinned versions vs EOL |
48
54
  | `/forerunner-check` | `check` | Check docs for staleness |
@@ -91,6 +97,9 @@ pip install codeforerunner
91
97
  # Install skills into Claude Code
92
98
  curl -fsSL https://raw.githubusercontent.com/derek-palmer/codeforerunner/main/install.sh | bash
93
99
 
100
+ # Or via npm
101
+ # npx -y codeforerunner
102
+
94
103
  # In Claude Code:
95
104
  # /forerunner-scan → scans your repo
96
105
  # /forerunner-readme → generates README.md
@@ -112,7 +121,7 @@ jobs:
112
121
  runs-on: ubuntu-latest
113
122
  steps:
114
123
  - uses: actions/checkout@v6.0.2
115
- - uses: derek-palmer/codeforerunner@v0.4.3
124
+ - uses: derek-palmer/codeforerunner@v0.4.6
116
125
  with:
117
126
  fail-on-drift: "true" # set "false" to warn-only
118
127
  ```
@@ -197,13 +206,13 @@ prompts/
197
206
  ├── scan.md api-docs.md audit.md
198
207
  ├── readme.md diagrams.md changelog.md
199
208
  ├── check.md flows.md version-audit.md
200
- ├── review.md stack-docs.md refresh.md
209
+ ├── review.md stack-docs.md arch-review.md
210
+ ├── refresh.md
201
211
  └── init-agent-onboarding.md
202
212
  ```
203
213
 
204
- ## Docs and spec
214
+ ## Docs
205
215
 
206
- - `SPEC.md` — canonical phase/task tracker
207
216
  - `docs/getting-started.md` — manual prompt use
208
217
  - `docs/prompt-guide.md` — how system, partial, and task prompts compose
209
218
  - `docs/editor-agent-setup.md` — adapting prompts to local agents
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codeforerunner"
7
- version = "0.4.4"
7
+ version = "0.4.6"
8
8
  description = "Model-agnostic repository documentation tooling (prompt-first; thin CLI)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -50,7 +50,7 @@ forerunner = "codeforerunner.cli:main"
50
50
  where = ["src"]
51
51
 
52
52
  [tool.setuptools.package-data]
53
- codeforerunner = ["py.typed", "prompts/**/*.md"]
53
+ codeforerunner = ["py.typed", "prompts/**/*.md", "tasks.json", "release_surfaces.json"]
54
54
 
55
55
  [tool.pytest.ini_options]
56
56
  testpaths = ["tests"]
@@ -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,77 +8,86 @@ 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 OutcomeKind, 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 _get_bundle(args: argparse.Namespace) -> tuple[str, int]:
18
- """Resolve bundle for args.task. Returns (bundle_text, exit_code). exit_code != 0 on error."""
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
+
26
+ def _resolve_bundle(repo, task: str) -> tuple[str, int]:
27
+ """Resolve bundle text for *task* under *repo*. Returns (text, exit_code).
28
+
29
+ Encodes the session's closed Outcome into CLI exit codes; the gate/order
30
+ lives in the Prompt Session, this is just the encoder.
31
+ """
19
32
  try:
20
- prompts_root = find_prompts_root(args.repo)
33
+ prompts_root = find_prompts_root(repo)
21
34
  except FileNotFoundError as e:
22
35
  print(f"error: {e}", file=sys.stderr)
23
36
  return "", 2
24
37
 
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)
38
+ repo_root = Path(repo).resolve() if repo else Path.cwd()
39
+ session = PromptSession(prompts_root, _scan_satisfied(repo_root))
40
+ outcome = session.resolve(task)
41
+ if outcome.kind is OutcomeKind.ALLOWED:
42
+ return outcome.text, 0
43
+ if outcome.kind is OutcomeKind.UNKNOWN_TASK:
44
+ print(f"error: unknown task '{task}'", file=sys.stderr)
28
45
  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
- ):
46
+ if outcome.kind is OutcomeKind.SCAN_REQUIRED:
36
47
  print(
37
- f"warning: SPEC V2 scan-first — run `forerunner scan` first, "
38
- f"then export {SCAN_DONE_ENV}=1 to silence this warning.",
48
+ f"error: scan-first required — run `forerunner scan` first "
49
+ f"(writes .forerunner/scan.md). Set {SCAN_DONE_ENV}=1 to skip.",
39
50
  file=sys.stderr,
40
51
  )
41
-
42
- try:
43
- return _resolve_bundle(prompts_root, args.task), 0
44
- except FileNotFoundError as e:
45
- print(f"error: {e}", file=sys.stderr)
46
- return "", 2
52
+ return "", 1
53
+ # MISSING
54
+ print(f"error: {outcome.message}", file=sys.stderr)
55
+ return "", 2
47
56
 
48
57
 
49
- def cmd_doc(args: argparse.Namespace) -> int:
50
- """Resolve base + partials + task bundle to stdout."""
51
- bundle, rc = _get_bundle(args)
58
+ def _emit_task(repo, task: str) -> int:
59
+ """Resolve *task* under *repo* and write its bundle to stdout. Returns rc."""
60
+ bundle, rc = _resolve_bundle(repo, task)
52
61
  if rc != 0:
53
62
  return rc
54
63
  sys.stdout.write(bundle)
55
64
  return 0
56
65
 
57
66
 
58
- def _doc_for(args: argparse.Namespace, task: str) -> int:
59
- """Emit bundle for *task* by delegating to cmd_doc with a synthetic Namespace."""
60
- ns = argparse.Namespace(repo=getattr(args, "repo", None), task=task)
61
- return cmd_doc(ns)
67
+ def cmd_doc(args: argparse.Namespace) -> int:
68
+ """Resolve base + partials + task bundle to stdout."""
69
+ return _emit_task(args.repo, args.task)
62
70
 
63
71
 
64
72
  def cmd_init(args: argparse.Namespace) -> int:
65
73
  """Emit onboarding bundle; prepend scan bundle when --full is given."""
74
+ repo = getattr(args, "repo", None)
66
75
  if getattr(args, "full", False):
67
76
  sys.stdout.write("<!-- forerunner init --full: section 1/2 (scan) -->\n")
68
- rc = _doc_for(args, "scan")
77
+ rc = _emit_task(repo, "scan")
69
78
  if rc != 0:
70
79
  return rc
71
80
  sys.stdout.write("\n<!-- forerunner init --full: section 2/2 (onboarding) -->\n")
72
- return _doc_for(args, "init-agent-onboarding")
81
+ return _emit_task(repo, "init-agent-onboarding")
73
82
 
74
83
 
75
84
  def cmd_scan(args: argparse.Namespace) -> int:
76
- """Emit the scan prompt bundle and hint about FORERUNNER_SCAN_DONE."""
77
- rc = _doc_for(args, "scan")
85
+ """Emit the scan prompt bundle and hint about scan artifact."""
86
+ rc = _emit_task(getattr(args, "repo", None), "scan")
78
87
  if rc == 0:
79
88
  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.",
89
+ "hint: write the scan result to .forerunner/scan.md to satisfy the "
90
+ f"scan-first gate on follow-up calls. Or set {SCAN_DONE_ENV}=1 to skip.",
82
91
  file=sys.stderr,
83
92
  )
84
93
  return rc
@@ -111,19 +120,19 @@ def cmd_mcp_server(args: argparse.Namespace) -> int:
111
120
  except FileNotFoundError as e:
112
121
  print(f"mcp_server: {e}", file=sys.stderr)
113
122
  return 2
114
- return mcp_server.serve(prompts_root)
123
+ repo_root = Path(args.repo).resolve() if args.repo else Path.cwd()
124
+ return mcp_server.serve(prompts_root, repo_root=repo_root)
115
125
 
116
126
 
117
127
  def cmd_refresh(args: argparse.Namespace) -> int:
118
128
  """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)
129
+ repo = getattr(args, "repo", None)
130
+ task_names = [t.name for t in _refresh_tasks()]
131
+ for i, task in enumerate(task_names):
132
+ rc = _emit_task(repo, task)
124
133
  if rc != 0:
125
134
  return rc
126
- if i < len(tasks) - 1:
135
+ if i < len(task_names) - 1:
127
136
  sys.stdout.write("\n---\n\n")
128
137
  return 0
129
138
 
@@ -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
 
@@ -0,0 +1,56 @@
1
+ """Distribution Inventory — single source of truth for distribution artifact
2
+ identity and install policy.
3
+
4
+ Owns the packaging facts that were previously duplicated across the installer,
5
+ doctor, and validation scripts: the canonical skill path, its distributed copy
6
+ paths, the Codex marketplace manifest path, the managed-region markers, and the
7
+ default install-destination templates. Consumers consult this module instead of
8
+ re-declaring constants, so a packaging change is one edit here.
9
+
10
+ Mirrors the Task Registry (``tasks.py``) and Release Surface Manifest
11
+ (``release_surfaces.py``) single-source pattern.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+
18
+ # --- artifact identity (repo-relative) ------------------------------------
19
+
20
+ # Source of truth for the codeforerunner skill body; copies derive from it.
21
+ CANONICAL_SKILL_REL = Path("agent/codeforerunner.skill.md")
22
+
23
+ # Distributed copies whose bodies must match the canonical (V10 body parity).
24
+ DISTRIBUTED_SKILL_COPIES_REL: tuple[Path, ...] = (
25
+ Path("plugins/codeforerunner/skills/codeforerunner/SKILL.md"),
26
+ Path("skills/codeforerunner/SKILL.md"),
27
+ )
28
+
29
+ # Codex marketplace manifest shipped as a release asset.
30
+ MARKETPLACE_MANIFEST_REL = Path("plugins/codex/marketplace.json")
31
+
32
+ # --- managed-region markers -----------------------------------------------
33
+
34
+ # Delimit the installer-owned region in a destination file so re-runs are
35
+ # idempotent and unmanaged content is never clobbered.
36
+ MARKER_BEGIN = "<!-- forerunner:begin managed=codeforerunner.skill -->"
37
+ MARKER_END = "<!-- forerunner:end -->"
38
+
39
+ # --- install-destination templates ----------------------------------------
40
+
41
+ # Agents whose default skill destination the inventory can resolve.
42
+ SKILL_DEST_AGENTS: tuple[str, ...] = ("codex", "claude")
43
+
44
+
45
+ def skill_destination(agent: str, slug: str, home: Path) -> Path:
46
+ """Default install path for skill ``slug`` under ``agent``, relative to ``home``."""
47
+ if agent == "codex":
48
+ return home / f".codex/skills/{slug}/SKILL.md"
49
+ if agent == "claude":
50
+ return home / f".claude/plugins/codeforerunner/skills/{slug}/SKILL.md"
51
+ raise ValueError(f"no default skill destination for agent {agent!r} (expected: {', '.join(SKILL_DEST_AGENTS)})")
52
+
53
+
54
+ def marketplace_destination(home: Path) -> Path:
55
+ """Default install path for the Codex marketplace manifest, relative to ``home``."""
56
+ return home / ".codex/marketplaces/codeforerunner.json"
@@ -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
 
@@ -11,17 +11,18 @@ from dataclasses import dataclass
11
11
  from pathlib import Path
12
12
  from typing import Callable
13
13
 
14
+ from codeforerunner import distribution as _dist
15
+ from codeforerunner import skill_parity as _parity
14
16
  from codeforerunner.config import CONFIG_FILENAME, ConfigError, load_from_repo
15
17
 
16
- CANONICAL_REL = Path("agent/codeforerunner.skill.md")
17
- SKILL_COPIES_REL: tuple[Path, ...] = (
18
- Path("plugins/codeforerunner/skills/codeforerunner/SKILL.md"),
19
- Path("skills/codeforerunner/SKILL.md"),
20
- )
21
- MARKETPLACE_REL = Path("plugins/codex/marketplace.json")
18
+ # Distribution artifact identity and markers come from the Distribution
19
+ # Inventory; re-exported here for callers/tests that import them off doctor.
20
+ CANONICAL_REL = _dist.CANONICAL_SKILL_REL
21
+ SKILL_COPIES_REL: tuple[Path, ...] = _dist.DISTRIBUTED_SKILL_COPIES_REL
22
+ MARKETPLACE_REL = _dist.MARKETPLACE_MANIFEST_REL
22
23
 
23
- MARKER_BEGIN = "<!-- forerunner:begin managed=codeforerunner.skill -->"
24
- MARKER_END = "<!-- forerunner:end -->"
24
+ MARKER_BEGIN = _dist.MARKER_BEGIN
25
+ MARKER_END = _dist.MARKER_END
25
26
 
26
27
 
27
28
  @dataclass(frozen=True)
@@ -37,14 +38,14 @@ def _installed_skill_destinations() -> list[Path]:
37
38
  """Return default install paths for the codeforerunner skill across supported agents."""
38
39
  home = Path(os.path.expanduser("~"))
39
40
  return [
40
- home / ".codex/skills/codeforerunner/SKILL.md",
41
- home / ".claude/plugins/codeforerunner/skills/codeforerunner/SKILL.md",
41
+ _dist.skill_destination(agent, "codeforerunner", home)
42
+ for agent in _dist.SKILL_DEST_AGENTS
42
43
  ]
43
44
 
44
45
 
45
46
  def _installed_marketplace_destination() -> Path:
46
47
  """Return default install path for the Codex marketplace manifest."""
47
- return Path(os.path.expanduser("~")) / ".codex/marketplaces/codeforerunner.json"
48
+ return _dist.marketplace_destination(Path(os.path.expanduser("~")))
48
49
 
49
50
 
50
51
  def _load_script_module(repo: Path, relpath: str, module_name: str):
@@ -62,25 +63,14 @@ def _load_script_module(repo: Path, relpath: str, module_name: str):
62
63
 
63
64
 
64
65
  def _check_skill_body_parity(repo: Path, run_scripts: bool = False) -> list[Finding]:
65
- """Verify that all distributed skill copies match the canonical body."""
66
- if not run_scripts:
67
- return [
68
- Finding(
69
- "warn",
70
- "skill-body-parity",
71
- "skipping script validation (pass --run-scripts to allow executing repo scripts)",
72
- )
73
- ]
74
- try:
75
- skill_mod = _load_script_module(
76
- repo, "scripts/validate_skill_copies.py", "_forerunner_doctor_skill_copies"
77
- )
78
- strip_frontmatter: Callable[[str], str] = skill_mod.strip_frontmatter
79
- except Exception as exc: # pragma: no cover - defensive
80
- return [Finding("error", "skill-body-parity", f"loader failure: {exc}")]
81
-
82
- canonical_path = repo / CANONICAL_REL
83
- if not canonical_path.is_file():
66
+ """Verify that all distributed skill copies match the canonical body.
67
+
68
+ Body parity is owned by the Skill Body Parity module, which only reads
69
+ files (no target-repo code is executed), so this runs regardless of
70
+ ``run_scripts`` — that flag still gates checks that load repo scripts.
71
+ """
72
+ result = _parity.check_skill_body_parity(repo)
73
+ if result.missing_canonical:
84
74
  return [
85
75
  Finding(
86
76
  "error",
@@ -88,21 +78,12 @@ def _check_skill_body_parity(repo: Path, run_scripts: bool = False) -> list[Find
88
78
  f"canonical skill missing: {CANONICAL_REL}",
89
79
  )
90
80
  ]
91
- canonical_body = strip_frontmatter(canonical_path.read_text(encoding="utf-8"))
92
81
 
93
82
  findings: list[Finding] = []
94
- for rel in SKILL_COPIES_REL:
95
- p = repo / rel
96
- if not p.is_file():
97
- findings.append(
98
- Finding("error", "skill-body-parity", f"copy missing: {rel}")
99
- )
100
- continue
101
- body = strip_frontmatter(p.read_text(encoding="utf-8"))
102
- if body != canonical_body:
103
- findings.append(
104
- Finding("error", "skill-body-parity", f"body drift in {rel}")
105
- )
83
+ for rel in result.missing_copies:
84
+ findings.append(Finding("error", "skill-body-parity", f"copy missing: {rel}"))
85
+ for rel in result.drifted_copies:
86
+ findings.append(Finding("error", "skill-body-parity", f"body drift in {rel}"))
106
87
  if not findings:
107
88
  findings.append(
108
89
  Finding(