codeforerunner 0.3.0__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.
@@ -0,0 +1,321 @@
1
+ """`forerunner doctor` — single-screen health report. See SPEC.md §T35."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib.util
7
+ import os
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Callable
12
+
13
+ from codeforerunner.config import CONFIG_FILENAME, ConfigError, load_from_repo
14
+
15
+ CANONICAL_REL = Path("agent/codeforerunner.skill.md")
16
+ SKILL_COPIES_REL: tuple[Path, ...] = (
17
+ Path("plugins/codeforerunner/skills/codeforerunner/SKILL.md"),
18
+ Path("skills/codeforerunner/SKILL.md"),
19
+ )
20
+ MARKETPLACE_REL = Path("plugins/codex/marketplace.json")
21
+
22
+ MARKER_BEGIN = "<!-- forerunner:begin managed=codeforerunner.skill -->"
23
+ MARKER_END = "<!-- forerunner:end -->"
24
+
25
+ _DEFAULT_PROVIDER_ENV = {
26
+ "anthropic": "ANTHROPIC_API_KEY",
27
+ "openai": "OPENAI_API_KEY",
28
+ "google": "GOOGLE_API_KEY",
29
+ "ollama": "OLLAMA_HOST",
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Finding:
35
+ severity: str # "ok" | "warn" | "error"
36
+ check: str
37
+ message: str
38
+
39
+
40
+ def _installed_skill_destinations() -> list[Path]:
41
+ home = Path(os.path.expanduser("~"))
42
+ return [
43
+ home / ".codex/skills/codeforerunner/SKILL.md",
44
+ home / ".claude/plugins/codeforerunner/skills/codeforerunner/SKILL.md",
45
+ ]
46
+
47
+
48
+ def _installed_marketplace_destination() -> Path:
49
+ return Path(os.path.expanduser("~")) / ".codex/marketplaces/codeforerunner.json"
50
+
51
+
52
+ def _load_script_module(repo: Path, relpath: str, module_name: str):
53
+ script_path = repo / relpath
54
+ spec = importlib.util.spec_from_file_location(module_name, script_path)
55
+ if spec is None or spec.loader is None: # pragma: no cover - defensive
56
+ raise RuntimeError(f"cannot load {script_path}")
57
+ module = importlib.util.module_from_spec(spec)
58
+ sys.modules[module_name] = module
59
+ spec.loader.exec_module(module)
60
+ return module
61
+
62
+
63
+ def _check_skill_body_parity(repo: Path) -> list[Finding]:
64
+ try:
65
+ skill_mod = _load_script_module(
66
+ repo, "scripts/validate_skill_copies.py", "_forerunner_doctor_skill_copies"
67
+ )
68
+ strip_frontmatter: Callable[[str], str] = skill_mod.strip_frontmatter
69
+ except Exception as exc: # pragma: no cover - defensive
70
+ return [Finding("error", "skill-body-parity", f"loader failure: {exc}")]
71
+
72
+ canonical_path = repo / CANONICAL_REL
73
+ if not canonical_path.is_file():
74
+ return [
75
+ Finding(
76
+ "error",
77
+ "skill-body-parity",
78
+ f"canonical skill missing: {CANONICAL_REL}",
79
+ )
80
+ ]
81
+ canonical_body = strip_frontmatter(canonical_path.read_text(encoding="utf-8"))
82
+
83
+ findings: list[Finding] = []
84
+ for rel in SKILL_COPIES_REL:
85
+ p = repo / rel
86
+ if not p.is_file():
87
+ findings.append(
88
+ Finding("error", "skill-body-parity", f"copy missing: {rel}")
89
+ )
90
+ continue
91
+ body = strip_frontmatter(p.read_text(encoding="utf-8"))
92
+ if body != canonical_body:
93
+ findings.append(
94
+ Finding("error", "skill-body-parity", f"body drift in {rel}")
95
+ )
96
+ if not findings:
97
+ findings.append(
98
+ Finding(
99
+ "ok",
100
+ "skill-body-parity",
101
+ f"canonical body matches {len(SKILL_COPIES_REL)} distributed copies",
102
+ )
103
+ )
104
+ return findings
105
+
106
+
107
+ def _check_codex_marketplace(repo: Path) -> list[Finding]:
108
+ try:
109
+ mp_mod = _load_script_module(
110
+ repo,
111
+ "scripts/validate_codex_marketplace.py",
112
+ "_forerunner_doctor_codex_marketplace",
113
+ )
114
+ validate: Callable[[Path], list[str]] = mp_mod.validate
115
+ except Exception as exc: # pragma: no cover - defensive
116
+ return [Finding("error", "codex-marketplace", f"loader failure: {exc}")]
117
+
118
+ errors = validate(repo)
119
+ if not errors:
120
+ return [
121
+ Finding("ok", "codex-marketplace", f"{MARKETPLACE_REL} validates")
122
+ ]
123
+ return [Finding("error", "codex-marketplace", msg) for msg in errors]
124
+
125
+
126
+ def _check_installed_destinations(repo: Path) -> list[Finding]:
127
+ findings: list[Finding] = []
128
+
129
+ for dest in _installed_skill_destinations():
130
+ if not dest.exists():
131
+ findings.append(
132
+ Finding(
133
+ "ok",
134
+ "installed-destinations",
135
+ f"{dest}: not installed (skip)",
136
+ )
137
+ )
138
+ continue
139
+ try:
140
+ text = dest.read_text(encoding="utf-8")
141
+ except OSError as exc:
142
+ findings.append(
143
+ Finding(
144
+ "warn",
145
+ "installed-destinations",
146
+ f"{dest}: unreadable ({exc})",
147
+ )
148
+ )
149
+ continue
150
+ if MARKER_BEGIN in text and MARKER_END in text:
151
+ findings.append(
152
+ Finding(
153
+ "ok",
154
+ "installed-destinations",
155
+ f"{dest}: managed (markers present)",
156
+ )
157
+ )
158
+ else:
159
+ findings.append(
160
+ Finding(
161
+ "warn",
162
+ "installed-destinations",
163
+ f"{dest}: exists without managed-region markers (installer will refuse to overwrite)",
164
+ )
165
+ )
166
+
167
+ mp_dest = _installed_marketplace_destination()
168
+ if not mp_dest.exists():
169
+ findings.append(
170
+ Finding(
171
+ "ok",
172
+ "installed-destinations",
173
+ f"{mp_dest}: not installed (skip)",
174
+ )
175
+ )
176
+ else:
177
+ src = repo / MARKETPLACE_REL
178
+ try:
179
+ installed = mp_dest.read_text(encoding="utf-8").rstrip()
180
+ canonical = src.read_text(encoding="utf-8").rstrip()
181
+ except OSError as exc:
182
+ findings.append(
183
+ Finding(
184
+ "warn",
185
+ "installed-destinations",
186
+ f"{mp_dest}: unreadable ({exc})",
187
+ )
188
+ )
189
+ else:
190
+ if installed == canonical:
191
+ findings.append(
192
+ Finding(
193
+ "ok",
194
+ "installed-destinations",
195
+ f"{mp_dest}: matches {MARKETPLACE_REL}",
196
+ )
197
+ )
198
+ else:
199
+ findings.append(
200
+ Finding(
201
+ "warn",
202
+ "installed-destinations",
203
+ f"{mp_dest}: drifted from {MARKETPLACE_REL}",
204
+ )
205
+ )
206
+
207
+ return findings
208
+
209
+
210
+ def _check_config_loadable(repo: Path) -> list[Finding]:
211
+ cfg_path = repo / CONFIG_FILENAME
212
+ if not cfg_path.is_file():
213
+ return [
214
+ Finding(
215
+ "ok",
216
+ "config-loadable",
217
+ f"no {CONFIG_FILENAME}; check is a no-op",
218
+ )
219
+ ]
220
+ try:
221
+ load_from_repo(repo)
222
+ except ConfigError as exc:
223
+ return [Finding("error", "config-loadable", str(exc))]
224
+ return [Finding("ok", "config-loadable", f"{CONFIG_FILENAME} parses cleanly")]
225
+
226
+
227
+ def _check_provider_api_key(repo: Path) -> list[Finding]:
228
+ cfg_path = repo / CONFIG_FILENAME
229
+ if not cfg_path.is_file():
230
+ return [
231
+ Finding(
232
+ "ok",
233
+ "provider-api-key",
234
+ f"no {CONFIG_FILENAME}; provider key not checked",
235
+ )
236
+ ]
237
+ try:
238
+ cfg = load_from_repo(repo)
239
+ except ConfigError:
240
+ # config-loadable check will surface this; skip here
241
+ return [
242
+ Finding(
243
+ "ok",
244
+ "provider-api-key",
245
+ "config unparseable; skipped (see config-loadable)",
246
+ )
247
+ ]
248
+ if cfg is None: # pragma: no cover - defensive
249
+ return [
250
+ Finding(
251
+ "ok",
252
+ "provider-api-key",
253
+ f"no {CONFIG_FILENAME}; provider key not checked",
254
+ )
255
+ ]
256
+ provider = cfg.provider
257
+ if provider == "ollama":
258
+ return [
259
+ Finding(
260
+ "ok",
261
+ "provider-api-key",
262
+ "ollama needs no API key (OLLAMA_HOST optional)",
263
+ )
264
+ ]
265
+ env_var = cfg.api_key_env.get(provider) or _DEFAULT_PROVIDER_ENV.get(provider, "")
266
+ if os.environ.get(env_var):
267
+ return [Finding("ok", "provider-api-key", f"{provider}: {env_var} is set")]
268
+ return [
269
+ Finding(
270
+ "warn",
271
+ "provider-api-key",
272
+ f"{provider}: ${env_var} is not set; `forerunner generate` will refuse to run",
273
+ )
274
+ ]
275
+
276
+
277
+ def run(repo: Path) -> list[Finding]:
278
+ repo = repo.resolve()
279
+ findings: list[Finding] = []
280
+ findings.extend(_check_skill_body_parity(repo))
281
+ findings.extend(_check_codex_marketplace(repo))
282
+ findings.extend(_check_installed_destinations(repo))
283
+ findings.extend(_check_config_loadable(repo))
284
+ findings.extend(_check_provider_api_key(repo))
285
+ return findings
286
+
287
+
288
+ def format_report(findings: list[Finding]) -> str:
289
+ lines = [f"[{f.severity}] {f.check}: {f.message}" for f in findings]
290
+ counts = {"ok": 0, "warn": 0, "error": 0}
291
+ for f in findings:
292
+ counts[f.severity] = counts.get(f.severity, 0) + 1
293
+ summary = (
294
+ f"summary: {counts.get('ok', 0)} ok, "
295
+ f"{counts.get('warn', 0)} warn, "
296
+ f"{counts.get('error', 0)} error"
297
+ )
298
+ lines.append(summary)
299
+ return "\n".join(lines)
300
+
301
+
302
+ def main(argv: list[str] | None = None) -> int:
303
+ parser = argparse.ArgumentParser(
304
+ prog="forerunner doctor",
305
+ description="Single-screen health report for codeforerunner repo.",
306
+ )
307
+ parser.add_argument(
308
+ "--repo",
309
+ type=Path,
310
+ default=Path.cwd(),
311
+ help="repo root (default: cwd)",
312
+ )
313
+ args = parser.parse_args(argv)
314
+
315
+ findings = run(args.repo)
316
+ print(format_report(findings))
317
+ return 1 if any(f.severity == "error" for f in findings) else 0
318
+
319
+
320
+ if __name__ == "__main__": # pragma: no cover
321
+ raise SystemExit(main())
@@ -0,0 +1,304 @@
1
+ """Idempotent skill installer. See SPEC.md §D.installer (cites V8, V10, V11, V12)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import os
8
+ import sys
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Iterable, Literal
12
+
13
+ MARKER_BEGIN = "<!-- forerunner:begin managed=codeforerunner.skill -->"
14
+ MARKER_END = "<!-- forerunner:end -->"
15
+
16
+ CANONICAL_REL = Path("agent/codeforerunner.skill.md")
17
+ MARKETPLACE_REL = Path("plugins/codex/marketplace.json")
18
+
19
+ EXIT_OK = 0
20
+ EXIT_USAGE = 2
21
+ EXIT_BODY_MISMATCH = 3
22
+ EXIT_UNMANAGED_DEST = 4
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Target:
27
+ name: str
28
+ path: Path
29
+
30
+
31
+ def _home() -> Path:
32
+ return Path(os.path.expanduser("~"))
33
+
34
+
35
+ def resolve_target(agent: str, override: Path | None) -> Target:
36
+ if agent == "generic":
37
+ if override is None:
38
+ raise ValueError("generic target requires --path PATH")
39
+ return Target(agent, override.expanduser().resolve())
40
+ if override is not None:
41
+ return Target(agent, override.expanduser().resolve())
42
+ home = _home()
43
+ if agent == "codex":
44
+ return Target(agent, home / ".codex/skills/codeforerunner/SKILL.md")
45
+ if agent == "claude":
46
+ return Target(agent, home / ".claude/plugins/codeforerunner/skills/codeforerunner/SKILL.md")
47
+ raise ValueError(f"unknown agent '{agent}' (expected: codex, claude, generic)")
48
+
49
+
50
+ def resolve_marketplace_target(agent: str, override: Path | None) -> Target:
51
+ if agent == "generic":
52
+ if override is None:
53
+ raise ValueError("generic marketplace target requires --path PATH")
54
+ return Target(agent, override.expanduser().resolve())
55
+ if override is not None:
56
+ return Target(agent, override.expanduser().resolve())
57
+ if agent == "codex":
58
+ return Target(agent, _home() / ".codex/marketplaces/codeforerunner.json")
59
+ raise ValueError(f"marketplace not supported for agent '{agent}' (expected: codex)")
60
+
61
+
62
+ def strip_frontmatter(text: str) -> str:
63
+ """Body extraction matching scripts/validate_skill_copies.py."""
64
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
65
+ lines = text.split("\n")
66
+ if lines and lines[0] == "---":
67
+ for i in range(1, len(lines)):
68
+ if lines[i] == "---":
69
+ return "\n".join(lines[i + 1 :]).strip()
70
+ return text.strip()
71
+
72
+
73
+ def extract_frontmatter(text: str) -> str:
74
+ """Return frontmatter block (incl. fences) or '' if none."""
75
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
76
+ lines = text.split("\n")
77
+ if lines and lines[0] == "---":
78
+ for i in range(1, len(lines)):
79
+ if lines[i] == "---":
80
+ return "\n".join(lines[: i + 1]) + "\n"
81
+ return ""
82
+
83
+
84
+ def _hash(s: str) -> str:
85
+ return hashlib.sha256(s.encode("utf-8")).hexdigest()
86
+
87
+
88
+ def _hash_bytes(b: bytes) -> str:
89
+ return hashlib.sha256(b).hexdigest()
90
+
91
+
92
+ def render(source_text: str, dest_existing: str | None, agent: str) -> str:
93
+ """Render dest content: preserve dest frontmatter if present; wrap source body in markers."""
94
+ body = strip_frontmatter(source_text)
95
+ if dest_existing:
96
+ fm = extract_frontmatter(dest_existing)
97
+ else:
98
+ fm = ""
99
+ managed = f"{MARKER_BEGIN}\n{body}\n{MARKER_END}\n"
100
+ if fm:
101
+ return fm + managed
102
+ return managed
103
+
104
+
105
+ def find_markers(text: str) -> tuple[int, int] | None:
106
+ a = text.find(MARKER_BEGIN)
107
+ if a < 0:
108
+ return None
109
+ b = text.find(MARKER_END, a + len(MARKER_BEGIN))
110
+ if b < 0:
111
+ return None
112
+ return (a, b + len(MARKER_END))
113
+
114
+
115
+ def overlay(dest_text: str, source_body: str) -> str:
116
+ """Replace managed region in-place. Caller has verified markers exist."""
117
+ span = find_markers(dest_text)
118
+ assert span is not None
119
+ a, b = span
120
+ managed = f"{MARKER_BEGIN}\n{source_body}\n{MARKER_END}"
121
+ return dest_text[:a] + managed + dest_text[b:]
122
+
123
+
124
+ @dataclass
125
+ class Plan:
126
+ action: str # "create" | "update" | "skip" | "abort"
127
+ reason: str
128
+ target: Target
129
+ new_content: str | None = None
130
+
131
+ def write(self) -> None:
132
+ if self.action in ("skip", "abort"):
133
+ return
134
+ assert self.new_content is not None
135
+ self.target.path.parent.mkdir(parents=True, exist_ok=True)
136
+ self.target.path.write_text(self.new_content, encoding="utf-8")
137
+
138
+
139
+ def plan_install(
140
+ *,
141
+ source_path: Path,
142
+ canonical_path: Path,
143
+ target: Target,
144
+ ) -> tuple[Plan, int]:
145
+ """Return (plan, exit_code). exit_code ≠ 0 → abort."""
146
+ src_text = source_path.read_text(encoding="utf-8")
147
+ canonical_text = canonical_path.read_text(encoding="utf-8")
148
+
149
+ src_body = strip_frontmatter(src_text)
150
+ canon_body = strip_frontmatter(canonical_text)
151
+ if src_body != canon_body:
152
+ return (
153
+ Plan(
154
+ action="abort",
155
+ reason=(
156
+ f"body-parity violation (V10): source body differs from canonical "
157
+ f"{canonical_path}"
158
+ ),
159
+ target=target,
160
+ ),
161
+ EXIT_BODY_MISMATCH,
162
+ )
163
+
164
+ dest = target.path
165
+ if dest.exists():
166
+ dest_text = dest.read_text(encoding="utf-8")
167
+ if find_markers(dest_text) is None:
168
+ return (
169
+ Plan(
170
+ action="abort",
171
+ reason=(
172
+ f"destination exists without managed markers ({dest}); refusing "
173
+ "to overwrite user content"
174
+ ),
175
+ target=target,
176
+ ),
177
+ EXIT_UNMANAGED_DEST,
178
+ )
179
+ new_text = overlay(dest_text, src_body)
180
+ if _hash(new_text) == _hash(dest_text):
181
+ return Plan(action="skip", reason="dest matches source (V12 idempotent)", target=target), EXIT_OK
182
+ return Plan(action="update", reason="overlay managed region", target=target, new_content=new_text), EXIT_OK
183
+
184
+ new_text = render(src_text, None, target.name)
185
+ return Plan(action="create", reason="dest absent", target=target, new_content=new_text), EXIT_OK
186
+
187
+
188
+ def plan_marketplace(*, source_path: Path, target: Target) -> tuple[Plan, int]:
189
+ """Idempotent JSON manifest install. Hash-equality on whole file (trimmed)."""
190
+ src_bytes = source_path.read_bytes()
191
+ src_trimmed = src_bytes.rstrip()
192
+ src_text = src_bytes.decode("utf-8")
193
+ dest = target.path
194
+ if dest.exists():
195
+ dest_bytes = dest.read_bytes()
196
+ dest_trimmed = dest_bytes.rstrip()
197
+ if _hash_bytes(src_trimmed) == _hash_bytes(dest_trimmed):
198
+ return (
199
+ Plan(action="skip", reason="dest matches source (V12 idempotent)", target=target),
200
+ EXIT_OK,
201
+ )
202
+ return (
203
+ Plan(
204
+ action="abort",
205
+ reason=(
206
+ f"destination exists and differs from source ({dest}); refusing "
207
+ "to overwrite user content"
208
+ ),
209
+ target=target,
210
+ ),
211
+ EXIT_UNMANAGED_DEST,
212
+ )
213
+ return (
214
+ Plan(action="create", reason="dest absent", target=target, new_content=src_text),
215
+ EXIT_OK,
216
+ )
217
+
218
+
219
+ def install(
220
+ *,
221
+ agent: str,
222
+ repo_root: Path,
223
+ source: Path | None,
224
+ dest_override: Path | None,
225
+ check_only: bool,
226
+ kind: Literal["skill", "marketplace"] = "skill",
227
+ out=None,
228
+ err=None,
229
+ ) -> int:
230
+ out = out or sys.stdout
231
+ err = err or sys.stderr
232
+
233
+ if kind == "marketplace":
234
+ try:
235
+ target = resolve_marketplace_target(agent, dest_override)
236
+ except ValueError as e:
237
+ print(f"error: {e}", file=err)
238
+ return EXIT_USAGE
239
+ src_path = source if source is not None else (repo_root / MARKETPLACE_REL)
240
+ if not src_path.is_file():
241
+ print(f"error: marketplace source not found: {src_path}", file=err)
242
+ return EXIT_USAGE
243
+ plan, code = plan_marketplace(source_path=src_path, target=target)
244
+ prefix = "would " if check_only else ""
245
+ stream = err if code != EXIT_OK else out
246
+ print(f"{prefix}{plan.action}: {target.path} ({plan.reason})", file=stream)
247
+ if code != EXIT_OK:
248
+ return code
249
+ if not check_only:
250
+ plan.write()
251
+ return EXIT_OK
252
+
253
+ try:
254
+ target = resolve_target(agent, dest_override)
255
+ except ValueError as e:
256
+ print(f"error: {e}", file=err)
257
+ return EXIT_USAGE
258
+
259
+ canonical = repo_root / CANONICAL_REL
260
+ src_path = source if source is not None else canonical
261
+ if not src_path.is_file():
262
+ print(f"error: source not found: {src_path}", file=err)
263
+ return EXIT_USAGE
264
+ if not canonical.is_file():
265
+ print(f"error: canonical not found: {canonical}", file=err)
266
+ return EXIT_USAGE
267
+
268
+ plan, code = plan_install(source_path=src_path, canonical_path=canonical, target=target)
269
+ prefix = "would " if check_only else ""
270
+ stream = err if code != EXIT_OK else out
271
+ print(f"{prefix}{plan.action}: {target.path} ({plan.reason})", file=stream)
272
+ if code != EXIT_OK:
273
+ return code
274
+ if not check_only:
275
+ plan.write()
276
+ return EXIT_OK
277
+
278
+
279
+ def add_subparser(sub: argparse._SubParsersAction) -> None:
280
+ p = sub.add_parser("install", help="install skill into agent-specific directory (D.installer)")
281
+ p.add_argument("agent", choices=["codex", "claude", "generic"])
282
+ p.add_argument("--check", action="store_true", help="dry-run: print plan, write nothing")
283
+ p.add_argument("--path", type=Path, help="dest path override (required for generic)")
284
+ p.add_argument("--source", type=Path, help="source skill file (default: agent/codeforerunner.skill.md)")
285
+ p.add_argument(
286
+ "--marketplace",
287
+ action="store_true",
288
+ help="install a marketplace manifest (codex only) instead of the skill body",
289
+ )
290
+ p.set_defaults(func=_cli_entry)
291
+
292
+
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)
297
+ return install(
298
+ agent=args.agent,
299
+ repo_root=root,
300
+ source=args.source,
301
+ dest_override=args.path,
302
+ check_only=args.check,
303
+ kind="marketplace" if getattr(args, "marketplace", False) else "skill",
304
+ )