Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 843cf4eee0 | |||
| 7058859e5c |
@@ -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` |
|
||||||
|
|||||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
24
CLAUDE.md
24
CLAUDE.md
@@ -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
28
TODO.md
@@ -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
|
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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
97
tests/test_agent_docs.py
Normal 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
|
||||||
@@ -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
100
tests/test_create_agent.py
Normal 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
|
||||||
74
tests/test_validate_schema.py
Normal file
74
tests/test_validate_schema.py
Normal 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"
|
||||||
|
]
|
||||||
@@ -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).
|
||||||
Reference in New Issue
Block a user