diff --git a/CHANGELOG.md b/CHANGELOG.md index 2757c16..31e4c17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`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 diff --git a/docs/CLI_CHEAT_SHEET.md b/docs/CLI_CHEAT_SHEET.md index 82bc13d..a9c6fd9 100644 --- a/docs/CLI_CHEAT_SHEET.md +++ b/docs/CLI_CHEAT_SHEET.md @@ -112,6 +112,8 @@ Session-close template: `docs/templates/session-close-protocol.md` # Opt this repo into fleet scheduling kaizen-agentic schedule init # coach + optimization weekly 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 list # enabled entries (--all incl. disabled) diff --git a/docs/adr/ADR-005-scheduled-agent-execution.md b/docs/adr/ADR-005-scheduled-agent-execution.md index ec90c0d..f9cdf91 100644 --- a/docs/adr/ADR-005-scheduled-agent-execution.md +++ b/docs/adr/ADR-005-scheduled-agent-execution.md @@ -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 --engagement [--agents A,B] [--bootstrap-cadence hourly|daily|weekly] kaizen-agentic schedule validate [--target PATH] kaizen-agentic schedule list [--target PATH] [--all] kaizen-agentic schedule prepare [--target PATH] [--format markdown|json] diff --git a/docs/integrations/activity-core-handoff-engagement.md b/docs/integrations/activity-core-handoff-engagement.md new file mode 100644 index 0000000..45b67cc --- /dev/null +++ b/docs/integrations/activity-core-handoff-engagement.md @@ -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 --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) diff --git a/docs/integrations/customer-engagement-playbook.md b/docs/integrations/customer-engagement-playbook.md new file mode 100644 index 0000000..9e2fddc --- /dev/null +++ b/docs/integrations/customer-engagement-playbook.md @@ -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 \ + --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 --success --time --quality <0-1> +kaizen-agentic metrics record --success --time --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 \ + --bootstrap-cadence daily --force + +# Operate phase +kaizen-agentic schedule init --engagement \ + --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-*` diff --git a/docs/integrations/schedule-schema.md b/docs/integrations/schedule-schema.md index c9eb29e..e534ae5 100644 --- a/docs/integrations/schedule-schema.md +++ b/docs/integrations/schedule-schema.md @@ -70,11 +70,18 @@ the distribution) may appear in a schedule. kaizen-agentic schedule init # defaults: coach + optimization weekly kaizen-agentic schedule init --timezone UTC # override timezone 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 `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 ```bash diff --git a/src/kaizen_agentic/cli.py b/src/kaizen_agentic/cli.py index 3eb2a71..3ed74a2 100644 --- a/src/kaizen_agentic/cli.py +++ b/src/kaizen_agentic/cli.py @@ -26,6 +26,7 @@ from .optimization import OptimizationLoop, MIN_SAMPLES_FOR_RECOMMENDATIONS from .schedule import ( ScheduleError, default_schedule_yaml, + engagement_schedule_yaml, load_schedule, schedule_path, validate_schedule, @@ -1455,8 +1456,39 @@ def schedule_validate(target: str): "--timezone", default="Europe/Berlin", show_default=True, help="Schedule timezone" ) @click.option("--force", is_flag=True, help="Overwrite an existing schedule.yml") -def schedule_init(target: str, timezone: str, force: bool): - """Scaffold a default .kaizen/schedule.yml (coach + optimization weekly).""" +@click.option( + "--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)) if path.exists() and not force: @@ -1464,9 +1496,41 @@ def schedule_init(target: str, timezone: str, force: bool): click.echo(" Use --force to overwrite.") 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.write_text(default_schedule_yaml(timezone=timezone), encoding="utf-8") + path.write_text(yaml_text, encoding="utf-8") 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") diff --git a/src/kaizen_agentic/schedule.py b/src/kaizen_agentic/schedule.py index 3fbcac4..ca003c2 100644 --- a/src/kaizen_agentic/schedule.py +++ b/src/kaizen_agentic/schedule.py @@ -27,6 +27,29 @@ DEFAULT_AGENTS: Dict[str, Dict[str, Any]] = { "tdd-workflow": {"cadence": "monthly", "enabled": False}, } 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): @@ -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) 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 diff --git a/tests/test_schedule_cli.py b/tests/test_schedule_cli.py index 44f58e9..e6d0eaf 100644 --- a/tests/test_schedule_cli.py +++ b/tests/test_schedule_cli.py @@ -6,11 +6,13 @@ import json from pathlib import Path import pytest +import yaml from click.testing import CliRunner from kaizen_agentic.cli import cli from kaizen_agentic.schedule import ( ScheduleError, + engagement_schedule_yaml, parse_schedule, schedule_path, validate_schedule, @@ -30,6 +32,20 @@ def project_dir(tmp_path: Path) -> Path: 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): with pytest.raises(ScheduleError): parse_schedule({"agents": {}}) @@ -67,6 +83,67 @@ class TestScheduleCli: assert path.exists() 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( self, runner: CliRunner, project_dir: Path ): diff --git a/workplans/kaizen-agentic-WP-0008-coulomb-loop-supplier-engagement.md b/workplans/kaizen-agentic-WP-0008-coulomb-loop-supplier-engagement.md index 3942244..40e0aab 100644 --- a/workplans/kaizen-agentic-WP-0008-coulomb-loop-supplier-engagement.md +++ b/workplans/kaizen-agentic-WP-0008-coulomb-loop-supplier-engagement.md @@ -18,13 +18,13 @@ tasks: status: todo title: Document customer engagement repo layout from coulomb-loop reference - id: T02 - status: todo + 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: todo + status: done title: Add schedule init --engagement mode for customer repos - id: T05 status: done @@ -188,7 +188,7 @@ Default: off (backward compatible). ```task id: KAIZEN-WP-0008-T04 -status: todo +status: done priority: medium state_hub_task_id: "62324bd2-1737-4864-889c-56179d0d11e8" ```