feat: mr-reviewer and solution-reviewer join the scaffold, so they exist in a repo

Eleven agents lived only in `~/.claude/agents/`, which is not a git repository —
they would have gone with the laptop. Eight of those were product-specific
reviewers already defined in the projects that need them, and they are removed from
the user level rather than duplicated here. Two were genuinely generic and already
product-neutral (zero references to any product, customer or employer), so they
belong in the shared scaffold: `mr-reviewer` reviews open MRs/PRs on a non-primary
remote in a multi-remote repository, and `solution-reviewer` does a whole-codebase
health review.

`irs-validator` is deliberately NOT brought in. It is personal tooling rather than
a development capability, and per the user CLAUDE.md personal tooling must not be
referenced inside a project repository because those are mirrored to other remotes.

The client manifest gains `fetch` and `websearch` capability mappings, which
`solution-reviewer` needs. Worth noting how that surfaced: the sync REFUSED to
render rather than dropping the two tools silently, which is the fail-safe
behaviour the capability mapping exists to provide — an unmapped capability that
rendered as absent would have quietly removed the agent's web access.

`~/.claude/agents/README.md` now records that ten of the eleven agents there are
installed output from this repository, how to regenerate and reinstall them, and
which one is deliberately personal.
This commit is contained in:
2026-09-19 18:20:11 -04:00
parent 3f0786f1e7
commit 7ec467451b
8 changed files with 1219 additions and 2 deletions
+34 -1
View File
@@ -205,6 +205,15 @@ def parse_role(source: Path) -> RoleDefinition:
mutation = metadata["mutation"]
if mutation not in _MUTATION_POLICIES:
raise ValueError(f"{source}: unsupported mutation policy {mutation!r}")
# A name is an identifier and appears in paths, matrix routing and Rust
# constants, so it stays a plain slug. A DESCRIPTION is prose and will
# legitimately contain a colon ("problems: missing indexes on FK columns"),
# so it is not the author's job to avoid YAML metacharacters — the renderer
# quotes it. See `_yaml_scalar`.
if ": " in metadata["name"]:
raise ValueError(
f"{source}: name must be a plain slug, got {metadata['name']!r}"
)
capabilities = tuple(
item.strip() for item in metadata["capabilities"].split(",") if item.strip()
)
@@ -237,13 +246,37 @@ def _mapped_capabilities(role: RoleDefinition, client: ClientDefinition) -> list
return [client.capabilities[name] for name in role.capabilities]
def _yaml_scalar(value: str) -> str:
"""Render a string as YAML that parses back to exactly this string.
A plain (unquoted) scalar cannot contain `: `, cannot contain ` #`, and cannot
begin with an indicator character. HF-2026-001173 shipped a description with
`here: ` in it, and the Rust model-policy gate — which parses frontmatter as
real YAML — refused the file the sync had just called current.
Quoting rather than refusing, because a description is prose: MA's
`data-architect` reads "…for schema-design and database-health problems:
missing indexes on FK/filter/join columns…". Making an author reword that to
satisfy a renderer puts the tool's limitation into the standard's wording.
"""
unsafe = (
": " in value
or " #" in value
or value.rstrip().endswith(":")
or value[:1] in "#&*!|>%@`'\"[]{},"
)
if not unsafe:
return value
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
def _render_claude(role: RoleDefinition, client: ClientDefinition) -> str:
tier = _resolve_tier(role, client)
tools = _mapped_capabilities(role, client)
lines = [
"---",
f"name: {role.name}",
f"description: {role.description}",
f"description: {_yaml_scalar(role.description)}",
]
if tier is None:
# A markdown client declaring tier_policy: unsupported. Not reachable