feat: agent authoring & doc generation (WP-0007, v1.4.0)
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>
This commit is contained in:
73
src/kaizen_agentic/agent_docs.py
Normal file
73
src/kaizen_agentic/agent_docs.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user