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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- Treat STATUSES, RELEASE_BUCKETS, and REQUIRED_ITEM_FIELDS as the schema contract: any new lifecycle value or item shape must be registered there in the same change that produces it.
- Add a CI step that regenerates the board (python3 scripts/generate_cc2_board.py) on every ROADMAP.md and .omx/research change, so validation failures surface in the PR that caused them instead of at release time.
- When adding custom item builders, copy the dict shape of roadmap_item()/issue_item() wholesale and assert is_valid_board_item(item) in a unit test rather than hand-writing keys.
- Sanity-check research manifests before use: every issue entry needs a unique 'number', otherwise issue_item() mints colliding 'CC2-ISSUE-...-None' ids.
- Never partially suppress coverage: if you filter headings from the board, adjust the coverage accounting in build_board() in the same edit so headings_total stays equal to headings_mapped.
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.