Hmbown/CodeWhale · error · RuntimeContractError
{kind} metric `{'.'.join((*base, 'tools'))}` must equal the
Error message
{kind} metric `{'.'.join((*base, 'tools'))}` must equal the owned tool_names length ({len(names)}) What it means
For each mode x surface, the integer metric tool_catalog.modes.<mode>.<surface>.tools must exactly equal len(tool_names) in the same node. The count is deliberately redundant with the names list, and validate_identity_structure cross-checks the redundancy so a hand-edited budget cannot hide a tool-count change behind stale names. Raised from the same pass as the sorted-names check (validate_receipt, validate_budget, compare, budget_from_receipt, run_measurement).
Source
Thrown at scripts/check-runtime-contract-budget.py:227
f"got {profile!r}"
)
for mode, _label in VISIBLE_MODES:
for surface, _surface_label in TOOL_SURFACES:
base = ("tool_catalog", "modes", mode, surface)
names = required_value(document, (*base, "tool_names"), kind)
dotted_names = ".".join((*base, "tool_names"))
if (
not isinstance(names, list)
or any(not isinstance(name, str) or not name for name in names)
or names != sorted(set(names))
):
raise RuntimeContractError(
f"{kind} field `{dotted_names}` must be sorted unique non-empty strings"
)
count = metric_value(document, (*base, "tools"), kind)
if count != len(names):
raise RuntimeContractError(
f"{kind} metric `{'.'.join((*base, 'tools'))}` must equal the "
f"owned tool_names length ({len(names)})"
)
digest = required_value(document, (*base, "identity_sha256"), kind)
expected = tool_identity_digest(names)
if digest != expected:
raise RuntimeContractError(
f"{kind} field `{'.'.join((*base, 'identity_sha256'))}` must "
"match the owned sorted tool_names"
)
for stage, _label in REPRESENTATIVE_STAGES:
path = ("representative_context", "stages", stage, "identity_sha256")
digest = required_value(document, path, kind)
if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None:
raise RuntimeContractError(
f"{kind} field `{'.'.join(path)}` must be a lowercase SHA-256 digest"
)View on GitHub (pinned to 8880682c63)
Solutions
- Set the named tools metric to exactly len(tool_names) for that node
- If a tool was genuinely added, update tool_names, tools, bytes, tokens_est, and identity_sha256 as one unit in scripts/runtime-contract-budget.json
- Or re-measure with scripts/measure-runtime-contract.py and check the fresh receipt via --receipt so the harness computes all fields consistently
Example fix
// before
"tool_catalog": { "modes": { "act": { "full": {
"tool_names": ["agent", "apply_patch", "bash"],
"tools": 2 } } } }
// after
"tool_catalog": { "modes": { "act": { "full": {
"tool_names": ["agent", "apply_patch", "bash"],
"tools": 3 } } } } Defensive patterns
Strategy: validation
Validate before calling
def tool_counts_consistent(doc: dict) -> bool:
for mode in ("plan", "act", "operate"):
for surface in ("full", "active"):
node = (
doc.get("tool_catalog", {})
.get("modes", {})
.get(mode, {})
.get(surface, {})
)
names = node.get("tool_names")
count = node.get("tools")
if not isinstance(names, list) or count != len(names):
return False
return True Prevention
- Edit tools count and tool_names only together, in the same commit hunk, so review shows them adjacent
- Prefer regenerating the budget via --update over hand-patching numbers
- Grep your diff for `"tools":` changes and verify each has a matching tool_names diff hunk
When it happens
Trigger: A document where `tools` was edited (or freshly measured) without the matching tool_names edit - budget says "tools": 33 while tool_names lists 34 entries, or a name was appended without bumping the count. The message names the exact node and the expected length.
Common situations: Hand-applying a tool addition to the budget JSON and forgetting the count; copying a tools block between modes/surfaces whose name lists differ; a receipt from a measure-script version that counts differently (for example counting disabled tools).
Related errors
- {kind} field `{dotted_names}` must be sorted unique non-empt
- baseline provenance build profile/sample count changed
- invalid {kind} {path}: {error}
- invalid {kind} {path}: top level must be an object
- {source} document_kind must be `{expected_kind}`, got {actua
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/e82ca4903cf483af.
Report an issue: GitHub.