"""Render and idempotently upsert the project ``## Installed Agents`` section. Single source of truth for the agent documentation block written into a project's ``CLAUDE.md``. Both :class:`~kaizen_agentic.installer.AgentInstaller` and the ``kaizen-agentic docs generate`` command reuse these helpers so the section is produced and replaced the same way everywhere — and, critically, **idempotently**: regenerating N times yields the same output as once. """ from __future__ import annotations import re from typing import Dict, Iterable, List from .registry import AgentDefinition SECTION_HEADING = "## Installed Agents" SECTION_FOOTER = ( "Use these agents by referencing them in your Claude Code interactions." ) # Match the Installed Agents block up to the next *top-level* heading (``\n## ``, # with a trailing space so ``### Subsection`` headings inside the block do not # terminate the match) or end-of-file. The previous ``(?=##|\Z)`` form stopped # at the first ``### Category`` subheading and so duplicated the block on every # run (WP-0007 T01). _SECTION_RE = re.compile(r"## Installed Agents.*?(?=\n## (?!#)|\Z)", re.DOTALL) def render_installed_agents_section(agents: Iterable[AgentDefinition]) -> str: """Render the ``## Installed Agents`` markdown block from agent metadata. Agents are grouped by category in first-seen order. The returned string is newline-terminated and contains no trailing top-level heading, so it can be spliced in front of any following section. """ lines: List[str] = [ SECTION_HEADING, "", "This project includes the following specialized agents:", "", ] categories: Dict[str, List[AgentDefinition]] = {} for agent in agents: categories.setdefault(agent.category.value, []).append(agent) for category, members in categories.items(): lines.append(f"### {category.replace('-', ' ').title()}") lines.append("") for agent in members: lines.append(f"- **{agent.name}**: {agent.description}") lines.append("") lines.append(SECTION_FOOTER) lines.append("") return "\n".join(lines) def upsert_installed_agents_section(content: str, section: str) -> str: """Return ``content`` with its Installed Agents block replaced or appended. Idempotent: if ``content`` already contains the block, exactly that block is replaced (never duplicated); otherwise the section is appended. ``section`` is normalised to end with a single trailing blank line. """ section = section.rstrip() + "\n" if SECTION_HEADING in content: return _SECTION_RE.sub(lambda _m: section, content, count=1) separator = "" if content.endswith("\n\n") or not content else "\n" return content + separator + section