2 Commits

Author SHA1 Message Date
843cf4eee0 feat: agent authoring & doc generation (WP-0007, v1.4.0)
Some checks failed
ci / test (push) Failing after 40s
Publish Python package / publish (push) Successful in 4m46s
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>
2026-06-18 02:06:14 +02:00
7058859e5c chore: add uv.lock for reproducible installs and SBOM ingest
Some checks failed
ci / test (push) Has been cancelled
Generated with uv 0.5.9 (77 packages, full resolution incl. dev group).
Enables State Hub SBOM snapshot ingest for kaizen-agentic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 01:05:58 +02:00
20 changed files with 2996 additions and 90 deletions

View File

@@ -17,7 +17,7 @@ Packaged copies live in `src/kaizen_agentic/data/agents/` for `pip install` dist
|----------|--------| |----------|--------|
| Testing | `tdd-workflow`, `test-maintenance`, `testing-efficiency` | | Testing | `tdd-workflow`, `test-maintenance`, `testing-efficiency` |
| Quality | `code-refactoring`, `datamodel-optimization` | | Quality | `code-refactoring`, `datamodel-optimization` |
| Process | `requirements-engineering`, `keepaTodofile`, `keepaChangelog`, `keepaContributingfile`, `project-management`, `priority-evaluation`, `scope-analyst` | | Process | `requirements-engineering`, `keepaTodofile`, `keepaChangelog`, `keepaContributingfile`, `project-assistant`, `priority-evaluation`, `scope-analyst` |
| Infrastructure | `setupRepository`, `tooling-optimization`, `sys-medic` | | Infrastructure | `setupRepository`, `tooling-optimization`, `sys-medic` |
| Release | `releaseManager` | | Release | `releaseManager` |
| Docs | `claude-documentation` | | Docs | `claude-documentation` |

View File

@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [1.4.0] - 2026-06-18
### Added
- **Agent authoring & doc generation (WP-0007)** — `kaizen-agentic create-agent`
scaffolds a schema-valid agent; `kaizen-agentic docs generate [--check]`
refreshes the CLAUDE.md `## Installed Agents` section idempotently
- **Frontmatter schema validation** — `kaizen-agentic validate` now enforces
required `name`/`description`/`category`, a known category, and valid
`memory`/`model` values with actionable errors
### Fixed
- **Idempotent doc regeneration** — `_update_documentation` no longer duplicates
the `## Installed Agents` block on each run (regex stopped at the first `###`
subheading); rendering is now a shared, idempotent helper
- **Declared category honoured** — agent frontmatter `category` is authoritative
when valid (name/content heuristic is fallback only)
- **Installed-agent resolution** — `list_installed_agents` reads the frontmatter
name, so agents whose filename differs from their name resolve correctly
### Changed
- **Renamed `agent-project-management.md``agent-project-assistant.md`** to
satisfy the `agent-<name>.md` convention (frontmatter `name: project-assistant`);
eliminates a registry name/filename collision
## [1.3.0] - 2026-06-17 ## [1.3.0] - 2026-06-17
### Added ### Added

View File

@@ -12,51 +12,45 @@
This project includes the following specialized agents: This project includes the following specialized agents:
### Testing
- **tdd-workflow**: Expert guidance for the TDD8 workflow methodology, specializing in the comprehensive ISSUE-TEST-RED-GREEN-REFACTOR-DOCUMENT-REFINE-PUBLISH cycle with sophisticated sidequest management and proper test organization.
Use these agents by referencing them in your Claude Code interactions.
### Documentation ### Documentation
- **claude-documentation**: Specialized assistant for Claude and Claude Code documentation, features, and best practices - **claude-documentation**: Specialized assistant for Claude and Claude Code documentation, features, and best practices
- **keepaContributingfile**: Specialized assistant for maintaining CONTRIBUTING.md files following Keep a Contributing-File V0.0.1 format within the Kaizen Agentic framework
- **wisdom-encouragement**: Provides encouraging wisdom and guidance for complex implementation tasks and challenging technical work
### Meta ### Meta
- **coach**: Coaching meta-agent that reads all agent memories in a project and synthesises cross-agent briefs and new-agent orientations - **coach**: Coaching meta-agent that reads all agent memories in a project and synthesises cross-agent briefs and new-agent orientations
- **optimization**: Meta-agent that analyzes and optimizes other Claude Code subagents based on their performance data, usage patterns, and effectiveness metrics. Use PROACTIVELY for agent ecosystem improvement.
### Code Quality ### Code Quality
- **code-refactoring**: Analyze code structure and quality, identify improvement opportunities, and provide actionable refactoring guidance. Use PROACTIVELY for code quality assessment and improvement. - **code-refactoring**: Analyze code structure and quality, identify improvement opportunities, and provide actionable refactoring guidance. Use PROACTIVELY for code quality assessment and improvement.
- **datamodel-optimization**: Specialized agent that systematically analyzes, optimizes, and enhances dataclasses, models, and data structures within a codebase. Provides comprehensive datamodel improvements including convenience methods, interface consistency, code reduction, and test alignment. - **datamodel-optimization**: Specialized agent that systematically analyzes, optimizes, and enhances dataclasses, models, and data structures within a codebase. Provides comprehensive datamodel improvements including convenience methods, interface consistency, code reduction, and test alignment.
- **optimization**: Meta-agent that analyzes and optimizes other Claude Code subagents based on their performance data, usage patterns, and effectiveness metrics. Use PROACTIVELY for agent ecosystem improvement.
- **tooling-optimization**: Meta-agent that analyzes and optimizes repository tooling usage to improve development efficiency
### Project Management ### Project Management
- **keepaChangelog**: Specialized assistant for maintaining CHANGELOG.md files following Keep a Changelog format - **keepaChangelog**: Specialized assistant for maintaining CHANGELOG.md files following Keep a Changelog format
- **keepaContributingfile**: Specialized assistant for maintaining CONTRIBUTING.md files following Keep a Contributing-File V0.0.1 format within the Kaizen Agentic framework
- **keepaTodofile**: Specialized assistant for maintaining TODO.md files following Keep a Todofile V0.0.1 format - **keepaTodofile**: Specialized assistant for maintaining TODO.md files following Keep a Todofile V0.0.1 format
- **priority-evaluation**: Specialized assistant to help evaluate and establish priorities for issues and tasks.
- **project-assistant**: Specialized assistant for project status, progress tracking, and development planning
- **releaseManager**: Manages software releases, version control, and publication workflows for Python packages
- **scope-analyst**: Analyze a repository and produce/improve SCOPE.md for rapid orientation
### Development Process ### Development Process
- **priority-evaluation**: Specialized assistant to help evaluate and establish priorities for issues and tasks.
- **releaseManager**: Manages software releases, version control, and publication workflows for Python packages
- **requirements-engineering**: Specialized agent designed to prevent interface compatibility issues and mock object mismatches by ensuring solid foundation planning before implementation. Based on lessons learned from Issue - **requirements-engineering**: Specialized agent designed to prevent interface compatibility issues and mock object mismatches by ensuring solid foundation planning before implementation. Based on lessons learned from Issue
- **scope-analyst**: Analyze a repository and produce/improve SCOPE.md for rapid orientation - **tdd-workflow**: Expert guidance for the TDD8 workflow methodology, specializing in the comprehensive ISSUE-TEST-RED-GREEN-REFACTOR-DOCUMENT-REFINE-PUBLISH cycle with sophisticated sidequest management and proper test organization.
- **wisdom-encouragement**: Provides encouraging wisdom and guidance for complex implementation tasks and challenging technical work
### Infrastructure ### Infrastructure
- **setupRepository**: Specialized assistant for setting up new Python repositories following PythonVibes best practices - **setupRepository**: Specialized assistant for setting up new Python repositories following PythonVibes best practices
- **sys-medic**: Linux/Kubernetes node health assessment agent — diagnoses process, memory, CPU, disk, network, and kubelet issues with safe, prioritized, evidence-driven guidance - **sys-medic**: Linux/Kubernetes node health assessment agent — diagnoses process, memory, CPU, disk, network, and kubelet issues with safe, prioritized, evidence-driven guidance
- **tooling-optimization**: Meta-agent that analyzes and optimizes repository tooling usage to improve development efficiency
### Testing ### Testing
- **tdd-workflow**: Expert guidance for the TDD8 workflow methodology, specializing in the comprehensive ISSUE-TEST-RED-GREEN-REFACTOR-DOCUMENT-REFINE-PUBLISH cycle with sophisticated sidequest management and proper test organization.
- **test-maintenance**: Specialized agent for analyzing and fixing failing tests in the project - **test-maintenance**: Specialized agent for analyzing and fixing failing tests in the project
- **testing-efficiency**: Specialized agent designed to optimize TDD8 workflow test execution, resolve pytest reliability issues, and enhance overall testing efficiency for red-green iterations. Focuses on smart test selection, parallel execution, and agent integration patterns. - **testing-efficiency**: Specialized agent designed to optimize TDD8 workflow test execution, resolve pytest reliability issues, and enhance overall testing efficiency for red-green iterations. Focuses on smart test selection, parallel execution, and agent integration patterns.
Use these agents by referencing them in your Claude Code interactions. Use these agents by referencing them in your Claude Code interactions.

28
TODO.md
View File

@@ -10,23 +10,27 @@ The structure organizes **future tasks** by their impact, just as a changelog or
## [Unreleased] - *Active Vibe-Coding State* 💡 ## [Unreleased] - *Active Vibe-Coding State* 💡
Tasks in workplan: `workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md` (v1.3.0) Tasks in workplan: `workplans/kaizen-agentic-WP-0007-agent-authoring-doc-generation.md` (v1.4.0)
### Implemented (pending v1.3.0 tag) ### Implemented (pending v1.4.0 tag)
* **`create-agent`** — scaffold schema-valid agents
* **`docs generate [--check]`** — idempotent CLAUDE.md Installed Agents refresh
* **Frontmatter schema validation** in `validate`
* **Doc-regeneration idempotency fix** + agent file rename (project-assistant)
### To Add (release)
* **Tag v1.4.0** — after review
* **activity-core implementation** — WP-0006 resolver + sync (separate repo; see handoff doc)
### Shipped — v1.3.0 (2026-06-17)
* **ADR-005 + `.kaizen/schedule.yml`** — scheduled agent execution contract * **ADR-005 + `.kaizen/schedule.yml`** — scheduled agent execution contract
* **`kaizen-agentic schedule`** — validate, init, prepare, list * **`kaizen-agentic schedule`** — validate, init, prepare, list
* **activity-core definitions** — weekly coach + optimization on preselected repos * **activity-core definitions** — weekly coach + optimization on preselected repos
* **Resolver + roster + event design** — `discover_kaizen_scheduled_repos`,
State Hub roster fields, `kaizen.schedule.prepared` payload, handoff checklist
### To Add (release) ### Deferred / future
* **Tag v1.3.0** — once activity-core handoff issue is opened and pilot smoke-tested * Interactive agent selection wizard (multi-step) — `create-agent` covers the
* **activity-core implementation** — resolver + sync (separate repo; see handoff doc) single-agent scaffold; a guided multi-agent wizard remains future work
* Multi-file agent packages / protocol scaffolding
### Deferred to WP-0007 (v1.3.0+)
* Interactive agent selection wizard
* Agent template schema validation in `validate`
* Documentation generation from agent metadata
*** ***

View File

@@ -52,9 +52,28 @@ kaizen-agentic remove old-agent-name
# Project status # Project status
kaizen-agentic status # Show current project status kaizen-agentic status # Show current project status
kaizen-agentic validate # Validate agent installation kaizen-agentic validate # Validate agents (incl. frontmatter schema)
``` ```
### Authoring & Docs (WP-0007)
```bash
# Scaffold a new schema-valid agent (agents/agent-<name>.md)
kaizen-agentic create-agent my-agent -c testing -d "What it does"
kaizen-agentic create-agent my-agent # prompts for category + description
kaizen-agentic create-agent my-agent --force # overwrite existing
# Refresh the CLAUDE.md "Installed Agents" section from agent metadata
kaizen-agentic docs generate # idempotent rewrite
kaizen-agentic docs generate --check # CI gate: non-zero if out of date
# validate enforces required name/description/category, valid category,
# and valid memory/model values
kaizen-agentic validate
```
After adding or editing agents, run `make agents-sync-package` so the packaged
`data/agents/` copies stay in parity (release-check gate).
### Project Metrics (ADR-004) ### Project Metrics (ADR-004)
```bash ```bash
# Record outcome at session close # Record outcome at session close

View File

@@ -80,6 +80,23 @@ memory: enabled # or: disabled
The `memory` field defaults to `enabled`. Set `memory: disabled` for agents that are stateless by design (e.g. `wisdom-encouragement`). The `memory` field defaults to `enabled`. Set `memory: disabled` for agents that are stateless by design (e.g. `wisdom-encouragement`).
The file name must follow `agent-<name>.md` where `<name>` equals the frontmatter
`name`. `kaizen-agentic validate` enforces the frontmatter schema (required
fields, known `category`, valid `memory`/`model`).
### Authoring and doc sync (WP-0007)
```bash
kaizen-agentic create-agent <name> -c <category> -d "<description>"
kaizen-agentic validate # schema + dependency checks
kaizen-agentic docs generate # refresh CLAUDE.md Installed Agents
make agents-sync-package # keep packaged data/agents/ in parity
```
`create-agent` writes a schema-valid skeleton; `docs generate` rewrites the
project `## Installed Agents` section **idempotently** (use `--check` as a CI
gate). The section is grouped by the declared frontmatter `category`.
--- ---
## The Coach Meta-Agent ## The Coach Meta-Agent

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kaizen-agentic" name = "kaizen-agentic"
version = "1.3.0" version = "1.4.0"
description = "AI agent development framework embracing continuous improvement (kaizen)" description = "AI agent development framework embracing continuous improvement (kaizen)"
readme = "README.md" readme = "README.md"
license = {file = "LICENSE"} license = {file = "LICENSE"}

View File

@@ -9,7 +9,7 @@ It also includes a comprehensive agent distribution system for sharing
specialized agents across projects via CLI tools and package management. specialized agents across projects via CLI tools and package management.
""" """
__version__ = "1.3.0" __version__ = "1.4.0"
__author__ = "Kaizen Agentic Team" __author__ = "Kaizen Agentic Team"
from .core import Agent, AgentConfig from .core import Agent, AgentConfig

View 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

View File

@@ -25,6 +25,10 @@ from .schedule import (
schedule_path, schedule_path,
validate_schedule, validate_schedule,
) )
from .agent_docs import (
render_installed_agents_section,
upsert_installed_agents_section,
)
def safe_cli_wrapper(): def safe_cli_wrapper():
@@ -418,8 +422,21 @@ def validate(target: str):
target_path = Path(target).resolve() target_path = Path(target).resolve()
# Validate agent frontmatter schema
click.echo("Validating agent frontmatter schema...")
schema_errors = registry.validate_frontmatter_schema()
if schema_errors:
click.echo("Frontmatter schema errors:")
for agent_file, errors in schema_errors.items():
click.echo(f" {agent_file}:")
for error in errors:
click.echo(f"{error}")
else:
click.echo(" ✅ Frontmatter schema validation passed")
# Validate registry agents # Validate registry agents
click.echo("Validating agent registry...") click.echo("\nValidating agent registry...")
registry_errors = registry.validate_agents() registry_errors = registry.validate_agents()
if registry_errors: if registry_errors:
@@ -1565,6 +1582,156 @@ def _render_prepare_markdown(bundle: dict) -> str:
return "\n".join(lines) return "\n".join(lines)
@cli.command("create-agent")
@click.argument("name")
@click.option(
"--category",
"-c",
type=click.Choice([c.value for c in AgentCategory]),
help="Agent category (prompted if omitted)",
)
@click.option("--description", "-d", help="One-line description (prompted if omitted)")
@click.option(
"--memory",
type=click.Choice(["enabled", "disabled"]),
default="enabled",
show_default=True,
help="Project memory support",
)
@click.option("--model", help="Optional model hint (e.g. claude-opus-4-8)")
@click.option(
"--target",
"-t",
default=".",
help="Project root containing agents/ (default: current)",
)
@click.option("--force", is_flag=True, help="Overwrite an existing agent file")
def create_agent(
name: str,
category: Optional[str],
description: Optional[str],
memory: str,
model: Optional[str],
target: str,
force: bool,
):
"""Scaffold a new schema-valid agent definition (agents/agent-<name>.md)."""
if not category:
category = click.prompt(
"Category", type=click.Choice([c.value for c in AgentCategory])
)
if not description:
description = click.prompt("One-line description")
agents_dir = _project_root(target) / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
agent_path = agents_dir / f"agent-{name}.md"
if agent_path.exists() and not force:
click.echo(f"Agent already exists: {agent_path}")
click.echo(" Use --force to overwrite.")
sys.exit(1)
frontmatter = [
"---",
f"name: {name}",
f"description: {description}",
f"category: {category}",
f"memory: {memory}",
]
if model:
frontmatter.append(f"model: {model}")
frontmatter.append("---")
title = name.replace("-", " ").title()
body = f"""
# {title} Agent
## Role
<!-- One paragraph: what this agent does and what it does not do. -->
## When to Use
<!-- Triggers and situations where this agent should be invoked. -->
## Instructions
<!-- Step-by-step guidance the agent follows. -->
## Output
<!-- What the agent produces and in what format. -->
"""
agent_path.write_text("\n".join(frontmatter) + "\n" + body)
# Validate the scaffold passes the frontmatter schema (T03).
errors = (
AgentRegistry(agents_dir).validate_frontmatter_schema().get(agent_path.name, [])
)
if errors:
click.echo(f"⚠️ Created {agent_path} but it has schema issues:")
for error in errors:
click.echo(f"{error}")
sys.exit(1)
click.echo(f"✅ Created agent: {agent_path}")
click.echo(" Edit the skeleton, then validate: kaizen-agentic validate")
click.echo(" Before release: make agents-sync-package")
@cli.group()
def docs():
"""Generate project documentation from agent metadata."""
pass
@docs.command("generate")
@click.option("--target", "-t", default=".", help="Project root (default: current)")
@click.option(
"--check",
is_flag=True,
help="Exit non-zero if CLAUDE.md would change (do not write)",
)
def docs_generate(target: str, check: bool):
"""Refresh the '## Installed Agents' section of CLAUDE.md (idempotent)."""
target_path = _project_root(target)
# Resolve agents from the target project's own agents/ when present, so
# `docs generate --target other/project` documents that project's agents
# rather than the registry resolved from the current directory.
local_agents = target_path / "agents"
registry = AgentRegistry(local_agents) if local_agents.exists() else _get_registry()
installer = AgentInstaller(registry)
installed = installer.list_installed_agents(target_path)
if not installed:
click.echo("No agents installed in this project — nothing to document.")
click.echo(" Run: kaizen-agentic install <agents>")
return
agents = [a for a in (registry.get_agent(n) for n in installed) if a is not None]
section = render_installed_agents_section(agents)
claude_md = target_path / "CLAUDE.md"
current = claude_md.read_text() if claude_md.exists() else ""
updated = upsert_installed_agents_section(current, section)
if check:
if updated != current:
click.echo(f"❌ CLAUDE.md is out of date: {claude_md}")
click.echo(" Run: kaizen-agentic docs generate")
sys.exit(1)
click.echo(f"✅ CLAUDE.md is up to date ({len(agents)} agents)")
return
if updated == current:
click.echo(f"CLAUDE.md already up to date ({len(agents)} agents)")
return
claude_md.write_text(updated)
click.echo(f"Updated Installed Agents section ({len(agents)} agents): {claude_md}")
def _project_root(target: str) -> Path: def _project_root(target: str) -> Path:
return Path(target).resolve() return Path(target).resolve()

View File

@@ -6,7 +6,11 @@ from pathlib import Path
from typing import List, Dict, Optional from typing import List, Dict, Optional
from dataclasses import dataclass from dataclasses import dataclass
from .registry import AgentRegistry from .registry import AgentRegistry, AgentDefinition
from .agent_docs import (
render_installed_agents_section,
upsert_installed_agents_section,
)
@dataclass @dataclass
@@ -82,7 +86,15 @@ class AgentInstaller:
installed = [] installed = []
for agent_file in agents_dir.glob("agent-*.md"): for agent_file in agents_dir.glob("agent-*.md"):
agent_name = agent_file.stem.replace("agent-", "") # Prefer the frontmatter name (registry-authoritative); fall back to
# the filename when frontmatter is missing/unreadable. The filename
# encodes the category for a few agents (e.g. agent-project-
# management.md → name: project-assistant), so a pure filename derive
# produces names the registry cannot resolve (WP-0007 T02).
try:
agent_name = AgentDefinition._read_frontmatter(agent_file)["name"]
except Exception:
agent_name = agent_file.stem.replace("agent-", "")
installed.append(agent_name) installed.append(agent_name)
return sorted(installed) return sorted(installed)
@@ -235,60 +247,25 @@ agents-validate:
try: try:
claude_md = project_dir / "CLAUDE.md" claude_md = project_dir / "CLAUDE.md"
agent_section = "## Installed Agents\n\n" agents = [
agent_section += ( agent
"This project includes the following specialized agents:\n\n" for agent in (self.registry.get_agent(name) for name in agent_names)
) if agent is not None
]
agent_section = render_installed_agents_section(agents)
# Group agents by category # Update or create CLAUDE.md (idempotent upsert — WP-0007 T01/T02)
categories = {}
for agent_name in agent_names:
agent = self.registry.get_agent(agent_name)
if agent:
category = agent.category.value
if category not in categories:
categories[category] = []
categories[category].append(agent)
# Generate documentation
for category, agents in categories.items():
agent_section += f"### {category.replace('-', ' ').title()}\n\n"
for agent in agents:
agent_section += f"- **{agent.name}**: {agent.description}\n"
agent_section += "\n"
agent_section += (
"Use these agents by referencing them in your "
"Claude Code interactions.\n\n"
)
# Update or create CLAUDE.md
if claude_md.exists(): if claude_md.exists():
with open(claude_md, "r") as f: content = claude_md.read_text()
content = f.read() content = upsert_installed_agents_section(content, agent_section)
claude_md.write_text(content)
# Replace existing agent section or append
if "## Installed Agents" in content:
import re
content = re.sub(
r"## Installed Agents.*?(?=##|\Z)",
agent_section,
content,
flags=re.DOTALL,
)
else:
content += "\n" + agent_section
with open(claude_md, "w") as f:
f.write(content)
else: else:
# Create new CLAUDE.md
header = "# Claude Code Configuration\n\n" header = "# Claude Code Configuration\n\n"
header += "This file contains Claude Code configuration and agent information.\n\n" header += (
"This file contains Claude Code configuration and agent "
with open(claude_md, "w") as f: "information.\n\n"
f.write(header + agent_section) )
claude_md.write_text(header + agent_section)
print(f"Updated documentation: {claude_md}") print(f"Updated documentation: {claude_md}")

View File

@@ -60,8 +60,10 @@ class AgentDefinition:
# Extract dependencies from frontmatter and content # Extract dependencies from frontmatter and content
dependencies = cls._extract_dependencies(content, frontmatter) dependencies = cls._extract_dependencies(content, frontmatter)
# Determine category from name or content # The declared frontmatter category is authoritative when it is a known
category = cls._determine_category(frontmatter["name"], content) # value (it is what `validate` enforces, WP-0007 T03); fall back to the
# name/content heuristic only when absent or unrecognised.
category = cls._resolve_category(frontmatter, content)
return cls( return cls(
name=frontmatter["name"], name=frontmatter["name"],
@@ -118,6 +120,17 @@ class AgentDefinition:
return dependencies return dependencies
@classmethod
def _resolve_category(cls, frontmatter: dict, content: str) -> AgentCategory:
"""Prefer the declared frontmatter category; heuristic as fallback."""
declared = frontmatter.get("category")
if isinstance(declared, str):
try:
return AgentCategory(declared.strip())
except ValueError:
pass
return cls._determine_category(frontmatter["name"], content)
@staticmethod @staticmethod
def _determine_category(name: str, content: str) -> AgentCategory: def _determine_category(name: str, content: str) -> AgentCategory:
"""Determine agent category based on name and content.""" """Determine agent category based on name and content."""
@@ -288,6 +301,65 @@ class AgentRegistry:
return errors return errors
def validate_frontmatter_schema(self) -> Dict[str, List[str]]:
"""Validate each agent file's frontmatter against the required schema.
Required: ``name``, ``description``, ``category`` (non-empty strings);
``category`` must be a known :class:`AgentCategory` value. Optional:
``memory`` ∈ {enabled, disabled}; ``model`` a non-empty string. Keyed by
filename so files with a missing/invalid ``name`` still surface.
"""
errors: Dict[str, List[str]] = {}
valid_categories = {c.value for c in AgentCategory}
if not self.agents_dir.exists():
return errors
for agent_file in sorted(self.agents_dir.glob("agent-*.md")):
file_errors: List[str] = []
try:
content = agent_file.read_text(encoding="utf-8")
match = re.match(r"^---\n(.*?)\n---\n", content, re.DOTALL)
if not match:
errors[agent_file.name] = ["missing YAML frontmatter"]
continue
frontmatter = yaml.safe_load(match.group(1))
if not isinstance(frontmatter, dict):
errors[agent_file.name] = ["frontmatter is not a mapping"]
continue
for field in ("name", "description", "category"):
value = frontmatter.get(field)
if not isinstance(value, str) or not value.strip():
file_errors.append(f"missing or empty required field: {field}")
category = frontmatter.get("category")
if isinstance(category, str) and category not in valid_categories:
file_errors.append(
f"invalid category '{category}' (expected one of "
f"{', '.join(sorted(valid_categories))})"
)
memory = frontmatter.get("memory")
if memory is not None and memory not in ("enabled", "disabled"):
file_errors.append(
f"invalid memory '{memory}' "
"(expected 'enabled' or 'disabled')"
)
model = frontmatter.get("model")
if model is not None and (
not isinstance(model, str) or not model.strip()
):
file_errors.append("model must be a non-empty string when present")
except Exception as exc: # pragma: no cover - defensive
file_errors.append(f"failed to parse frontmatter: {exc}")
if file_errors:
errors[agent_file.name] = file_errors
return errors
def _has_circular_dependency( def _has_circular_dependency(
self, agent_name: str, visited: Optional[Set[str]] = None self, agent_name: str, visited: Optional[Set[str]] = None
) -> bool: ) -> bool:

97
tests/test_agent_docs.py Normal file
View File

@@ -0,0 +1,97 @@
"""Tests for idempotent agent-docs generation (WP-0007 T01/T02)."""
from __future__ import annotations
from pathlib import Path
import pytest
from click.testing import CliRunner
from kaizen_agentic.agent_docs import (
SECTION_FOOTER,
SECTION_HEADING,
render_installed_agents_section,
upsert_installed_agents_section,
)
from kaizen_agentic.cli import cli
from kaizen_agentic.registry import AgentRegistry
AGENTS_DIR = Path(__file__).parent.parent / "agents"
@pytest.fixture
def runner() -> CliRunner:
return CliRunner()
def _section() -> str:
agents = AgentRegistry(AGENTS_DIR).list_agents()[:4]
return render_installed_agents_section(agents)
class TestUpsertIdempotency:
def test_upsert_is_idempotent(self):
section = _section()
base = f"# Project\n\nintro\n\n{section}\n## Keep Me\nbody\n"
once = upsert_installed_agents_section(base, section)
twice = upsert_installed_agents_section(once, section)
assert once == twice
assert once.count(SECTION_HEADING) == 1
assert once.count(SECTION_FOOTER) == 1
# A following top-level section must survive the replace
assert once.count("## Keep Me") == 1
def test_subheadings_do_not_truncate_section(self):
section = _section()
# The block contains '### Category' subheadings; the replace must not
# stop at the first one (the original regex bug).
merged = upsert_installed_agents_section("# P\n\n", section)
assert merged.count(SECTION_FOOTER) == 1
assert "### " in merged # categories rendered
def test_append_when_absent(self):
section = _section()
merged = upsert_installed_agents_section("# Project\n\nbody\n", section)
assert SECTION_HEADING in merged
assert merged.count(SECTION_FOOTER) == 1
class TestDocsGenerateCli:
def test_generate_then_check_clean(self, runner: CliRunner, tmp_path: Path):
# A project with two installed agents
proj = tmp_path / "proj"
(proj / "agents").mkdir(parents=True)
for name, cat in (("alpha", "testing"), ("beta", "code-quality")):
(proj / "agents" / f"agent-{name}.md").write_text(
f"---\nname: {name}\ndescription: d{name}\ncategory: {cat}\n---\nx\n"
)
gen = runner.invoke(cli, ["docs", "generate", "--target", str(proj)])
assert gen.exit_code == 0
claude = (proj / "CLAUDE.md").read_text()
assert claude.count(SECTION_HEADING) == 1
assert "**alpha**" in claude and "**beta**" in claude
# Second generate is a no-op
again = runner.invoke(cli, ["docs", "generate", "--target", str(proj)])
assert "already up to date" in again.output
# --check passes on a synced repo
check = runner.invoke(
cli, ["docs", "generate", "--check", "--target", str(proj)]
)
assert check.exit_code == 0
assert "up to date" in check.output
def test_check_fails_when_stale(self, runner: CliRunner, tmp_path: Path):
proj = tmp_path / "proj"
(proj / "agents").mkdir(parents=True)
(proj / "agents" / "agent-alpha.md").write_text(
"---\nname: alpha\ndescription: d\ncategory: testing\n---\nx\n"
)
(proj / "CLAUDE.md").write_text("# Proj\n\nno agents section yet\n")
check = runner.invoke(
cli, ["docs", "generate", "--check", "--target", str(proj)]
)
assert check.exit_code == 1
assert "out of date" in check.output

View File

@@ -40,10 +40,15 @@ class TestClickWorkaround:
assert "Got unexpected extra argument" not in stdout_content assert "Got unexpected extra argument" not in stdout_content
assert "Got unexpected extra argument" not in stderr_content assert "Got unexpected extra argument" not in stderr_content
def test_update_command_error_suppression(self): def test_update_command_error_suppression(self, tmp_path):
"""Test that spurious 'unexpected extra argument' errors are suppressed for update commands.""" """Test that spurious 'unexpected extra argument' errors are suppressed for update commands."""
# Seed a temp project so `update` does not rewrite the repo's own agents/
(tmp_path / "agents").mkdir()
(tmp_path / "agents" / "agent-tdd-workflow.md").write_text(
"---\nname: tdd-workflow\ndescription: d\ncategory: testing\n---\nx\n"
)
# Test the update command that also shows spurious errors # Test the update command that also shows spurious errors
with patch("sys.argv", ["kaizen-agentic", "update"]): with patch("sys.argv", ["kaizen-agentic", "update", "--target", str(tmp_path)]):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout: with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr: with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try: try:
@@ -116,9 +121,12 @@ class TestClickWorkaround:
class TestInstallCommandSpecifics: class TestInstallCommandSpecifics:
"""Test specific install command scenarios.""" """Test specific install command scenarios."""
def test_install_with_valid_agent(self): def test_install_with_valid_agent(self, tmp_path):
"""Test install command with a valid agent name.""" """Test install command with a valid agent name."""
with patch("sys.argv", ["kaizen-agentic", "install", "tdd-workflow"]): with patch(
"sys.argv",
["kaizen-agentic", "install", "tdd-workflow", "--target", str(tmp_path)],
):
with patch("sys.stdout", new_callable=StringIO) as mock_stdout: with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
with patch("sys.stderr", new_callable=StringIO) as mock_stderr: with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
try: try:

100
tests/test_create_agent.py Normal file
View File

@@ -0,0 +1,100 @@
"""Tests for the create-agent scaffold (WP-0007 T04)."""
from __future__ import annotations
from pathlib import Path
import pytest
from click.testing import CliRunner
from kaizen_agentic.cli import cli
from kaizen_agentic.registry import AgentRegistry
@pytest.fixture
def runner() -> CliRunner:
return CliRunner()
class TestCreateAgent:
def test_scaffold_is_schema_valid_and_loads(
self, runner: CliRunner, tmp_path: Path
):
result = runner.invoke(
cli,
[
"create-agent",
"demo-helper",
"-c",
"testing",
"-d",
"Demo helper for tests",
"--target",
str(tmp_path),
],
)
assert result.exit_code == 0, result.output
agent_path = tmp_path / "agents" / "agent-demo-helper.md"
assert agent_path.exists()
registry = AgentRegistry(tmp_path / "agents")
# Passes the schema and is loadable by the registry.
assert registry.validate_frontmatter_schema() == {}
agent = registry.get_agent("demo-helper")
assert agent is not None
assert agent.category.value == "testing"
assert agent.memory == "enabled"
def test_interactive_prompts_when_flags_missing(
self, runner: CliRunner, tmp_path: Path
):
result = runner.invoke(
cli,
["create-agent", "prompted", "--target", str(tmp_path)],
input="testing\nA prompted agent\n",
)
assert result.exit_code == 0, result.output
content = (tmp_path / "agents" / "agent-prompted.md").read_text()
assert "category: testing" in content
assert "description: A prompted agent" in content
def test_refuses_overwrite_without_force(self, runner: CliRunner, tmp_path: Path):
args = [
"create-agent",
"dup",
"-c",
"meta",
"-d",
"first",
"--target",
str(tmp_path),
]
assert runner.invoke(cli, args).exit_code == 0
second = runner.invoke(cli, args)
assert second.exit_code == 1
assert "already exists" in second.output
def test_force_overwrites(self, runner: CliRunner, tmp_path: Path):
base = ["create-agent", "dup", "--target", str(tmp_path)]
runner.invoke(cli, base + ["-c", "meta", "-d", "first"])
result = runner.invoke(cli, base + ["-c", "meta", "-d", "second", "--force"])
assert result.exit_code == 0
assert (
"description: second" in (tmp_path / "agents" / "agent-dup.md").read_text()
)
def test_rejects_invalid_category(self, runner: CliRunner, tmp_path: Path):
result = runner.invoke(
cli,
[
"create-agent",
"x",
"-c",
"nonsense",
"-d",
"d",
"--target",
str(tmp_path),
],
)
assert result.exit_code != 0

View File

@@ -0,0 +1,74 @@
"""Tests for agent frontmatter schema validation (WP-0007 T03)."""
from __future__ import annotations
from pathlib import Path
from kaizen_agentic.registry import AgentRegistry
REPO_AGENTS = Path(__file__).parent.parent / "agents"
def _registry(tmp_path: Path, files: dict) -> AgentRegistry:
agents = tmp_path / "agents"
agents.mkdir(parents=True)
for filename, content in files.items():
(agents / filename).write_text(content)
return AgentRegistry(agents)
class TestFrontmatterSchema:
def test_repo_agents_are_schema_valid(self):
# The shipped agents/ must always pass the schema.
assert AgentRegistry(REPO_AGENTS).validate_frontmatter_schema() == {}
def test_good_agent_passes(self, tmp_path: Path):
reg = _registry(
tmp_path,
{
"agent-good.md": (
"---\nname: good\ndescription: A fine agent\n"
"category: testing\nmemory: enabled\n---\nbody\n"
)
},
)
assert reg.validate_frontmatter_schema() == {}
def test_missing_required_fields(self, tmp_path: Path):
reg = _registry(
tmp_path,
{"agent-x.md": "---\nname: x\ncategory: testing\n---\nbody\n"},
)
errors = reg.validate_frontmatter_schema()["agent-x.md"]
assert any("description" in e for e in errors)
def test_invalid_category(self, tmp_path: Path):
reg = _registry(
tmp_path,
{
"agent-x.md": (
"---\nname: x\ndescription: d\ncategory: nonsense\n---\nb\n"
)
},
)
errors = reg.validate_frontmatter_schema()["agent-x.md"]
assert any("invalid category" in e for e in errors)
def test_invalid_memory(self, tmp_path: Path):
reg = _registry(
tmp_path,
{
"agent-x.md": (
"---\nname: x\ndescription: d\ncategory: testing\n"
"memory: maybe\n---\nb\n"
)
},
)
errors = reg.validate_frontmatter_schema()["agent-x.md"]
assert any("invalid memory" in e for e in errors)
def test_missing_frontmatter(self, tmp_path: Path):
reg = _registry(tmp_path, {"agent-x.md": "just text, no frontmatter\n"})
assert reg.validate_frontmatter_schema()["agent-x.md"] == [
"missing YAML frontmatter"
]

2149
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
---
id: KAIZEN-WP-0007
type: workplan
title: "Agent Authoring & Doc Generation (v1.4.0)"
domain: custodian
repo: kaizen-agentic
status: done
owner: kaizen-agentic
topic_slug: custodian
state_hub_workstream_id: a8bc88a4-0ee3-44c6-aff5-9d7f54a316f5
depends_on:
- KAIZEN-WP-0006
created: "2026-06-18"
updated: "2026-06-18"
tasks:
- id: T01
state_hub_task_id: debaf0ac-47df-4bbd-aa1f-96c7b96a64ed
status: done
title: Fix idempotent CLAUDE.md doc regeneration (installer regex bug)
- id: T02
state_hub_task_id: e2d9bea8-243b-4b12-bba3-4d43a3bae71c
status: done
title: Reusable agent-docs module and kaizen-agentic docs generate command
- id: T03
state_hub_task_id: 57176887-5344-444c-aa5a-a4aea161ee71
status: done
title: Enforce agent frontmatter schema in validate
- id: T04
state_hub_task_id: 724e3862-602b-4ef0-88b8-ddb78a225046
status: done
title: kaizen-agentic create-agent scaffold for new agents
- id: T05
state_hub_task_id: 4d6e323c-9cad-49ce-9356-9192c20cc986
status: done
title: Unit tests for docs generate, schema validation, create-agent
- id: T06
state_hub_task_id: 6715aa6f-1ee0-4f22-9249-f1cd41763cd1
status: done
title: Docs, CLI cheat sheet, CHANGELOG for v1.4.0
---
# KAIZEN-WP-0007 — Agent Authoring & Doc Generation
**Status:** done
**Owner:** kaizen-agentic
**Repo:** kaizen-agentic
**Target version:** 1.4.0
**Depends on:** WP-0006 (scheduled agent execution)
## Goal
Close the WP-0005/WP-0006 deferrals — **agent selection/authoring**, **template
schema enforcement**, and **doc generation** — and fix the doc-generation defect
they exposed. After this workplan, authoring a new agent and keeping project
docs in sync is a first-class, idempotent, validated CLI flow.
## Background
`AgentInstaller._update_documentation()` regenerates the `## Installed Agents`
block in a project's `CLAUDE.md`, but its regex
(`## Installed Agents.*?(?=##|\Z)`) is non-greedy and the `(?=##)` lookahead
matches `### Category` subheadings *inside* the section — so each run leaves the
old category lists and footer in place and appends a fresh copy. The block
duplicates and grows unbounded (reported to state-hub; the corrupting write is
ours). `validate` only checks dependencies and file existence — it does not
enforce the agent frontmatter schema — and there is no scaffolding command for
new agents.
## Tasks
### T01 — Fix idempotent doc regeneration
- Anchor the replace to a top-level heading: `(?=\n## (?!#)|\Z)`.
- Add a regression test that runs regeneration twice and asserts exactly one
`## Installed Agents`, one footer, one block per category.
- Clean the existing baseline duplication in this repo's `CLAUDE.md`.
### T02 — Reusable agent-docs module + `docs generate`
- Extract the "Installed Agents" rendering into a pure function
(`render_installed_agents_section`) and an idempotent
`upsert_installed_agents_section(content, section)`.
- `_update_documentation` reuses them (no behavioral fork).
- New `kaizen-agentic docs generate [--target PATH] [--check]` refreshes the
section for installed agents; `--check` exits non-zero if it would change
(CI-friendly).
### T03 — Enforce agent frontmatter schema in `validate`
- Add `validate_frontmatter_schema()` to the registry: required `name`,
`description`, `category`; `category``AgentCategory`; `memory`
{`enabled`,`disabled`} when present; `model` a non-empty string when present.
- Surface schema errors in `kaizen-agentic validate` alongside dependency
errors, with actionable messages.
### T04 — `create-agent` scaffold
- `kaizen-agentic create-agent <name> [--category] [--description] [--memory]
[--target] [--force]` writes a schema-valid `agents/agent-<name>.md` with
frontmatter + section skeleton.
- Missing `--category`/`--description` fall back to interactive prompts.
- Refuses invalid category; refuses overwrite without `--force`; output passes
T03 validation.
### T05 — Tests
- `tests/test_agent_docs.py` — idempotency, `docs generate --check`.
- `tests/test_validate_schema.py` — schema pass/fail cases.
- `tests/test_create_agent.py` — scaffold + round-trips through registry.
### T06 — Docs
- `docs/CLI_CHEAT_SHEET.md` — `docs generate`, `create-agent`, schema validate.
- `docs/agency-framework.md` — authoring + doc-sync note.
- `CHANGELOG.md` `[Unreleased]`; `TODO.md` pointer.
## Definition of Done
- Doc regeneration is idempotent (N runs == 1 run); baseline cleaned.
- `kaizen-agentic validate` fails on malformed agent frontmatter with clear errors.
- `kaizen-agentic create-agent` produces an agent that passes `validate`.
- `kaizen-agentic docs generate --check` is green on a synced repo.
- Full test suite + `make release-check` green.
## Out of Scope
- Multi-file agent packages / protocol scaffolding (separate workplan).
- Publishing generated docs to external knowledge bases (info-tech-canon).
- Changing the agent frontmatter schema itself (only enforcing the current one).
## Notes
- Boundary: the doc-regeneration *trigger* (a state-hub routine invoking the
installer mid-session) is tracked separately in the state-hub inbox; this
workplan fixes the *write* so the trigger becomes harmless/idempotent.
- `create-agent` writes to `agents/`; remember `make agents-sync-package` before
release so packaged `data/agents/` parity holds (existing release-check gate).