ultraworkers/claw-code · error · SystemExit

board validation failed:\n{errors}

Error message

board validation failed:\n{errors}

What it means

Raised at the end of build_board() (scripts/generate_cc2_board.py:493-495) as a SystemExit whose message is the newline-joined list returned by validate_board(). It is a self-check gate: after assembling every board item from ROADMAP.md headings/actions and .omx/research JSON, the generator asserts each item has all REQUIRED_ITEM_FIELDS, a unique id, a status in STATUSES, a release_bucket in RELEASE_BUCKETS, list-typed dependencies, and that roadmap heading coverage is complete and non-duplicated (headings_total == headings_mapped, no unmapped or duplicate heading lines). Any violation aborts generation before board.json/board.md are written.

Source

Thrown at scripts/generate_cc2_board.py:495

            "roadmap_headings_total": len(headings),
            "roadmap_headings_mapped": len(mapped_heading_lines),
            "unmapped_roadmap_heading_lines": unmapped_heading_lines,
            "duplicate_roadmap_heading_lines": duplicate_heading_lines,
            "roadmap_actions_total": len(actions),
            "roadmap_actions_mapped": len([item for item in items if item.get("source_type") == "roadmap_action"]),
        },
        "summary": {},
        "items": items,
    }
    board["summary"] = {
        "by_status": summarize_counts(items, "status"),
        "by_release_bucket": summarize_counts(items, "release_bucket"),
        "by_source_type": summarize_counts(items, "source_type"),
        "by_owner_lane": summarize_counts(items, "owner_lane"),
    }
    errors = validate_board(board)
    if errors:
        raise SystemExit("board validation failed:\n" + "\n".join(errors))
    return board


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repo-root", type=Path, default=Path.cwd())
    parser.add_argument("--out-dir", type=Path, default=None)
    args = parser.parse_args()
    repo_root = args.repo_root.resolve()
    out_dir = args.out_dir or (repo_root / ".omx" / "cc2")
    try:
        board = build_board(repo_root)
    except FileNotFoundError as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1
    out_dir.mkdir(parents=True, exist_ok=True)
    board_json = out_dir / "board.json"
    board_md = out_dir / "board.md"

View on GitHub (pinned to b71afddae1)

Solutions

  1. Read the error body first: it names the exact item index/id and rule (e.g. 'CC2-RM-H0012 invalid status in_progress'), so fix the named item or the classifier branch that produced the bad value.
  2. If you intentionally added a new status/bucket, add it to the STATUSES / RELEASE_BUCKETS sets at the top of the file AND to generation_policy.status_values/release_buckets so validation and the emitted policy stay consistent.
  3. For 'duplicate id' on issue items, verify each entry in .omx/research/claw-open-latest.json and claw-issues.json has a unique 'number'; re-export the manifests or give issue_item() a fallback (e.g. hash of url) when number is None.
  4. For coverage errors ('unmapped heading lines', 'total/mapped mismatch'), re-run without local edits to parse_roadmap/roadmap_item and confirm every heading still yields an item; the mapped set is items with source_type == 'roadmap_heading', so ensure your changes don't drop or relabel that field.
  5. For 'missing fields' on custom items, copy the full key set from roadmap_item()/issue_item() (all 9 REQUIRED_ITEM_FIELDS) instead of building a partial dict.
  6. Rerun `python3 scripts/generate_cc2_board.py` after each fix; validation re-executes on every run, so iterate until it writes .omx/cc2/board.json and board.md.

Example fix

# before: classifier emits an unregistered status -> 'board validation failed: CC2-RM-H0007 invalid status in_progress'
def status_for(record):
    ...
    if "wip" in combined:
        return "in_progress"  # not in STATUSES -> validation aborts

# after: register the value in the schema sets, then use it
STATUSES = {
    "context", "active", "open", "done_verify", "stale_done",
    "superseded", "deferred_with_rationale", "rejected_not_claw",
    "in_progress",  # added alongside every consumer (validate_board, render, policy)
}

def status_for(record):
    ...
    if "wip" in combined:
        return "in_progress"
Defensive patterns

Strategy: validation

Validate before calling

import scripts.generate_cc2_board as gen

def items_pass_schema(items: list[dict]) -> list[str]:
    """Dry-run the same rules validate_board() enforces, before wiring items in."""
    problems: list[str] = []
    seen: set = set()
    for i, item in enumerate(items, 1):
        missing = [f for f in gen.REQUIRED_ITEM_FIELDS if f not in item]
        if missing:
            problems.append(f"item {i} missing fields: {missing}")
        if item.get("id") in seen:
            problems.append(f"duplicate id: {item.get('id')}")
        seen.add(item.get("id"))
        if item.get("status") not in gen.STATUSES:
            problems.append(f"{item.get('id')} invalid status {item.get('status')}")
        if item.get("release_bucket") not in gen.RELEASE_BUCKETS:
            problems.append(f"{item.get('id')} invalid release_bucket {item.get('release_bucket')}")
        if not isinstance(item.get("dependencies"), list):
            problems.append(f"{item.get('id')} dependencies must be list")
    return problems

# in tests / CI before regenerating the board:
# assert gen.validate_board(gen.build_board(repo_root)) == []

Type guard

from typing import Any

def is_valid_board_item(item: Any) -> bool:
    required = {
        "id", "title", "source_anchor", "source_type", "release_bucket",
        "status", "dependencies", "verification_required", "deferral_rationale",
    }
    return (
        isinstance(item, dict)
        and required <= item.keys()
        and isinstance(item.get("id"), str)
        and item.get("status") in {
            "context", "active", "open", "done_verify", "stale_done",
            "superseded", "deferred_with_rationale", "rejected_not_claw",
        }
        and item.get("release_bucket") in {
            "alpha_blocker", "beta_adoption", "ga_ecosystem",
            "post_2_0_research", "rejected_not_claw", "context", "2.x_intake",
        }
        and isinstance(item.get("dependencies"), list)
    )

Try / catch

import sys
import scripts.generate_cc2_board as gen

try:
    board = gen.build_board(repo_root)
except SystemExit as exc:
    # exc.code carries the full 'board validation failed:\n<item-level details>' message;
    # parse item ids out of it and fail the CI step with per-item diagnostics
    details = str(exc.code)
    print(f"board regeneration failed schema gate:\n{details}", file=sys.stderr)
    sys.exit(1)

Prevention

When it happens

Trigger: Editing the classification functions (status_for, release_bucket_for, dependencies_for) so a status like 'in_progress' or a bucket like 'beta' is produced that is not in the STATUSES/RELEASE_BUCKETS sets at scripts/generate_cc2_board.py:27-45. Research JSON entries missing the 'number' field: issue_item() then emits id 'CC2-ISSUE-CLAW-OPEN-LATEST-None' for every such issue, triggering 'duplicate id'. Adding a new item type that omits a REQUIRED_ITEM_FIELDS key (e.g. verification_required or deferral_rationale). Filtering/renaming headings in ROADMAP.md inconsistently with the heading-vs-item mapping, producing 'unmapped heading lines' or 'roadmap heading total/mapped mismatch'. Making dependencies a string instead of a list in a custom item builder.

Common situations: Extending the board with a new lifecycle state (e.g. splitting done_verify) while forgetting to register it in STATUSES. Curated research manifests from GraphQL/REST exports where some issues lack fields the script assumes. Hand-rolled item dicts in a fork that drift from the schema. ROADMAP.md edits (removed or duplicated headings) on a branch that the coverage accounting at scripts/generate_cc2_board.py:443-446 then flags. CI regenerating the board after upstream renames of the frozen plan/roadmap structure.

Related errors


AI-assisted analysis of ultraworkers/claw-code@b71afddae1 (2026-08-16). Data as JSON: /api/errors/3c277cb194fdd6e5. Report an issue: GitHub.