7 Commits

Author SHA1 Message Date
93bf49479b feat: schedule init --engagement for customer bootstrap presets
Some checks failed
ci / test (push) Has been cancelled
Add --engagement, --agents, and --bootstrap-cadence flags to scaffold
hourly/daily/weekly engagement schedules. Hourly bootstrap keeps
cadence: daily with hourly cron overrides per coulomb-loop ADR-003.
Document activity-core requirements in activity-core-handoff-engagement.md.
Closes KAIZEN-WP-0008 T02 and T04.
2026-06-18 08:59:45 +02:00
1641a3165d feat: metrics record --emit-event for kaizen.metrics.recorded
Publish activity-core EventEnvelope payloads to NATS subject
activity.kaizen.metrics.recorded after a successful append.
Optional nats-py via kaizen-agentic[events]; project slug from
KAIZEN_PROJECT_SLUG or directory basename. Skips emit on
idempotency duplicates. Closes KAIZEN-WP-0008 T03.
2026-06-18 08:53:36 +02:00
c5798f58e4 docs: fix supplier ActivityDefinition rule path syntax
Use context.* field paths and {context.*} placeholders to match
activity-core rule evaluator conventions.
2026-06-18 08:11:14 +02:00
7424893758 Mark discover_kaizen_scheduled_repos as implemented in activity-core. 2026-06-18 07:46:46 +02:00
d220bae007 Add coulomb-loop pilot schedule opt-in and supplier engagement workplan.
Commit ADR-005 schedule manifest for hourly bootstrap phase and KAIZEN-WP-0008
to support the coulomb-loop customer engagement.
2026-06-18 04:54:34 +02:00
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
40 changed files with 4153 additions and 114 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

@@ -2,12 +2,23 @@
# Custodian Brief — kaizen-agentic # Custodian Brief — kaizen-agentic
**Domain:** custodian **Domain:** custodian
**Last synced:** 2026-06-16 23:04 UTC **Last synced:** 2026-06-18 02:54 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams ## Active Workstreams
*(none — repo may need first-session setup)* ### Coulomb-loop supplier engagement (customer-repo playbook)
Progress: 1/10 done | workstream_id: `80f473eb-d052-4f50-a633-806f03c469be`
**Open tasks:**
- · Document customer engagement repo layout `177bb16c`
- · Playbook skeleton `90bd0fc2`
- · metrics record --emit-event `26ee0f8d`
- · schedule init --engagement `62324bd2`
- · ADR-006 customer engagement convention `5c06cdd9`
- · Tests `f45077ea`
- · Absorb supplier-notes into playbook v1 `0ef49fb5`
- … and 2 more open tasks
--- ---
## MCP Orientation (when available) ## MCP Orientation (when available)

17
.kaizen/schedule.yml Normal file
View File

@@ -0,0 +1,17 @@
# Kaizen scheduled agent execution (ADR-005)
# Engagement: coulomb-loop bootstrap — hourly crons, daily cadence enum
# Regulator promotes to daily/weekly per ADR-003
version: '1'
timezone: Europe/Berlin
agents:
coach:
cadence: daily
cron: "15 * * * *"
enabled: true
optimization:
cadence: daily
cron: "30 * * * *"
enabled: true
tdd-workflow:
cadence: monthly
enabled: false

View File

@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- **`metrics record --emit-event`** — publishes `kaizen.metrics.recorded` NATS
envelope for activity-core event-driven definitions (optional `nats-py` via
`pip install 'kaizen-agentic[events]'`)
- **`schedule init --engagement`** — bootstrap presets for customer engagements
(`--bootstrap-cadence hourly|daily|weekly`)
- **Event contract** — `docs/integrations/kaizen-metrics-recorded-event.md`
- **Engagement handoff** — `docs/integrations/activity-core-handoff-engagement.md`
## [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
@@ -93,6 +112,8 @@ Session-close template: `docs/templates/session-close-protocol.md`
# Opt this repo into fleet scheduling # Opt this repo into fleet scheduling
kaizen-agentic schedule init # coach + optimization weekly kaizen-agentic schedule init # coach + optimization weekly
kaizen-agentic schedule init --timezone UTC # override timezone kaizen-agentic schedule init --timezone UTC # override timezone
kaizen-agentic schedule init --engagement coulomb-loop \
--agents coach,optimization --bootstrap-cadence hourly
kaizen-agentic schedule validate # schema + agent-name checks kaizen-agentic schedule validate # schema + agent-name checks
kaizen-agentic schedule list # enabled entries (--all incl. disabled) kaizen-agentic schedule list # enabled entries (--all incl. disabled)

View File

@@ -37,7 +37,7 @@ invoke kaizen-agentic CLI commands.
|------------|---------|-------------| |------------|---------|-------------|
| [weekly-metrics-optimize](integrations/activity-definitions/weekly-metrics-optimize.md) | Cron Mon 08:00 | `metrics optimize` | | [weekly-metrics-optimize](integrations/activity-definitions/weekly-metrics-optimize.md) | Cron Mon 08:00 | `metrics optimize` |
| [post-install-metrics-scaffold](integrations/activity-definitions/post-install-metrics-scaffold.md) | `kaizen.agent.installed` | `memory init` validation | | [post-install-metrics-scaffold](integrations/activity-definitions/post-install-metrics-scaffold.md) | `kaizen.agent.installed` | `memory init` validation |
| [low-success-rate-review](integrations/activity-definitions/low-success-rate-review.md) | `kaizen.metrics.recorded` | `metrics show` + `optimize` | | [low-success-rate-review](integrations/activity-definitions/low-success-rate-review.md) | `kaizen.metrics.recorded` | `metrics record --emit-event` |
**Activation handoff (activity-core owners):** **Activation handoff (activity-core owners):**

View File

@@ -17,6 +17,7 @@ Memory-enabled agents record per-session outcomes at close:
```bash ```bash
kaizen-agentic metrics record <agent> --success --time <s> --quality <0-1> kaizen-agentic metrics record <agent> --success --time <s> --quality <0-1>
kaizen-agentic metrics record <agent> --success --time <s> --quality <0-1> --emit-event
kaizen-agentic metrics optimize [agent] kaizen-agentic metrics optimize [agent]
kaizen-agentic memory brief <agent> # includes Performance Summary kaizen-agentic memory brief <agent> # includes Performance Summary
``` ```

View File

@@ -115,6 +115,7 @@ embed it in a task `description` or a runner can parse it.
``` ```
kaizen-agentic schedule init [--target PATH] [--timezone TZ] [--force] kaizen-agentic schedule init [--target PATH] [--timezone TZ] [--force]
kaizen-agentic schedule init --engagement <slug> [--agents A,B] [--bootstrap-cadence hourly|daily|weekly]
kaizen-agentic schedule validate [--target PATH] kaizen-agentic schedule validate [--target PATH]
kaizen-agentic schedule list [--target PATH] [--all] kaizen-agentic schedule list [--target PATH] [--all]
kaizen-agentic schedule prepare <agent> [--target PATH] [--format markdown|json] kaizen-agentic schedule prepare <agent> [--target PATH] [--format markdown|json]

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

@@ -0,0 +1,109 @@
# activity-core Handoff — Customer Engagement Bootstrap (WP-0008)
Coordination requirements for **activity-core** to support coulomb-loop-style
customer engagements after kaizen-agentic ships `schedule init --engagement` and
`metrics record --emit-event`.
Open as an activity-core issue titled *"Engagement bootstrap: event-payload +
bootstrap cadence alignment"*.
## Supplier capabilities (kaizen-agentic — done)
- [x] `schedule init --engagement <slug> --agents coach,optimization --bootstrap-cadence hourly`
- [x] `metrics record --emit-event``activity.kaizen.metrics.recorded`
- [x] Resolver contract: [discover-kaizen-scheduled-repos.md](discover-kaizen-scheduled-repos.md)
## activity-core requirements
### R1 — `event-payload` context resolver (blocks LOOP-WP-0002 event path)
**Problem:** Event-triggered definitions bind `context.metrics` via
`type: event-payload`, but `resolve_context` treats unknown types as `{}`.
**Requirement:** Register a resolver (or inline handler) that, when
`event_envelope_json` is present, binds `EventEnvelope.attributes` to the
`bind_to` key (stripping `context.` prefix).
```yaml
context_sources:
- type: event-payload
bind_to: context.metrics
```
Expected snapshot after resolve:
```python
{"metrics": {"agent": "coach", "project": "kaizen-agentic", "summary": {...}}}
```
**Acceptance:** Manual NATS publish of a [kaizen-metrics-recorded-event](kaizen-metrics-recorded-event.md)
envelope triggers `low-success-rate-review` and evaluates
`context.metrics.summary.success_rate` without binding `{}`.
### R2 — Bootstrap cadence enum vs cron (blocks mis-tuned filters)
**Problem:** Engagement bootstrap writes **hourly cron** expressions but keeps
`cadence: daily` in `.kaizen/schedule.yml` (coulomb-loop ADR-003 convention).
Customer ActivityDefinitions filter `discover_kaizen_scheduled_repos` with
`cadence: daily`.
**Requirement:** Do **not** require `cadence: hourly` in schedule.yml (invalid
in ADR-005 schema). Continue matching on the `cadence` enum field; rely on
per-repo `cron` overrides for hourly firing.
**Acceptance:** Resolver with `cadence: daily` returns pilot repos whose
`schedule.yml` contains `cron: "15 * * * *"` / `"30 * * * *"`.
### R3 — ActivityDefinition cron alignment
Customer coulomb definitions offset from repo schedule crons:
| Definition | activity-core cron | Repo schedule cron |
|------------|-------------------|-------------------|
| hourly-metrics-optimize | `0 * * * *` | (metrics path; no schedule) |
| hourly-coach-orientation | `15 * * * *` | coach `15 * * * *` |
| hourly-optimization-review | `30 * * * *` | optimization `30 * * * *` |
**Requirement:** Keep definition crons and repo schedule crons in sync during
bootstrap. Document in customer handoff that `schedule init --engagement`
presets must match enabled ActivityDefinition offsets.
### R4 — Event router registration
**Requirement:** Ensure `kaizen.metrics.recorded` is an allowed event type in
`event_type_registry` (if registry is enforced on publish path).
**Acceptance:** Event router dispatches to enabled `low-success-rate-review`
definitions when envelope `type` matches.
### R5 — Engagement roster path (optional v1.1)
Customer rosters live in the engagement repo (e.g.
`coulomb-loop/loops/kaizen-stack/roster.yaml`), not in target repos.
**Suggestion:** Support optional resolver param `engagement_slug` that looks up
roster path from a hub field or env `KAIZEN_ENGAGEMENT_ROSTER` so definitions
need not hard-code absolute paths.
## Smoke sequence (end-to-end)
```bash
# Target repo (supplier runs on pilot)
kaizen-agentic schedule init --engagement coulomb-loop \
--agents coach,optimization --bootstrap-cadence hourly --force
kaizen-agentic schedule validate
kaizen-agentic memory init coach && kaizen-agentic memory init optimization
# Record + emit (requires nats-py)
kaizen-agentic metrics record coach --success --time 60 --quality 0.9 --emit-event
# activity-core
# 1. Resolver dry-run: discover_kaizen_scheduled_repos + cadence=daily → 6 runs
# 2. Event dry-run: publish sample envelope → low-success-rate-review tasks
```
## Related
- [customer engagement playbook](customer-engagement-playbook.md) (supplier)
- coulomb-loop `docs/integrations/activity-core-handoff.md` (customer)
- [KAIZEN-WP-0008](../../workplans/kaizen-agentic-WP-0008-coulomb-loop-supplier-engagement.md)

View File

@@ -35,7 +35,9 @@ action:
**Threshold:** 0.8 success rate, minimum 5 executions (avoids noise on early pilots). **Threshold:** 0.8 success rate, minimum 5 executions (avoids noise on early pilots).
**CLI mapping:** Event emitter is future work; manual check today: **CLI mapping:** `kaizen-agentic metrics record <agent> --emit-event` after each
append (see [kaizen-metrics-recorded-event.md](../kaizen-metrics-recorded-event.md)).
Manual check today:
```bash ```bash
kaizen-agentic metrics show <agent> # inspect summary.success_rate kaizen-agentic metrics show <agent> # inspect summary.success_rate

View File

@@ -33,15 +33,15 @@ returns one `scheduled_run` per `(repo, agent)`; this definition selects the
id: run-weekly-coach id: run-weekly-coach
for_each: context.scheduled_runs for_each: context.scheduled_runs
bind_as: r bind_as: r
condition: 'r.agent == "coach" and r.enabled == true' condition: 'context.r.agent == "coach" and context.r.enabled'
action: action:
task_template: "Weekly coach orientation: {{r.repo}}" task_template: "Weekly coach orientation: {context.r.repo}"
description: | description: |
{{r.prepare_command}} {context.r.prepare_command}
Then load agents/agent-coach.md in a coding-agent session, paste the Then load agents/agent-coach.md in a coding-agent session, paste the
prepared bundle, and follow the coach synthesis. At session close: prepared bundle, and follow the coach synthesis. At session close:
kaizen-agentic metrics record coach --success --time <s> --quality <0-1> kaizen-agentic metrics record coach --success --time <s> --quality <0-1>
target_repo: "{{r.repo}}" target_repo: context.r.repo
priority: medium priority: medium
labels: ["kaizen", "agent-run", "coach", "scheduled", "automated"] labels: ["kaizen", "agent-run", "coach", "scheduled", "automated"]
``` ```

View File

@@ -27,13 +27,13 @@ Invokes the kaizen-agentic optimizer CLI per project.
id: run-weekly-optimizer id: run-weekly-optimizer
for_each: context.projects for_each: context.projects
bind_as: p bind_as: p
condition: 'p.has_metrics == true' condition: 'context.p.has_metrics'
action: action:
task_template: "Run kaizen metrics optimize on {{p.repo}}" task_template: "Run kaizen metrics optimize on {context.p.repo}"
description: | description: |
cd {{p.root}} && kaizen-agentic metrics optimize cd {context.p.root} && kaizen-agentic metrics optimize
Optional: kaizen-agentic metrics publish (when artifact-store configured) Optional: kaizen-agentic metrics publish (when artifact-store configured)
target_repo: "{{p.repo}}" target_repo: context.p.repo
priority: medium priority: medium
labels: ["kaizen", "metrics", "optimizer", "automated"] labels: ["kaizen", "metrics", "optimizer", "automated"]
``` ```

View File

@@ -30,16 +30,16 @@ review is evidence-backed.
id: run-weekly-optimization id: run-weekly-optimization
for_each: context.scheduled_runs for_each: context.scheduled_runs
bind_as: r bind_as: r
condition: 'r.agent == "optimization" and r.enabled == true' condition: 'context.r.agent == "optimization" and context.r.enabled'
action: action:
task_template: "Weekly optimization review: {{r.repo}}" task_template: "Weekly optimization review: {context.r.repo}"
description: | description: |
{{r.prepare_command}} {context.r.prepare_command}
kaizen-agentic metrics optimize # refresh evidence kaizen-agentic metrics optimize # refresh evidence
Then load agents/agent-optimization.md, paste the prepared bundle plus the Then load agents/agent-optimization.md, paste the prepared bundle plus the
optimizer report, and act on recommendations. At session close: optimizer report, and act on recommendations. At session close:
kaizen-agentic metrics record optimization --success --time <s> --quality <0-1> kaizen-agentic metrics record optimization --success --time <s> --quality <0-1>
target_repo: "{{r.repo}}" target_repo: context.r.repo
priority: medium priority: medium
labels: ["kaizen", "agent-run", "optimization", "scheduled", "automated"] labels: ["kaizen", "agent-run", "optimization", "scheduled", "automated"]
``` ```

View File

@@ -0,0 +1,66 @@
# Customer Engagement Playbook (supplier)
How kaizen-agentic supports a **customer engagement repo** (e.g. coulomb-loop)
that orchestrates improvement loops across a pilot roster.
## Roles
| Repo | Role |
|------|------|
| Customer (coulomb-loop) | Roster, ActivityDefinition copies, cadence policy, loop health |
| Supplier (kaizen-agentic) | Agents, CLI, integration contracts |
| Target repos | `.kaizen/` state (schedule, memory, metrics) |
| activity-core | Cron + event orchestration, task creation |
## Bootstrap checklist
### 1. Customer repo
- Register engagement in state-hub
- Commit pilot roster (`loops/kaizen-stack/roster.yaml`)
- Copy ActivityDefinitions to `activity-definitions/`
- Enable definitions incrementally (metrics → coach → optimization)
### 2. Target repos (per pilot)
```bash
kaizen-agentic schedule init --engagement <customer-slug> \
--agents coach,optimization --bootstrap-cadence hourly
kaizen-agentic schedule validate
kaizen-agentic memory init coach
kaizen-agentic memory init optimization
```
Hourly bootstrap writes `cadence: daily` with hourly `cron` overrides — see
[activity-core-handoff-engagement.md](activity-core-handoff-engagement.md) R2.
### 3. Session close (each agent run)
```bash
kaizen-agentic metrics record <agent> --success --time <s> --quality <0-1>
kaizen-agentic metrics record <agent> --success --time <s> --quality <0-1> --emit-event
```
### 4. activity-core
Hand off [activity-core-handoff-engagement.md](activity-core-handoff-engagement.md)
requirements before enabling event-driven quality escalation.
## Cadence promotion
Customer regulator (LOOP-WP-0004) approves promotion. Re-init or patch schedules:
```bash
# Stabilize phase
kaizen-agentic schedule init --engagement <slug> \
--bootstrap-cadence daily --force
# Operate phase
kaizen-agentic schedule init --engagement <slug> \
--bootstrap-cadence weekly --force
```
## Related
- [KAIZEN-WP-0008](../../workplans/kaizen-agentic-WP-0008-coulomb-loop-supplier-engagement.md)
- coulomb-loop `INTENT.md` and `workplans/LOOP-WP-*`

View File

@@ -1,6 +1,6 @@
# Resolver Spec: `discover_kaizen_scheduled_repos` # Resolver Spec: `discover_kaizen_scheduled_repos`
**Status:** specification — **implemented in activity-core**, not here. This doc **Status:** implemented in activity-core (`context_resolvers/kaizen.py`). This doc
is the contract an activity-core implementer needs to add the context resolver is the contract an activity-core implementer needs to add the context resolver
that feeds the scheduled-agent ActivityDefinitions (ADR-005). that feeds the scheduled-agent ActivityDefinitions (ADR-005).

View File

@@ -0,0 +1,69 @@
# Event Payload: `kaizen.metrics.recorded`
**Status:** implemented — `kaizen-agentic metrics record --emit-event`
Emitted after a successful metrics append when `--emit-event` is set. Default
off for backward compatibility.
## Subject
```
activity.kaizen.metrics.recorded
```
Published to NATS (activity-core `EventEnvelope` format). Consumed by
activity-core event router and definitions such as `low-success-rate-review`.
## Envelope
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "kaizen.metrics.recorded",
"version": "1.0",
"timestamp": "2026-06-18T12:00:00Z",
"publisher": "kaizen-agentic",
"attributes": {
"agent": "coach",
"project": "kaizen-agentic",
"summary": {
"success_rate": 0.75,
"execution_count": 12,
"avg_quality": 0.81
}
}
}
```
### Attribute fields
| Field | Type | Notes |
|-------|------|-------|
| `agent` | string | Agent name from `metrics record <agent>` |
| `project` | string | Repo slug — `KAIZEN_PROJECT_SLUG` env or directory basename |
| `summary.success_rate` | float | Rolling rate from `summary.json` after append |
| `summary.execution_count` | int | Total executions |
| `summary.avg_quality` | float | Maps from `avg_quality_score` in ADR-004 summary |
## CLI
```bash
kaizen-agentic metrics record coach --success --time 120 --quality 0.9 --emit-event
```
Requires `nats-py` (`pip install 'kaizen-agentic[events]'`). Configure broker via
`NATS_URL` (default `nats://localhost:4222`).
Events are **not** emitted when append is skipped (duplicate idempotency key).
## Consumers
- **activity-core** — `trigger.type: event` with `event_type: kaizen.metrics.recorded`
- **coulomb-loop** — `low-success-rate-review` (LOOP-WP-0002); replaces hourly
health sweep when event path is stable
## Related
- [low-success-rate-review](activity-definitions/low-success-rate-review.md)
- [INTEGRATION_PATTERNS.md Pattern 2](../INTEGRATION_PATTERNS.md)
- coulomb-loop `loops/quality-escalation/event-payload.md` (customer contract)

View File

@@ -70,11 +70,18 @@ the distribution) may appear in a schedule.
kaizen-agentic schedule init # defaults: coach + optimization weekly kaizen-agentic schedule init # defaults: coach + optimization weekly
kaizen-agentic schedule init --timezone UTC # override timezone kaizen-agentic schedule init --timezone UTC # override timezone
kaizen-agentic schedule init --force # overwrite existing kaizen-agentic schedule init --force # overwrite existing
kaizen-agentic schedule init --engagement coulomb-loop \
--agents coach,optimization --bootstrap-cadence hourly
``` ```
The default scaffold enables `coach` and `optimization` weekly and declares The default scaffold enables `coach` and `optimization` weekly and declares
`tdd-workflow` monthly but **disabled** (operator opts in deliberately). `tdd-workflow` monthly but **disabled** (operator opts in deliberately).
**Engagement bootstrap** (`--engagement`) writes customer-loop presets: hourly
bootstrap uses `cadence: daily` with hourly `cron` overrides (coach `:15`,
optimization `:30`). See
[customer-engagement-playbook.md](customer-engagement-playbook.md).
## Listing ## Listing
```bash ```bash

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"}
@@ -32,6 +32,9 @@ dependencies = [
] ]
[project.optional-dependencies] [project.optional-dependencies]
events = [
"nats-py>=2.6.0",
]
dev = [ dev = [
"pytest>=6.0.0", "pytest>=6.0.0",
"pytest-cov>=4.0.0", "pytest-cov>=4.0.0",

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

@@ -15,16 +15,26 @@ from .integrations.artifact_store import (
default_api_url, default_api_url,
publish_optimizer_evidence, publish_optimizer_evidence,
) )
from .integrations.event_bus import (
build_metrics_recorded_envelope,
publish_metrics_recorded_event,
resolve_project_slug,
)
from .integrations.helix import HelixCorrelationAdapter, enrich_helix_correlation from .integrations.helix import HelixCorrelationAdapter, enrich_helix_correlation
from .metrics import MetricsStore, OptimizerStore, performance_summary_markdown from .metrics import MetricsStore, OptimizerStore, performance_summary_markdown
from .optimization import OptimizationLoop, MIN_SAMPLES_FOR_RECOMMENDATIONS from .optimization import OptimizationLoop, MIN_SAMPLES_FOR_RECOMMENDATIONS
from .schedule import ( from .schedule import (
ScheduleError, ScheduleError,
default_schedule_yaml, default_schedule_yaml,
engagement_schedule_yaml,
load_schedule, load_schedule,
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 +428,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:
@@ -1052,6 +1075,11 @@ def metrics():
@click.option( @click.option(
"--json", "json_input", is_flag=True, help="Read full record JSON from stdin" "--json", "json_input", is_flag=True, help="Read full record JSON from stdin"
) )
@click.option(
"--emit-event",
is_flag=True,
help="Publish kaizen.metrics.recorded to NATS (requires nats-py)",
)
def metrics_record( def metrics_record(
agent_name: str, agent_name: str,
target: str, target: str,
@@ -1062,6 +1090,7 @@ def metrics_record(
session_id: Optional[str], session_id: Optional[str],
idempotency_key: Optional[str], idempotency_key: Optional[str],
json_input: bool, json_input: bool,
emit_event: bool,
): ):
"""Append one execution record for an agent.""" """Append one execution record for an agent."""
store = MetricsStore(_project_root(target), agent_name) store = MetricsStore(_project_root(target), agent_name)
@@ -1092,6 +1121,21 @@ def metrics_record(
if store.append(payload, idempotency_key=idempotency_key): if store.append(payload, idempotency_key=idempotency_key):
click.echo(f"Recorded metrics for '{agent_name}'") click.echo(f"Recorded metrics for '{agent_name}'")
if emit_event:
summary = store.read_summary() or store.write_summary()
envelope = build_metrics_recorded_envelope(
agent=agent_name,
project=resolve_project_slug(store.project_root),
summary=summary,
)
try:
subject = publish_metrics_recorded_event(envelope)
except RuntimeError as exc:
click.echo(f"Error: {exc}", err=True)
sys.exit(1)
click.echo(
f"Emitted kaizen.metrics.recorded for '{agent_name}'{subject}"
)
else: else:
click.echo( click.echo(
f"Skipped duplicate record for '{agent_name}' (idempotency key exists)" f"Skipped duplicate record for '{agent_name}' (idempotency key exists)"
@@ -1412,8 +1456,39 @@ def schedule_validate(target: str):
"--timezone", default="Europe/Berlin", show_default=True, help="Schedule timezone" "--timezone", default="Europe/Berlin", show_default=True, help="Schedule timezone"
) )
@click.option("--force", is_flag=True, help="Overwrite an existing schedule.yml") @click.option("--force", is_flag=True, help="Overwrite an existing schedule.yml")
def schedule_init(target: str, timezone: str, force: bool): @click.option(
"""Scaffold a default .kaizen/schedule.yml (coach + optimization weekly).""" "--engagement",
default=None,
help="Customer engagement slug (bootstrap schedule for target repos)",
)
@click.option(
"--agents",
default=None,
help="Comma-separated agents for --engagement (default: coach,optimization)",
)
@click.option(
"--bootstrap-cadence",
type=click.Choice(["hourly", "daily", "weekly"]),
default="hourly",
show_default=True,
help="Cadence preset for --engagement (hourly uses daily enum + hourly cron)",
)
def schedule_init(
target: str,
timezone: str,
force: bool,
engagement: Optional[str],
agents: Optional[str],
bootstrap_cadence: str,
):
"""Scaffold .kaizen/schedule.yml (weekly default or engagement bootstrap)."""
if (agents or bootstrap_cadence != "hourly") and not engagement:
click.echo(
"Error: --agents and --bootstrap-cadence require --engagement",
err=True,
)
sys.exit(1)
path = schedule_path(_project_root(target)) path = schedule_path(_project_root(target))
if path.exists() and not force: if path.exists() and not force:
@@ -1421,9 +1496,41 @@ def schedule_init(target: str, timezone: str, force: bool):
click.echo(" Use --force to overwrite.") click.echo(" Use --force to overwrite.")
return return
if engagement:
agent_list = (
[item.strip() for item in agents.split(",") if item.strip()]
if agents
else None
)
known_agents = _get_registry().agent_names()
if agent_list:
unknown = [name for name in agent_list if name not in known_agents]
if unknown:
click.echo(
f"Error: unknown agent(s) for engagement schedule: {', '.join(unknown)}",
err=True,
)
sys.exit(1)
try:
yaml_text = engagement_schedule_yaml(
engagement,
agents=agent_list,
bootstrap_cadence=bootstrap_cadence,
timezone=timezone,
)
except ScheduleError as exc:
click.echo(f"Error: {exc}", err=True)
sys.exit(1)
else:
yaml_text = default_schedule_yaml(timezone=timezone)
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(default_schedule_yaml(timezone=timezone), encoding="utf-8") path.write_text(yaml_text, encoding="utf-8")
click.echo(f"Initialized schedule: {path}") click.echo(f"Initialized schedule: {path}")
if engagement:
click.echo(
f" Engagement: {engagement} (bootstrap-cadence={bootstrap_cadence})"
)
click.echo(" Validate with: kaizen-agentic schedule validate") click.echo(" Validate with: kaizen-agentic schedule validate")
@@ -1565,6 +1672,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

@@ -1,10 +1,18 @@
"""Ecosystem integration adapters (Helix Forge, artifact-store).""" """Ecosystem integration adapters (Helix Forge, artifact-store, event bus)."""
from .artifact_store import publish_optimizer_evidence from .artifact_store import publish_optimizer_evidence
from .event_bus import (
build_metrics_recorded_envelope,
publish_metrics_recorded_event,
resolve_project_slug,
)
from .helix import HelixCorrelationAdapter, enrich_helix_correlation from .helix import HelixCorrelationAdapter, enrich_helix_correlation
__all__ = [ __all__ = [
"HelixCorrelationAdapter", "HelixCorrelationAdapter",
"build_metrics_recorded_envelope",
"enrich_helix_correlation", "enrich_helix_correlation",
"publish_metrics_recorded_event",
"publish_optimizer_evidence", "publish_optimizer_evidence",
"resolve_project_slug",
] ]

View File

@@ -0,0 +1,95 @@
"""NATS event emission for activity-core integration (Pattern 2)."""
from __future__ import annotations
import asyncio
import json
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Mapping, Optional
EVENT_TYPE_METRICS_RECORDED = "kaizen.metrics.recorded"
DEFAULT_NATS_URL = "nats://localhost:4222"
DEFAULT_PUBLISHER = "kaizen-agentic"
def resolve_project_slug(project_root: Path) -> str:
"""Return state-hub repo slug for a project checkout."""
override = os.environ.get("KAIZEN_PROJECT_SLUG", "").strip()
if override:
return override
return Path(project_root).resolve().name
def metrics_summary_for_event(summary: Mapping[str, Any]) -> Dict[str, Any]:
"""Map ADR-004 summary.json to the LOOP-WP-0002 event contract."""
return {
"success_rate": summary.get("success_rate", 0.0),
"execution_count": summary.get("execution_count", 0),
"avg_quality": summary.get(
"avg_quality",
summary.get("avg_quality_score", 0.0),
),
}
def build_metrics_recorded_envelope(
*,
agent: str,
project: str,
summary: Mapping[str, Any],
event_id: Optional[str] = None,
publisher: str = DEFAULT_PUBLISHER,
) -> Dict[str, Any]:
"""Build an activity-core EventEnvelope dict for kaizen.metrics.recorded."""
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
return {
"id": event_id or str(uuid.uuid4()),
"type": EVENT_TYPE_METRICS_RECORDED,
"version": "1.0",
"timestamp": timestamp,
"publisher": publisher,
"attributes": {
"agent": agent,
"project": project,
"summary": metrics_summary_for_event(summary),
},
}
def nats_subject_for_event(event_type: str) -> str:
"""Subject pattern used by activity-core webhook receiver and event router."""
return f"activity.{event_type}"
async def _publish_bytes(subject: str, payload: bytes, *, nats_url: str) -> None:
try:
import nats
except ImportError as exc:
raise RuntimeError(
"nats-py is required for --emit-event. "
"Install with: pip install 'kaizen-agentic[events]'"
) from exc
nc = await nats.connect(nats_url)
try:
await nc.publish(subject, payload)
await nc.flush()
finally:
await nc.close()
def publish_metrics_recorded_event(
envelope: Mapping[str, Any],
*,
nats_url: Optional[str] = None,
) -> str:
"""Publish envelope to NATS. Returns the subject used."""
url = (nats_url or os.environ.get("NATS_URL") or DEFAULT_NATS_URL).strip()
event_type = str(envelope.get("type", EVENT_TYPE_METRICS_RECORDED))
subject = nats_subject_for_event(event_type)
payload = json.dumps(envelope, sort_keys=True).encode("utf-8")
asyncio.run(_publish_bytes(subject, payload, nats_url=url))
return subject

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:

View File

@@ -27,6 +27,29 @@ DEFAULT_AGENTS: Dict[str, Dict[str, Any]] = {
"tdd-workflow": {"cadence": "monthly", "enabled": False}, "tdd-workflow": {"cadence": "monthly", "enabled": False},
} }
DEFAULT_TIMEZONE = "Europe/Berlin" DEFAULT_TIMEZONE = "Europe/Berlin"
DEFAULT_ENGAGEMENT_AGENTS = ("coach", "optimization")
# Bootstrap cadence presets for customer engagements (coulomb-loop ADR-003).
# Hourly bootstrap keeps cadence enum ``daily`` so activity-core definitions
# filtering ``cadence: daily`` still match while per-repo cron overrides fire
# hourly (see docs/integrations/activity-core-handoff-engagement.md).
ENGAGEMENT_CADENCE_PRESETS: Dict[str, Dict[str, Dict[str, Any]]] = {
"hourly": {
"coach": {"cadence": "daily", "cron": "15 * * * *", "enabled": True},
"optimization": {"cadence": "daily", "cron": "30 * * * *", "enabled": True},
"tdd-workflow": {"cadence": "monthly", "enabled": False},
},
"daily": {
"coach": {"cadence": "daily", "cron": "0 8 * * *", "enabled": True},
"optimization": {"cadence": "daily", "cron": "0 9 * * *", "enabled": True},
"tdd-workflow": {"cadence": "monthly", "enabled": False},
},
"weekly": {
"coach": {"cadence": "weekly", "cron": "0 9 * * 1", "enabled": True},
"optimization": {"cadence": "weekly", "cron": "0 10 * * 1", "enabled": True},
"tdd-workflow": {"cadence": "monthly", "enabled": False},
},
}
class ScheduleError(Exception): class ScheduleError(Exception):
@@ -183,3 +206,56 @@ def default_schedule_yaml(timezone: str = DEFAULT_TIMEZONE) -> str:
) )
body = yaml.safe_dump(document, sort_keys=False, default_flow_style=False) body = yaml.safe_dump(document, sort_keys=False, default_flow_style=False)
return header + body return header + body
def engagement_schedule_yaml(
engagement: str,
*,
agents: Optional[List[str]] = None,
bootstrap_cadence: str = "hourly",
timezone: str = DEFAULT_TIMEZONE,
) -> str:
"""Render a customer-engagement bootstrap schedule for `schedule init --engagement`."""
if bootstrap_cadence not in ENGAGEMENT_CADENCE_PRESETS:
raise ScheduleError(
f"unsupported bootstrap cadence '{bootstrap_cadence}' "
f"(expected one of {', '.join(ENGAGEMENT_CADENCE_PRESETS)})"
)
slug = engagement.strip()
if not slug:
raise ScheduleError("engagement slug must not be empty")
selected = list(agents or DEFAULT_ENGAGEMENT_AGENTS)
if not selected:
raise ScheduleError(
"at least one agent is required for engagement schedule init"
)
preset = ENGAGEMENT_CADENCE_PRESETS[bootstrap_cadence]
agent_entries: Dict[str, Dict[str, Any]] = {}
for name in selected:
if name not in preset:
raise ScheduleError(
f"agent '{name}' has no preset for bootstrap cadence '{bootstrap_cadence}'"
)
agent_entries[name] = dict(preset[name])
if bootstrap_cadence == "hourly":
cadence_note = "hourly crons, daily cadence enum"
else:
cadence_note = f"{bootstrap_cadence} cadence"
document = {
"version": SCHEDULE_VERSION,
"timezone": timezone,
"agents": agent_entries,
}
header = (
"# Kaizen scheduled agent execution manifest (ADR-005)\n"
f"# Engagement: {slug} bootstrap — {cadence_note}\n"
"# Regulator promotes cadence per customer engagement policy (ADR-003).\n"
"# Validate with: kaizen-agentic schedule validate\n"
)
body = yaml.safe_dump(document, sort_keys=False, default_flow_style=False)
return header + body

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,188 @@
"""Tests for kaizen.metrics.recorded event emission."""
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import AsyncMock, patch
import pytest
from click.testing import CliRunner
from kaizen_agentic.cli import cli
from kaizen_agentic.integrations.event_bus import (
EVENT_TYPE_METRICS_RECORDED,
build_metrics_recorded_envelope,
metrics_summary_for_event,
nats_subject_for_event,
publish_metrics_recorded_event,
resolve_project_slug,
)
from kaizen_agentic.metrics import MetricsStore
def test_metrics_summary_for_event_maps_avg_quality_score() -> None:
summary = metrics_summary_for_event(
{
"success_rate": 0.75,
"execution_count": 12,
"avg_quality_score": 0.81,
}
)
assert summary == {
"success_rate": 0.75,
"execution_count": 12,
"avg_quality": 0.81,
}
def test_build_metrics_recorded_envelope_shape() -> None:
envelope = build_metrics_recorded_envelope(
agent="coach",
project="kaizen-agentic",
summary={
"success_rate": 0.9,
"execution_count": 5,
"avg_quality_score": 0.85,
},
event_id="test-event-id",
)
assert envelope["id"] == "test-event-id"
assert envelope["type"] == EVENT_TYPE_METRICS_RECORDED
assert envelope["publisher"] == "kaizen-agentic"
assert envelope["attributes"] == {
"agent": "coach",
"project": "kaizen-agentic",
"summary": {
"success_rate": 0.9,
"execution_count": 5,
"avg_quality": 0.85,
},
}
def test_nats_subject_for_event() -> None:
assert nats_subject_for_event("kaizen.metrics.recorded") == (
"activity.kaizen.metrics.recorded"
)
def test_resolve_project_slug_prefers_env(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setenv("KAIZEN_PROJECT_SLUG", "custom-slug")
assert resolve_project_slug(tmp_path / "some-dir") == "custom-slug"
def test_resolve_project_slug_falls_back_to_directory_name(tmp_path: Path) -> None:
project = tmp_path / "kaizen-agentic"
project.mkdir()
assert resolve_project_slug(project) == "kaizen-agentic"
def test_publish_metrics_recorded_event_uses_activity_subject(
monkeypatch: pytest.MonkeyPatch,
) -> None:
published: dict[str, object] = {}
async def fake_publish(subject: str, payload: bytes, *, nats_url: str) -> None:
published["subject"] = subject
published["payload"] = payload
published["url"] = nats_url
monkeypatch.setattr(
"kaizen_agentic.integrations.event_bus._publish_bytes",
fake_publish,
)
envelope = build_metrics_recorded_envelope(
agent="coach",
project="activity-core",
summary={"success_rate": 1.0, "execution_count": 1, "avg_quality_score": 1.0},
event_id="evt-1",
)
subject = publish_metrics_recorded_event(
envelope, nats_url="nats://broker.test:4222"
)
assert subject == "activity.kaizen.metrics.recorded"
assert published["subject"] == "activity.kaizen.metrics.recorded"
body = json.loads(published["payload"].decode())
assert body["attributes"]["project"] == "activity-core"
def test_metrics_record_emit_event_after_append(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
emitted: list[dict] = []
def capture(envelope, *, nats_url=None):
emitted.append(dict(envelope))
return "activity.kaizen.metrics.recorded"
monkeypatch.setattr(
"kaizen_agentic.cli.publish_metrics_recorded_event",
capture,
)
runner = CliRunner()
result = runner.invoke(
cli,
[
"metrics",
"record",
"coach",
"--target",
str(tmp_path),
"--success",
"--time",
"120",
"--quality",
"0.9",
"--emit-event",
],
)
assert result.exit_code == 0, result.output
assert "Recorded metrics for 'coach'" in result.output
assert "Emitted kaizen.metrics.recorded" in result.output
assert len(emitted) == 1
assert emitted[0]["attributes"]["agent"] == "coach"
assert emitted[0]["attributes"]["project"] == tmp_path.name
assert emitted[0]["attributes"]["summary"]["execution_count"] == 1
store = MetricsStore(tmp_path, "coach")
assert store.read_summary() is not None
def test_metrics_record_skips_emit_on_idempotency_duplicate(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
emitted: list[dict] = []
def capture(envelope, *, nats_url=None):
emitted.append(dict(envelope))
return "activity.kaizen.metrics.recorded"
monkeypatch.setattr(
"kaizen_agentic.cli.publish_metrics_recorded_event",
capture,
)
runner = CliRunner()
common = [
"metrics",
"record",
"coach",
"--target",
str(tmp_path),
"--success",
"--emit-event",
"--idempotency-key",
"session-1",
]
assert runner.invoke(cli, common).exit_code == 0
assert runner.invoke(cli, common).exit_code == 0
assert len(emitted) == 1

View File

@@ -6,11 +6,13 @@ import json
from pathlib import Path from pathlib import Path
import pytest import pytest
import yaml
from click.testing import CliRunner from click.testing import CliRunner
from kaizen_agentic.cli import cli from kaizen_agentic.cli import cli
from kaizen_agentic.schedule import ( from kaizen_agentic.schedule import (
ScheduleError, ScheduleError,
engagement_schedule_yaml,
parse_schedule, parse_schedule,
schedule_path, schedule_path,
validate_schedule, validate_schedule,
@@ -30,6 +32,20 @@ def project_dir(tmp_path: Path) -> Path:
class TestScheduleModule: class TestScheduleModule:
def test_engagement_schedule_yaml_hourly_preset(self):
text = engagement_schedule_yaml(
"coulomb-loop",
agents=["coach", "optimization"],
bootstrap_cadence="hourly",
)
assert "Engagement: coulomb-loop bootstrap" in text
body = "\n".join(line for line in text.splitlines() if not line.startswith("#"))
schedule = parse_schedule(yaml.safe_load(body))
coach = schedule.entry_for("coach")
assert coach is not None
assert coach.cadence == "daily"
assert coach.cron == "15 * * * *"
def test_parse_requires_version(self): def test_parse_requires_version(self):
with pytest.raises(ScheduleError): with pytest.raises(ScheduleError):
parse_schedule({"agents": {}}) parse_schedule({"agents": {}})
@@ -67,6 +83,67 @@ class TestScheduleCli:
assert path.exists() assert path.exists()
assert "coach" in path.read_text() assert "coach" in path.read_text()
def test_engagement_init_hourly_bootstrap(
self, runner: CliRunner, project_dir: Path
):
result = runner.invoke(
cli,
[
"schedule",
"init",
"--target",
str(project_dir),
"--engagement",
"coulomb-loop",
"--agents",
"coach,optimization",
"--bootstrap-cadence",
"hourly",
],
)
assert result.exit_code == 0, result.output
text = schedule_path(project_dir).read_text()
assert "Engagement: coulomb-loop bootstrap" in text
assert "cron: 15 * * * *" in text
assert "cadence: daily" in text
assert "Engagement: coulomb-loop" in result.output
def test_engagement_init_validates_unknown_agent(
self, runner: CliRunner, project_dir: Path
):
result = runner.invoke(
cli,
[
"schedule",
"init",
"--target",
str(project_dir),
"--engagement",
"demo",
"--agents",
"not-a-real-agent",
],
)
assert result.exit_code == 1
assert "unknown agent" in result.output
def test_engagement_flags_require_engagement_slug(
self, runner: CliRunner, project_dir: Path
):
result = runner.invoke(
cli,
[
"schedule",
"init",
"--target",
str(project_dir),
"--agents",
"coach",
],
)
assert result.exit_code == 1
assert "--engagement" in result.output
def test_init_no_overwrite_without_force( def test_init_no_overwrite_without_force(
self, runner: CliRunner, project_dir: Path self, runner: CliRunner, project_dir: Path
): ):

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).

View File

@@ -0,0 +1,302 @@
---
id: KAIZEN-WP-0008
type: workplan
title: "Coulomb-loop supplier engagement (customer-repo playbook)"
domain: custodian
repo: kaizen-agentic
status: active
owner: kaizen-agentic
topic_slug: custodian
customer_repo: coulomb-loop
created: "2026-06-18"
updated: "2026-06-18"
depends_on:
- KAIZEN-WP-0006
- KAIZEN-WP-0004
tasks:
- id: T01
status: todo
title: Document customer engagement repo layout from coulomb-loop reference
- id: T02
status: done
title: Add docs/integrations/customer-engagement-playbook.md skeleton
- id: T03
status: done
title: Implement metrics record --emit-event for kaizen.metrics.recorded
- id: T04
status: done
title: Add schedule init --engagement mode for customer repos
- id: T05
status: done
title: Support pilot schedule init on kaizen-agentic the-custodian activity-core
- id: T06
status: todo
title: Draft ADR-006 customer engagement convention
- id: T07
status: todo
title: Absorb coulomb-loop supplier-notes into playbook v1
- id: T08
status: todo
title: ActivityDefinition override manifest design for hybrid sync
- id: T09
status: todo
title: Tests for emit-event and engagement init
- id: T10
status: todo
title: Update CHANGELOG wiki and cross-link coulomb-loop INTENT
state_hub_workstream_id: "80f473eb-d052-4f50-a633-806f03c469be"
---
# KAIZEN-WP-0008 — Coulomb-loop Supplier Engagement
**Status:** active
**Owner:** kaizen-agentic (supplier)
**Customer:** `coulomb-loop` (coulomb_social domain)
**Depends on:** WP-0006 (schedule contract), WP-0004 (activity-core integration)
## Goal
Deliver supplier capabilities for Coulomb's self-improvement loop engagement and
**generalize learnings** into a reusable customer-repo bootstrap playbook — so the
next engagement requires ≤50% setup effort compared to coulomb-loop.
This workplan is the **supplier mirror** of coulomb-loop LOOP-WP-00010004.
Customer-specific operations stay in `coulomb-loop`; reusable IP stays here.
## Engagement model
```mermaid
flowchart LR
CL[coulomb-loop customer]
KA[kaizen-agentic supplier]
TR[target repos fleet]
AC[activity-core]
CL -->|contracts rosters definitions| AC
KA -->|agents CLI ADRs playbook| CL
KA -->|schedule prepare metrics| TR
AC -->|tasks| TR
```
See coulomb-loop `docs/adr/ADR-002-customer-supplier-boundary.md`.
## Sequencing (per DEC-004 default — smoke-first)
```
Part 1 (T01T02, T05) ── parallel with coulomb-loop smoke test
Part 2 (T03T04, T06T09) ── after first hourly E2E pass
Part 3 (T07T08, T10) ── after LOOP-WP-0004 supplier-notes available
```
---
## Part 1 — Document and support smoke test
## Document customer engagement repo layout
```task
id: KAIZEN-WP-0008-T01
status: todo
priority: high
state_hub_task_id: "177bb16c-6239-43f2-8d99-f4498c31d74a"
```
Create `docs/integrations/customer-engagement-repo-layout.md` from coulomb-loop
reference:
```
customer-repo/
INTENT.md SCOPE.md
workplans/LOOP-WP-* or <PREFIX>-WP-*
docs/adr/ docs/decisions/
history/
activity-definitions/ # customer-owned copies
loops/<loop-id>/ # roster cadence health
```
No code — layout contract only.
## Playbook skeleton
```task
id: KAIZEN-WP-0008-T02
status: todo
priority: high
state_hub_task_id: "90bd0fc2-6e49-4a59-9a78-91e749cef8a6"
```
Add `docs/integrations/customer-engagement-playbook.md`:
1. Register repo (state-hub `register_project.sh`)
2. Write INTENT + 4 loop workplans
3. Run `fix-consistency`
4. Pilot `schedule init` on target repos
5. Sync ActivityDefinitions to activity-core
6. Bootstrap hourly → regulator promotes cadence
Link to coulomb-loop as reference implementation.
## Support pilot schedule init
```task
id: KAIZEN-WP-0008-T05
status: done
priority: high
state_hub_task_id: "a48598b7-2a33-46ef-8594-6a2702459f39"
```
Completed 2026-06-18 on kaizen-agentic, the-custodian, activity-core. Bootstrap
hourly crons patched manually; friction logged in coulomb-loop `supplier-notes.md`.
Execute on pilot repos (after DEC-001 approval):
```bash
for repo in kaizen-agentic the-custodian activity-core; do
cd ~/$repo
kaizen-agentic schedule init --timezone Europe/Berlin
kaizen-agentic memory init coach
kaizen-agentic memory init optimization
kaizen-agentic schedule validate
done
```
Record friction in coulomb-loop `loops/kaizen-stack/supplier-notes.md`.
---
## Part 2 — Supplier automation
## metrics record --emit-event
```task
id: KAIZEN-WP-0008-T03
status: done
priority: medium
state_hub_task_id: "26ee0f8d-2b69-4796-b276-b76238d67546"
```
Emit NATS event `kaizen.metrics.recorded` when flag set:
```bash
kaizen-agentic metrics record coach --success --time 120 --quality 0.9 --emit-event
```
Payload per coulomb-loop LOOP-WP-0002 T03 / `low-success-rate-review` definition.
Default: off (backward compatible).
## schedule init --engagement
```task
id: KAIZEN-WP-0008-T04
status: done
priority: medium
state_hub_task_id: "62324bd2-1737-4864-889c-56179d0d11e8"
```
Scaffold customer-target schedule with bootstrap crons:
```bash
kaizen-agentic schedule init --engagement coulomb-loop \
--agents coach,optimization --bootstrap-cadence hourly
```
Writes hourly crons per ADR-003; documents engagement slug in schedule comment.
## ADR-006 customer engagement convention
```task
id: KAIZEN-WP-0008-T06
status: todo
priority: medium
state_hub_task_id: "5c06cdd9-655d-4837-b725-1f89b83db6d4"
```
`docs/adr/ADR-006-customer-engagement-convention.md` — formalize supplier/customer
split, `.kaizen/` placement in target repos, playbook lifecycle.
## Tests
```task
id: KAIZEN-WP-0008-T09
status: todo
priority: medium
state_hub_task_id: "f45077ea-5d24-4a85-bac2-ab9a3f61c20b"
```
Unit tests: `--emit-event` payload shape; `--engagement` schedule output.
---
## Part 3 — Playbook v1 and hybrid sync design
## Absorb supplier-notes into playbook v1
```task
id: KAIZEN-WP-0008-T07
status: todo
priority: low
state_hub_task_id: "0ef49fb5-af2f-4adf-aa90-1ea2cf389d00"
```
After LOOP-WP-0004 T07 draft in coulomb-loop, merge into playbook v1.
Target: second customer can copy template repo and run checklist in one session.
## ActivityDefinition override manifest
```task
id: KAIZEN-WP-0008-T08
status: todo
priority: low
state_hub_task_id: "c9bee570-89b5-43e5-aabc-23c7dcc4e30c"
```
Design-only (implements DEC-003 option C): YAML manifest mapping supplier
definition id → customer cron/labels/enabled overrides. No runtime in v1.
## Documentation release
```task
id: KAIZEN-WP-0008-T10
status: todo
priority: low
state_hub_task_id: "052a592b-ae7c-4213-9e09-eb8b37119d5e"
```
Update `wiki/EcosystemIntegration.md`, `CHANGELOG [Unreleased]`, cross-link
coulomb-loop INTENT from `docs/integrations/customer-engagement-playbook.md`.
---
## ADR-004 follow-on (customer accepted 2026-06-18)
After bootstrap metrics baseline, supplier may add:
```
kaizen-agentic metrics rotation-signals [--target PATH]
```
Reads `.kaizen/metrics/` + optimizer output; emits saturation score per
`coulomb-loop/loops/regulator/rotation-policy.yml`. Feeds LOOP-WP-0004 T09.
Track as KAIZEN-WP-0008 extension task if needed after T03 ships.
## Out of scope
- activity-core resolver implementation (activity-core repo)
- coulomb-loop workplan execution (customer repo)
- Fleet-wide rollout beyond agreed pilot (DEC-001)
## Success criteria
1. Pilot repos have valid `.kaizen/schedule.yml` via supplier CLI
2. `metrics record --emit-event` enables LOOP-WP-0002 event path
3. Playbook v1 committed; coulomb-loop cited as reference
4. ADR-006 accepted
## Customer workplans (do not duplicate here)
| Customer WP | Supplier support |
|-------------|------------------|
| LOOP-WP-0001 | T05 schedule init; activity-core handoff docs |
| LOOP-WP-0002 | T03 emit-event |
| LOOP-WP-0003 | scope-analyst agent (existing) |
| LOOP-WP-0004 | T07 playbook feedback |