From 99cc652fe10a1a233541ea8a348dbfb1df2a6388 Mon Sep 17 00:00:00 2001 From: albertoelopez Date: Mon, 15 Jun 2026 20:22:11 -0700 Subject: [PATCH] fix: warn before missing CC2 source failure --- scripts/generate_cc2_board.py | 36 ++++++++++++++++++++++--------- tests/test_cc2_board_generator.py | 17 +++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/scripts/generate_cc2_board.py b/scripts/generate_cc2_board.py index 7f044a89..d1f3ee7d 100755 --- a/scripts/generate_cc2_board.py +++ b/scripts/generate_cc2_board.py @@ -12,7 +12,7 @@ import sys from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path -from typing import Any +from typing import Any, Callable REQUIRED_ITEM_FIELDS = [ "id", @@ -94,26 +94,42 @@ def slugify(text: str, limit: int = 54) -> str: return slug[:limit].strip("-") or "item" -def find_source_omx(repo_root: Path, env_value: str | None = None) -> Path: +def source_omx_candidates(repo_root: Path, env_value: str | None = None) -> list[Path]: candidates: list[Path] = [] env = env_value if env_value is not None else os.environ.get("CC2_SOURCE_OMX") if env: candidates.append(Path(env).expanduser()) candidates.append(repo_root / ".omx") candidates.extend(parent / ".omx" for parent in repo_root.parents) + return list(dict.fromkeys(candidate.resolve() for candidate in candidates)) + +def format_source_omx_search(paths: list[Path]) -> str: + searched = "\n".join(f" - {path}" for path in paths) + return ( + f"searched:\n{searched}\n" + "Recovery: restore the frozen CC2 source bundle or Set CC2_SOURCE_OMX=/path/to/.omx. " + "The source root must contain plans/claw-code-2-0-adaptive-plan.md and research/." + ) + + +def find_source_omx( + repo_root: Path, + env_value: str | None = None, + warn: Callable[[str], None] | None = None, +) -> Path: checked: list[Path] = [] - for candidate in dict.fromkeys(candidate.resolve() for candidate in candidates): + for candidate in source_omx_candidates(repo_root, env_value): checked.append(candidate) if (candidate / "plans" / "claw-code-2-0-adaptive-plan.md").exists() and (candidate / "research").exists(): return candidate - searched = "\n".join(f" - {path}" for path in checked) + search_message = format_source_omx_search(checked) + if warn is not None: + warn(f"warning: searching for CC2 source .omx\n{search_message}") raise FileNotFoundError( "could not locate source .omx with plans/claw-code-2-0-adaptive-plan.md and research/\n" - f"searched:\n{searched}\n" - "Recovery: restore the frozen CC2 source bundle or Set CC2_SOURCE_OMX=/path/to/.omx. " - "The source root must contain plans/claw-code-2-0-adaptive-plan.md and research/." + f"{search_message}" ) @@ -425,9 +441,9 @@ def validate_board(board: dict[str, Any]) -> list[str]: return errors -def build_board(repo_root: Path) -> dict[str, Any]: +def build_board(repo_root: Path, warn: Callable[[str], None] | None = None) -> dict[str, Any]: roadmap_path = repo_root / "ROADMAP.md" - source_omx = find_source_omx(repo_root) + source_omx = find_source_omx(repo_root, warn=warn) research = source_omx / "research" plan_path = source_omx / "plans" / "claw-code-2-0-adaptive-plan.md" headings, actions = parse_roadmap(roadmap_path) @@ -510,7 +526,7 @@ def main() -> int: repo_root = args.repo_root.resolve() out_dir = args.out_dir or (repo_root / ".omx" / "cc2") try: - board = build_board(repo_root) + board = build_board(repo_root, warn=lambda message: print(message, file=sys.stderr)) except FileNotFoundError as exc: print(f"error: {exc}", file=sys.stderr) return 1 diff --git a/tests/test_cc2_board_generator.py b/tests/test_cc2_board_generator.py index 6aa8f066..f6f0aa81 100644 --- a/tests/test_cc2_board_generator.py +++ b/tests/test_cc2_board_generator.py @@ -39,6 +39,23 @@ class CC2BoardGeneratorTests(unittest.TestCase): self.assertIn("plans/claw-code-2-0-adaptive-plan.md", message) self.assertIn("research/", message) + def test_missing_source_omx_emits_warning_before_failure(self) -> None: + generator = load_generator() + warnings: list[str] = [] + with tempfile.TemporaryDirectory() as temp_dir: + repo_root = Path(temp_dir) / "repo" + repo_root.mkdir() + + with self.assertRaises(FileNotFoundError): + generator.find_source_omx(repo_root, warn=warnings.append) + + self.assertEqual(1, len(warnings)) + warning = warnings[0] + self.assertIn("warning: searching for CC2 source .omx", warning) + self.assertIn("searched:", warning) + self.assertIn(str(repo_root / ".omx"), warning) + self.assertIn("Set CC2_SOURCE_OMX", warning) + def test_source_omx_can_be_supplied_by_env(self) -> None: generator = load_generator() with tempfile.TemporaryDirectory() as temp_dir: