New authoring tooling and a fix for the doc-regeneration defect it exposed. Added: - src/kaizen_agentic/agent_docs.py — render + idempotent upsert of the CLAUDE.md "## Installed Agents" section (shared by installer and CLI) - `kaizen-agentic docs generate [--check]` — idempotent doc refresh / CI gate - `kaizen-agentic create-agent` — scaffold a schema-valid agent - Frontmatter schema validation in `kaizen-agentic validate` (required name/description/category, known category, valid memory/model) - tests: test_agent_docs, test_validate_schema, test_create_agent Fixed: - _update_documentation regex duplicated the Installed Agents block on every run (stopped at the first ### subheading) — now idempotent - declared frontmatter `category` is authoritative (heuristic is fallback) - list_installed_agents reads the frontmatter name, not the filename - renamed agent-project-management.md -> agent-project-assistant.md to satisfy the agent-<name>.md convention (eliminates a name/filename collision that caused install/update to write a divergent duplicate) - test_cli_error_handling no longer installs into the repo root (uses tmp) Version 1.4.0; CHANGELOG, CLI cheat sheet, agency-framework, TODO updated. Workplan KAIZEN-WP-0007 closed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
"""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
|