diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b34339..deeafc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Scheduled agent execution (WP-0006, ADR-005)** — run agents on a cadence + against a preselected repo roster, orchestrated by activity-core and prepared + by kaizen-agentic (no Temporal workers or LLM runtime in this repo) +- **`kaizen-agentic schedule`** CLI group — `init`, `validate`, `list`, + `prepare ` (markdown/json) over `.kaizen/schedule.yml` +- **`.kaizen/schedule.yml`** manifest + schema docs and example + (`docs/integrations/schedule-schema.md`, `docs/examples/.kaizen/schedule.yml`) +- **ActivityDefinition drafts** (`enabled: false`) — `weekly-coach-orientation`, + `weekly-optimization-review` +- **Design specs** — `discover_kaizen_scheduled_repos` resolver, State Hub + roster fields, `kaizen.schedule.prepared` event payload, activity-core handoff + checklist + ## [1.2.0] - 2026-06-16 ### Added diff --git a/TODO.md b/TODO.md index b3948f2..b0adf25 100644 --- a/TODO.md +++ b/TODO.md @@ -12,10 +12,16 @@ The structure organizes **future tasks** by their impact, just as a changelog or Tasks in workplan: `workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md` (v1.3.0) -### To Add +### Implemented (pending v1.3.0 tag) * **ADR-005 + `.kaizen/schedule.yml`** — scheduled agent execution contract * **`kaizen-agentic schedule`** — validate, init, prepare, list * **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) +* **Tag v1.3.0** — once activity-core handoff issue is opened and pilot smoke-tested +* **activity-core implementation** — resolver + sync (separate repo; see handoff doc) ### Deferred to WP-0007 (v1.3.0+) * Interactive agent selection wizard diff --git a/docs/CLI_CHEAT_SHEET.md b/docs/CLI_CHEAT_SHEET.md index 54a8498..3c06bce 100644 --- a/docs/CLI_CHEAT_SHEET.md +++ b/docs/CLI_CHEAT_SHEET.md @@ -88,6 +88,22 @@ kaizen-agentic memory init tdd-workflow --no-metrics # memory only Session-close template: `docs/templates/session-close-protocol.md` +### Scheduled Agent Execution (ADR-005) +```bash +# Opt this repo into fleet scheduling +kaizen-agentic schedule init # coach + optimization weekly +kaizen-agentic schedule init --timezone UTC # override timezone +kaizen-agentic schedule validate # schema + agent-name checks +kaizen-agentic schedule list # enabled entries (--all incl. disabled) + +# Prepare orientation for a scheduled run (offline; no State Hub needed) +kaizen-agentic schedule prepare coach # markdown bundle +kaizen-agentic schedule prepare optimization --format json +``` + +activity-core fires the schedule and creates a task per (repo, agent); the task +runs `schedule prepare`. kaizen-agentic does not run cron or invoke Claude. + ### Information ```bash # List templates diff --git a/docs/INTEGRATION_PATTERNS.md b/docs/INTEGRATION_PATTERNS.md index 439949c..5141d4a 100644 --- a/docs/INTEGRATION_PATTERNS.md +++ b/docs/INTEGRATION_PATTERNS.md @@ -73,6 +73,45 @@ test -f .kaizen/metrics/optimizer/analysis.json && echo OK **Boundary:** kaizen-agentic does not run Temporal schedules. +### Scheduled agent execution (WP-0006, ADR-005) + +Beyond the metrics-only definitions above, agents themselves run on a cadence +against a **preselected repo roster**. The roster combines three sources: + +| Source | Purpose | +|--------|---------| +| State Hub `GET /repos/` | Canonical slug list + `host_paths` | +| Repo opt-in: `.kaizen/schedule.yml` exists with `version` set | Per-repo enablement | +| Optional hub flag `kaizen_schedule_enabled: true` (future) | Operator override | + +A repo is **schedule-eligible** when it is registered with reachable +`host_paths` **and** carries a valid `.kaizen/schedule.yml`. The activity-core +resolver `discover_kaizen_scheduled_repos` intersects these and emits +`context.scheduled_runs` (one entry per `(repo, agent)`); definitions `for_each` +over that output. + +| Definition | Trigger | Agent | Prepare command | +|------------|---------|-------|-----------------| +| [weekly-coach-orientation](integrations/activity-definitions/weekly-coach-orientation.md) | Cron Mon 09:00 | `coach` | `schedule prepare coach` | +| [weekly-optimization-review](integrations/activity-definitions/weekly-optimization-review.md) | Cron Mon 10:00 | `optimization` | `schedule prepare optimization` | + +**Listing schedule-eligible repos** (operator, no activity-core code): + +```bash +# In each candidate repo on a host listed in state-hub host_paths: +kaizen-agentic schedule validate && kaizen-agentic schedule list +``` + +**Design docs (no state-hub / activity-core code in this repo):** + +- [schedule-schema.md](integrations/schedule-schema.md) — `.kaizen/schedule.yml` +- [state-hub-roster-fields.md](integrations/state-hub-roster-fields.md) — hub fields/filters +- [discover-kaizen-scheduled-repos.md](integrations/discover-kaizen-scheduled-repos.md) — resolver spec +- [kaizen-schedule-prepared-event.md](integrations/kaizen-schedule-prepared-event.md) — event payload + +**Boundary:** kaizen-agentic declares and prepares; activity-core schedules; +state-hub owns the roster. + --- ## Pattern 3 — artifact-store evidence retention diff --git a/docs/PACKAGE_RELEASE.md b/docs/PACKAGE_RELEASE.md index 3667250..8d43a67 100644 --- a/docs/PACKAGE_RELEASE.md +++ b/docs/PACKAGE_RELEASE.md @@ -128,3 +128,27 @@ token with `TWINE_USERNAME=__token__`): make release-publish python -m twine upload dist/* ``` + +## Scheduled-run runner prerequisites (WP-0006) + +A runner that executes a scheduled kaizen agent task (fired by activity-core) +needs: + +- **`kaizen-agentic` on PATH** — `pip install kaizen-agentic` (or `pipx install + kaizen-agentic`) using the Gitea PyPI extra index when installing from the + internal registry: + ```bash + pip install kaizen-agentic \ + --extra-index-url https://gitea.coulomb.social/api/packages/coulomb/pypi/simple/ + ``` +- **Repo checkout reachable** at the `host_paths[]` registered in State + Hub, with a valid `.kaizen/schedule.yml` (`kaizen-agentic schedule validate`). +- **No State Hub required for `prepare`** — `schedule prepare` reads local + `.kaizen/` state only. The hub is needed by the *resolver* (activity-core), + not by the prepared session. + +**Enabling a definition** (activity-core operator): keep the kaizen definitions +at `enabled: false` until a manual smoke test passes (see +[INTEGRATION_PATTERNS.md Pattern 2](INTEGRATION_PATTERNS.md) and the +[activity-core handoff checklist](integrations/activity-core-handoff-wp0006.md)), +then flip one definition to `enabled: true` in staging before fleet-wide enable. diff --git a/docs/adr/ADR-005-scheduled-agent-execution.md b/docs/adr/ADR-005-scheduled-agent-execution.md new file mode 100644 index 0000000..ec90c0d --- /dev/null +++ b/docs/adr/ADR-005-scheduled-agent-execution.md @@ -0,0 +1,149 @@ +--- +id: ADR-005 +title: Scheduled Agent Execution Convention +status: accepted +date: "2026-06-17" +--- + +# ADR-005 — Scheduled Agent Execution Convention + +## Status + +Accepted + +## Context + +Kaizen agents are markdown instruction sets invoked in coding-agent sessions. +ADR-004 added project-scoped metrics; WP-0004 committed three metrics-focused +`ActivityDefinition` drafts (`enabled: false`). What is still missing is a way to +run **agents themselves** — not just the metrics optimizer — on a **regular +cadence** against a **preselected set of repos**, without kaizen-agentic owning +Temporal workers, cron, or an LLM runtime. + +The ecosystem already separates concerns: + +- **activity-core** owns scheduling (cron/event → task creation). +- **state-hub** owns the canonical repo roster and `host_paths`. +- **kaizen-agentic** owns the agents, project memory, and metrics. + +A scheduled agent run therefore needs a contract that crosses these repos +without merging them. + +## Decision + +Introduce a **repo-local schedule manifest** and a **prepare** step. The +end-to-end flow: + +``` +activity-core cron + → context resolver (roster ∩ repos with schedule.yml) + → task per (repo, agent) + → coding-agent session runs `kaizen-agentic schedule prepare ` + → session executes the agent instructions in that repo +``` + +kaizen-agentic's responsibilities are exactly two: **declare** the schedule +(`.kaizen/schedule.yml`) and **prepare** an orientation bundle for a run. It +does **not** fire cron, create tasks, or invoke Claude. + +### 1. Schedule manifest — `.kaizen/schedule.yml` + +A repo opts into fleet scheduling by committing this file: + +```yaml +version: "1" +timezone: Europe/Berlin +agents: + coach: + cadence: weekly + cron: "0 9 * * 1" # optional override; default from ActivityDefinition + enabled: true + optimization: + cadence: weekly + cron: "0 10 * * 1" + enabled: true + tdd-workflow: + cadence: monthly + enabled: false +``` + +**Schema:** + +| Key | Required | Type | Notes | +|-----|----------|------|-------| +| `version` | yes | string | Must be `"1"` | +| `timezone` | no | string | IANA tz; default supplied by ActivityDefinition | +| `agents` | yes | mapping | `agent-name → settings` | +| `agents..cadence` | yes | enum | `daily` \| `weekly` \| `monthly` | +| `agents..cron` | no | string | 5-field cron; overrides cadence default | +| `agents..enabled` | no | bool | Default `true` | + +**Validation rules** (`kaizen-agentic schedule validate`): + +- `version` must equal `"1"`. +- Every agent key must be an installed or packaged agent name. +- `cadence` must be one of the allowed values. +- Duplicate agent entries are rejected. + +### 2. Roster (preselected repos) + +A repo is **schedule-eligible** when **all** of: + +1. It is a registered repo in state-hub (`GET /repos/`) with reachable + `host_paths`. +2. It contains a valid `.kaizen/schedule.yml`. +3. (Optional, future) it carries a `kaizen_schedule_enabled: true` hub flag. + +The resolver `discover_kaizen_scheduled_repos` (specified in +`docs/integrations/discover-kaizen-scheduled-repos.md`, implemented in +activity-core) intersects these sources and emits `context.scheduled_runs`. + +### 3. Prepare bundle — `schedule prepare ` + +Assembles, from **local `.kaizen/` state only** (offline-safe): + +- The agent prompt (`agents/agent-.md`, installed or packaged). +- Project memory (`.kaizen/agents//memory.md`) when present. +- Metrics summary (`.kaizen/metrics//summary.json`) when present. +- Repo pointers (`SCOPE.md`, `TODO.md`) when present. +- Suggested session-close commands (`metrics record`, memory update). + +Output is `markdown` (default) or `json` (`--format json`) so activity-core can +embed it in a task `description` or a runner can parse it. + +### CLI interface + +``` +kaizen-agentic schedule init [--target PATH] [--timezone TZ] [--force] +kaizen-agentic schedule validate [--target PATH] +kaizen-agentic schedule list [--target PATH] [--all] +kaizen-agentic schedule prepare [--target PATH] [--format markdown|json] +``` + +## Boundaries + +- **No scheduling code** in kaizen-agentic. Cron and task creation belong to + activity-core; the roster query belongs to state-hub. +- **No LLM invocation.** `prepare` produces a runner-agnostic bundle; a human or + automated coding-agent session executes it. +- **State-hub schema changes** (roster opt-in flag) are designed here but + implemented in `the-custodian` (repo boundary). + +## Consequences + +- Operators declare per-repo schedules and a fleet roster without tribal + knowledge. +- activity-core can fire recurring tasks referencing `schedule prepare`. +- A scheduled session opens with full orientation (prompt + memory + metrics). +- The existing `weekly-metrics-optimize` definition (ADR-004 / WP-0004) remains + complementary; an `optimization` agent run may chain `schedule prepare + optimization` then `metrics optimize`. + +## Related Documents + +- [ADR-002: Project Memory Convention](ADR-002-project-memory-convention.md) +- [ADR-004: Project Metrics Convention](ADR-004-project-metrics-convention.md) +- [docs/integrations/schedule-schema.md](../integrations/schedule-schema.md) +- [docs/integrations/discover-kaizen-scheduled-repos.md](../integrations/discover-kaizen-scheduled-repos.md) +- [docs/agency-framework.md](../agency-framework.md) +- [KAIZEN-WP-0006](../../workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md) diff --git a/docs/agency-framework.md b/docs/agency-framework.md index f9f28d2..e38efd1 100644 --- a/docs/agency-framework.md +++ b/docs/agency-framework.md @@ -299,12 +299,35 @@ Manifest: [integrations/optimizer-artifact-manifest.md](integrations/optimizer-a --- +## Scheduled Agent Execution + +Agents can run on a **regular cadence** against **preselected repos**, fired by +**activity-core** and prepared by kaizen-agentic (ADR-005). A repo opts in by +committing `.kaizen/schedule.yml`: + +```bash +kaizen-agentic schedule init # scaffold (coach + optimization weekly) +kaizen-agentic schedule validate # check schema + agent names +kaizen-agentic schedule list # show enabled entries +kaizen-agentic schedule prepare coach # orientation bundle for a scheduled run +``` + +`schedule prepare ` bundles the agent prompt, project memory, metrics +summary, and repo pointers — offline, no State Hub required. kaizen-agentic does +**not** run cron or invoke Claude; activity-core fires the schedule and a +coding-agent session executes the prepared bundle. + +Schema: [integrations/schedule-schema.md](integrations/schedule-schema.md). + +--- + ## Related Documents - [ADR-001: Workplan Convention](adr/ADR-001-workplan-convention.md) - [ADR-002: Project Memory Convention](adr/ADR-002-project-memory-convention.md) - [ADR-003: Protocols Artifact Convention](adr/ADR-003-protocols-artifact-convention.md) - [ADR-004: Project Metrics Convention](adr/ADR-004-project-metrics-convention.md) +- [ADR-005: Scheduled Agent Execution](adr/ADR-005-scheduled-agent-execution.md) - [wiki/EcosystemIntegration.md](../wiki/EcosystemIntegration.md) — two-layer measurement model - [WP-0002: Agency Framework](../workplans/kaizen-agentic-WP-0002-agency-framework.md) - [WP-0003: Measurement Loop](../workplans/kaizen-agentic-WP-0003-measurement-loop.md) diff --git a/docs/examples/.kaizen/schedule.yml b/docs/examples/.kaizen/schedule.yml new file mode 100644 index 0000000..26ab8bf --- /dev/null +++ b/docs/examples/.kaizen/schedule.yml @@ -0,0 +1,17 @@ +# Kaizen scheduled agent execution manifest (ADR-005) +# Declares which agents run on what cadence in this repo. +# Validate with: kaizen-agentic schedule validate +version: "1" +timezone: Europe/Berlin +agents: + coach: + cadence: weekly + cron: "0 9 * * 1" # Monday 09:00 — cross-agent orientation brief + enabled: true + optimization: + cadence: weekly + cron: "0 10 * * 1" # Monday 10:00 — agent performance review + enabled: true + tdd-workflow: + cadence: monthly + enabled: false # declared but paused; operator opts in deliberately diff --git a/docs/integrations/activity-core-handoff-wp0006.md b/docs/integrations/activity-core-handoff-wp0006.md new file mode 100644 index 0000000..a1bdea8 --- /dev/null +++ b/docs/integrations/activity-core-handoff-wp0006.md @@ -0,0 +1,69 @@ +# activity-core Handoff — Scheduled Agent Execution (WP-0006) + +Coordination checklist for the **activity-core** team to enable kaizen scheduled +agent runs. kaizen-agentic owns the schedule contract, the prepare CLI, and the +ActivityDefinition **drafts**; activity-core owns the resolver, the schedule +firing, and task creation (repo boundary, ADR-005). + +Open this as an activity-core issue/PR titled *"Enable kaizen scheduled agent +execution (WP-0006)"* and track the boxes there. + +## What kaizen-agentic ships (done in this repo) + +- [x] `.kaizen/schedule.yml` schema + `schedule validate|init|list|prepare` CLI +- [x] ADR-005 contract +- [x] Resolver spec: [discover-kaizen-scheduled-repos.md](discover-kaizen-scheduled-repos.md) +- [x] State Hub roster fields design: [state-hub-roster-fields.md](state-hub-roster-fields.md) +- [x] Draft definitions (`enabled: false`): + [weekly-coach-orientation](activity-definitions/weekly-coach-orientation.md), + [weekly-optimization-review](activity-definitions/weekly-optimization-review.md) +- [x] Event payload spec: [kaizen-schedule-prepared-event.md](kaizen-schedule-prepared-event.md) + +## What activity-core must do + +- [ ] **Implement resolver** `discover_kaizen_scheduled_repos` per the spec + (hub roster ∩ repos with valid `.kaizen/schedule.yml`). +- [ ] **Add resolver unit tests** using the four fixtures in the spec. +- [ ] **Copy definitions** from `docs/integrations/activity-definitions/` + (`weekly-coach-orientation.md`, `weekly-optimization-review.md`) into the + activity-core catalog path (per ACT-ADR-002). +- [ ] **Register** each definition slug in the activity-core index. +- [ ] **Run** `make sync-activity-definitions` in activity-core. +- [ ] **Wire cron** triggers (Mon 09:00 / 10:00 Europe/Berlin) to the resolver. +- [ ] **Smoke test** against a pilot repo (see below) with the definition still + `enabled: false` (dry-run task creation). +- [ ] **Enable gradually** — flip one definition to `enabled: true` in staging + after the smoke test passes. +- [ ] **Verify runner prerequisites** — `kaizen-agentic` on PATH and the Gitea + PyPI extra index if the runner installs from registry (see + [PACKAGE_RELEASE.md](../PACKAGE_RELEASE.md)). + +## State Hub team (the-custodian) + +- [ ] Optional: add `kaizen_schedule_enabled` repo flag + `GET /repos/` filter + (v2 pre-filter; the repo file remains the source of truth). + +## Smoke test (manual, runner-agnostic) + +```bash +cd /path/to/pilot-repo +kaizen-agentic schedule init # if not already present +kaizen-agentic schedule validate # exit 0 +kaizen-agentic schedule list # shows coach + optimization enabled +kaizen-agentic schedule prepare coach # non-empty orientation bundle +``` + +Then in activity-core: run the resolver (dry-run) and confirm one +`scheduled_run` per enabled `(repo, agent)` with a correct `prepare_command`. + +## Pilot roster + +- `kaizen-agentic` (dogfood) +- `the-custodian` (hub operator) +- one additional custodian-domain repo with `.kaizen/` state (TBD at pilot time) + +## Related + +- [ADR-005](../adr/ADR-005-scheduled-agent-execution.md) +- [INTEGRATION_PATTERNS.md Pattern 2](../INTEGRATION_PATTERNS.md) +- [KAIZEN-WP-0006](../../workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md) diff --git a/docs/integrations/activity-definitions/weekly-coach-orientation.md b/docs/integrations/activity-definitions/weekly-coach-orientation.md new file mode 100644 index 0000000..fa7a62d --- /dev/null +++ b/docs/integrations/activity-definitions/weekly-coach-orientation.md @@ -0,0 +1,55 @@ +--- +id: kaizen-weekly-coach-orientation +name: Weekly Kaizen Coach Orientation +enabled: false +owner: kaizen-agentic +governance: custodian +status: proposed +trigger: + type: cron + cron_expression: "0 9 * * 1" + timezone: Europe/Berlin + misfire_policy: skip +context_sources: + - type: resolver + query: discover_kaizen_scheduled_repos + params: + domain: custodian + cadence: weekly + bind_to: context.scheduled_runs +--- + +# Weekly Kaizen Coach Orientation + +Every Monday 09:00 Berlin time, opens a coach orientation task for each +schedule-eligible repo whose `.kaizen/schedule.yml` enables the `coach` agent. + +The resolver `discover_kaizen_scheduled_repos` (see +[discover-kaizen-scheduled-repos.md](../discover-kaizen-scheduled-repos.md)) +returns one `scheduled_run` per `(repo, agent)`; this definition selects the +`coach` runs. + +```rule +id: run-weekly-coach +for_each: context.scheduled_runs +bind_as: r +condition: 'r.agent == "coach" and r.enabled == true' +action: + task_template: "Weekly coach orientation: {{r.repo}}" + description: | + {{r.prepare_command}} + Then load agents/agent-coach.md in a coding-agent session, paste the + prepared bundle, and follow the coach synthesis. At session close: + kaizen-agentic metrics record coach --success --time --quality <0-1> + target_repo: "{{r.repo}}" + priority: medium + labels: ["kaizen", "agent-run", "coach", "scheduled", "automated"] +``` + +**CLI mapping:** `kaizen-agentic schedule prepare coach` (offline-safe; reads +local `.kaizen/` state). + +**Activation:** sync into activity-core via `make sync-activity-definitions` +after the `discover_kaizen_scheduled_repos` resolver is enabled. Keep +`enabled: false` until a manual smoke test passes on a pilot repo. See +[INTEGRATION_PATTERNS.md Pattern 2](../../INTEGRATION_PATTERNS.md). diff --git a/docs/integrations/activity-definitions/weekly-optimization-review.md b/docs/integrations/activity-definitions/weekly-optimization-review.md new file mode 100644 index 0000000..d9e2579 --- /dev/null +++ b/docs/integrations/activity-definitions/weekly-optimization-review.md @@ -0,0 +1,55 @@ +--- +id: kaizen-weekly-optimization-review +name: Weekly Kaizen Optimization Review +enabled: false +owner: kaizen-agentic +governance: custodian +status: proposed +trigger: + type: cron + cron_expression: "0 10 * * 1" + timezone: Europe/Berlin + misfire_policy: skip +context_sources: + - type: resolver + query: discover_kaizen_scheduled_repos + params: + domain: custodian + cadence: weekly + bind_to: context.scheduled_runs +--- + +# Weekly Kaizen Optimization Review + +Every Monday 10:00 Berlin time, opens an optimization-agent review task for each +schedule-eligible repo whose `.kaizen/schedule.yml` enables the `optimization` +agent. Chains the agent orientation with the existing metrics optimizer so the +review is evidence-backed. + +```rule +id: run-weekly-optimization +for_each: context.scheduled_runs +bind_as: r +condition: 'r.agent == "optimization" and r.enabled == true' +action: + task_template: "Weekly optimization review: {{r.repo}}" + description: | + {{r.prepare_command}} + kaizen-agentic metrics optimize # refresh evidence + Then load agents/agent-optimization.md, paste the prepared bundle plus the + optimizer report, and act on recommendations. At session close: + kaizen-agentic metrics record optimization --success --time --quality <0-1> + target_repo: "{{r.repo}}" + priority: medium + labels: ["kaizen", "agent-run", "optimization", "scheduled", "automated"] +``` + +**CLI mapping:** `kaizen-agentic schedule prepare optimization` then +`kaizen-agentic metrics optimize`. + +**Complementarity:** this generalizes the metrics-only +[weekly-metrics-optimize](weekly-metrics-optimize.md) definition into a full +agent run. Repos may run either; running both duplicates the optimizer step. + +**Activation:** sync into activity-core via `make sync-activity-definitions` +after the resolver is enabled; hold at `enabled: false` until smoke-tested. diff --git a/docs/integrations/discover-kaizen-scheduled-repos.md b/docs/integrations/discover-kaizen-scheduled-repos.md new file mode 100644 index 0000000..2dcb9fa --- /dev/null +++ b/docs/integrations/discover-kaizen-scheduled-repos.md @@ -0,0 +1,109 @@ +# Resolver Spec: `discover_kaizen_scheduled_repos` + +**Status:** specification — **implemented in activity-core**, not here. This doc +is the contract an activity-core implementer needs to add the context resolver +that feeds the scheduled-agent ActivityDefinitions (ADR-005). + +## Purpose + +Given the fleet roster and per-repo schedule manifests, emit one entry per +`(repo, agent)` that is due to run, so ActivityDefinitions can `for_each` over +the result. + +## Signature + +``` +discover_kaizen_scheduled_repos( + domain: str | None = None, # optional scope filter, e.g. "custodian" + cadence: str | None = None, # optional: "daily" | "weekly" | "monthly" + now: datetime | None = None, # injection point for testing +) -> { "scheduled_runs": list[ScheduledRun] } +``` + +Bound in a definition as: + +```yaml +context_sources: + - type: resolver + query: discover_kaizen_scheduled_repos + params: + domain: custodian + cadence: weekly + bind_to: context.scheduled_runs +``` + +## Inputs (sources, in order) + +1. **State Hub** `GET /repos/` (optionally filtered by `domain` and, when the v2 + flag lands, `kaizen_schedule_enabled=true`). Yields `slug`, `host_paths`, + `domain`. +2. **Repo checkout** at `host_paths[]`: read + `.kaizen/schedule.yml`. Skip repos without the file. +3. **Validation**: run the equivalent of `kaizen-agentic schedule validate`. + Skip (and log) repos whose schedule is invalid — never emit a bad entry. + +## Output shape + +```json +{ + "scheduled_runs": [ + { + "repo": "kaizen-agentic", + "root": "/home/worsch/kaizen-agentic", + "agent": "coach", + "cadence": "weekly", + "cron": "0 9 * * 1", + "timezone": "Europe/Berlin", + "enabled": true, + "prepare_command": "kaizen-agentic schedule prepare coach --target /home/worsch/kaizen-agentic" + } + ] +} +``` + +### `ScheduledRun` fields + +| Field | Source | Notes | +|-------|--------|-------| +| `repo` | hub `slug` | becomes `target_repo` on the created task | +| `root` | `host_paths[]` | absolute checkout path on the runner | +| `agent` | schedule.yml key | | +| `cadence` | schedule.yml | | +| `cron` | schedule.yml or definition default | per-repo override when present | +| `timezone` | schedule.yml or definition default | | +| `enabled` | schedule.yml (`true` only emitted) | disabled entries are filtered out | +| `prepare_command` | derived | exact CLI the task should run | + +## Filtering rules + +- Emit only entries with `enabled: true`. +- When `cadence` param is set, emit only matching entries (lets each cron-bound + definition select its own cadence slice). +- When `cron` is present on the entry, it is the authoritative per-repo time; + otherwise the definition's cron applies. + +## Errors + +| Condition | Behavior | +|-----------|----------| +| Repo unreachable / path missing on host | Skip + log `repo_unreachable` | +| `.kaizen/schedule.yml` absent | Skip silently (not opted in) | +| schedule.yml invalid | Skip + log `schedule_invalid` with validation errors | +| Hub unreachable | Fail the resolver run (no roster = no safe output) | + +The resolver must be **idempotent** and **side-effect free**: it reads, it does +not write. Task creation happens in the ActivityDefinition rule, not here. + +## Test fixtures + +- A repo with valid `.kaizen/schedule.yml` (coach enabled) → one entry. +- A repo with the file but coach `enabled: false` → no entry. +- A repo without the file → no entry. +- A repo with an invalid schedule → no entry + logged error. + +## Related + +- [state-hub-roster-fields.md](state-hub-roster-fields.md) +- [schedule-schema.md](schedule-schema.md) +- [activity-definitions/weekly-coach-orientation.md](activity-definitions/weekly-coach-orientation.md) +- [ADR-005](../adr/ADR-005-scheduled-agent-execution.md) diff --git a/docs/integrations/kaizen-schedule-prepared-event.md b/docs/integrations/kaizen-schedule-prepared-event.md new file mode 100644 index 0000000..c077f5b --- /dev/null +++ b/docs/integrations/kaizen-schedule-prepared-event.md @@ -0,0 +1,90 @@ +# Event Payload: `kaizen.schedule.prepared` + +**Status:** design — for **future event-driven runs**. v1 of WP-0006 is +cron-driven (activity-core fires the schedule). This event lets a runner or +activity-core react when a `schedule prepare` bundle has been assembled, without +polling. + +kaizen-agentic does **not** publish this event today; the `prepare` command +writes to stdout. This spec fixes the contract so an emitter (a runner wrapper +or a thin `--emit` flag in a later iteration) and consumers agree on the shape. + +## Subject + +``` +kaizen.schedule.prepared +``` + +NATS/event-bus subject, sibling to the existing `kaizen.metrics.recorded` and +`kaizen.agent.installed` subjects (Pattern 2). + +## Payload + +```json +{ + "event": "kaizen.schedule.prepared", + "version": "1", + "occurred_at": "2026-06-17T09:00:12Z", + "repo": "kaizen-agentic", + "root": "/home/worsch/kaizen-agentic", + "agent": "coach", + "cadence": "weekly", + "prepare_command": "kaizen-agentic schedule prepare coach --target /home/worsch/kaizen-agentic", + "bundle": { + "format": "markdown", + "agent_prompt_found": true, + "has_memory": true, + "has_metrics": true, + "pointers": ["scope", "todo"], + "bytes": 8421 + }, + "session_close": [ + "kaizen-agentic metrics record coach --success --time --quality <0-1>" + ] +} +``` + +### Fields + +| Field | Type | Notes | +|-------|------|-------| +| `event` | string | Always `kaizen.schedule.prepared` | +| `version` | string | Payload schema version | +| `occurred_at` | RFC3339 | When the bundle was assembled | +| `repo` | string | State Hub slug | +| `root` | string | Absolute checkout path | +| `agent` | string | Agent the bundle orients | +| `cadence` | string | `daily` \| `weekly` \| `monthly` | +| `prepare_command` | string | Exact CLI that produced the bundle | +| `bundle.format` | string | `markdown` \| `json` | +| `bundle.agent_prompt_found` | bool | Mirrors `schedule prepare --format json` | +| `bundle.has_memory` | bool | Memory file present | +| `bundle.has_metrics` | bool | Metrics summary present | +| `bundle.pointers` | string[] | Repo pointer labels found (`scope`, `todo`) | +| `bundle.bytes` | int | Rendered bundle size | +| `session_close` | string[] | Suggested close commands | + +The `bundle.*` flags map 1:1 to the JSON output of +`kaizen-agentic schedule prepare --format json`, so an emitter can build +this payload from the existing command without new computation. + +## Consumers (illustrative) + +- **activity-core** — close the loop: mark the scheduled task `in_progress` when + a bundle is prepared. +- **artifact-store** — archive large bundles by reference. +- **dashboards** — fleet view of which scheduled runs have been prepared vs. + executed. + +## Boundary + +- The payload carries **metadata and a command**, never secrets or full repo + contents. +- kaizen-agentic owns the schema; emission wiring (NATS publish) is a future + iteration and belongs to the runner / activity-core integration. + +## Related + +- [discover-kaizen-scheduled-repos.md](discover-kaizen-scheduled-repos.md) +- [INTEGRATION_PATTERNS.md Pattern 2](../INTEGRATION_PATTERNS.md) +- [ADR-005](../adr/ADR-005-scheduled-agent-execution.md) diff --git a/docs/integrations/schedule-schema.md b/docs/integrations/schedule-schema.md new file mode 100644 index 0000000..c9eb29e --- /dev/null +++ b/docs/integrations/schedule-schema.md @@ -0,0 +1,93 @@ +# `.kaizen/schedule.yml` Schema + +The schedule manifest declares which kaizen agents run on what cadence in an +opted-in repo. It is the repo-local half of the scheduled-agent-execution +contract (ADR-005). activity-core reads it (via the roster resolver) to fire +recurring tasks; `kaizen-agentic schedule prepare` reads it indirectly by +preparing per-agent orientation. + +Canonical example: [`docs/examples/.kaizen/schedule.yml`](../examples/.kaizen/schedule.yml). + +## Location + +``` +/.kaizen/schedule.yml +``` + +Lives alongside `.kaizen/agents/` (memory) and `.kaizen/metrics/`. Like those, +its presence is the **opt-in signal** for fleet scheduling. + +## Fields + +| Key | Required | Type | Default | Notes | +|-----|----------|------|---------|-------| +| `version` | yes | string | — | Must be `"1"` | +| `timezone` | no | string | from ActivityDefinition | IANA tz, e.g. `Europe/Berlin` | +| `agents` | yes | mapping | — | `agent-name → settings` | +| `agents..cadence` | yes | enum | — | `daily` \| `weekly` \| `monthly` | +| `agents..cron` | no | string | cadence default | 5-field cron expression | +| `agents..enabled` | no | bool | `true` | Set `false` to declare but pause | + +## Example + +```yaml +version: "1" +timezone: Europe/Berlin +agents: + coach: + cadence: weekly + cron: "0 9 * * 1" + enabled: true + optimization: + cadence: weekly + cron: "0 10 * * 1" + enabled: true + tdd-workflow: + cadence: monthly + enabled: false +``` + +## Validation + +```bash +kaizen-agentic schedule validate +``` + +Errors are emitted with actionable messages and a non-zero exit code: + +- Missing or non-`"1"` `version`. +- `agents` not a mapping, or no agents declared. +- An agent name that is **not** installed or packaged (typo guard). +- A `cadence` outside `daily` / `weekly` / `monthly`. +- Duplicate agent entries. + +Only agents available to the project (installed under `agents/` or packaged in +the distribution) may appear in a schedule. + +## Scaffolding + +```bash +kaizen-agentic schedule init # defaults: coach + optimization weekly +kaizen-agentic schedule init --timezone UTC # override timezone +kaizen-agentic schedule init --force # overwrite existing +``` + +The default scaffold enables `coach` and `optimization` weekly and declares +`tdd-workflow` monthly but **disabled** (operator opts in deliberately). + +## Listing + +```bash +kaizen-agentic schedule list # enabled entries only +kaizen-agentic schedule list --all # include disabled +``` + +## Relationship to activity-core + +The `cron` field is an **optional per-repo override**. When omitted, the cadence +maps to the default cron declared in the matching ActivityDefinition (e.g. +`weekly-coach-orientation` fires Mon 09:00). This keeps fleet-wide timing in one +place while letting individual repos shift their slot. + +See [ADR-005](../adr/ADR-005-scheduled-agent-execution.md) and +[INTEGRATION_PATTERNS.md Pattern 2](../INTEGRATION_PATTERNS.md). diff --git a/docs/integrations/state-hub-roster-fields.md b/docs/integrations/state-hub-roster-fields.md new file mode 100644 index 0000000..4a95ac2 --- /dev/null +++ b/docs/integrations/state-hub-roster-fields.md @@ -0,0 +1,64 @@ +# State Hub Roster Fields for Kaizen Scheduling (Design) + +**Status:** design only — implemented in `the-custodian/state-hub`, not here +(repo boundary). This document specifies what kaizen-agentic and activity-core +need from the hub so the State Hub team can add the fields and filter. + +## Problem + +activity-core's resolver needs to answer: *which registered repos participate in +kaizen fleet scheduling, and where do they live on disk?* Today state-hub knows +the canonical repo list and `host_paths` but has no notion of schedule opt-in. + +## Existing hub data (sufficient for v1) + +`GET /repos/` already returns, per repo: + +| Field | Use | +|-------|-----| +| `slug` | Canonical repo identifier (`target_repo` in tasks) | +| `host_paths[hostname] → local_path` | Where the repo is checked out on a runner | +| `domain` | Scope filter (e.g. `custodian`) | + +For **v1**, opt-in is detected **in the repo** (`.kaizen/schedule.yml` exists and +validates). The resolver clones/reads each candidate path and checks for the +file. No hub schema change is strictly required to ship the pilot. + +## Proposed hub fields (v2, optional) + +To let operators query eligibility **without touching every checkout**, add an +optional repo-metadata flag: + +| Field | Type | Default | Meaning | +|-------|------|---------|---------| +| `kaizen_schedule_enabled` | bool | `false` | Operator-confirmed fleet participation | +| `kaizen_schedule_updated_at` | timestamp | null | Last time schedule.yml was synced/seen | + +### Suggested filter + +``` +GET /repos/?kaizen_schedule_enabled=true&domain=custodian +``` + +Returns only schedule-eligible repos with their `host_paths`, so the resolver +skips repos that have not opted in — cheaper than scanning every checkout. + +### Write path + +The flag is set by an operator (or a future `kaizen-agentic schedule register` +that calls the hub). It is **advisory**: the authoritative opt-in remains the +presence of a valid `.kaizen/schedule.yml` in the repo, re-checked by the +resolver at run time. The flag is an index, not a source of truth. + +## Boundary + +- kaizen-agentic does **not** write these fields in WP-0006. +- state-hub schema migration is tracked in `the-custodian`. +- The resolver (activity-core) treats the hub flag as a pre-filter and the repo + file as the decision. + +## Related + +- [discover-kaizen-scheduled-repos.md](discover-kaizen-scheduled-repos.md) +- [schedule-schema.md](schedule-schema.md) +- [ADR-005](../adr/ADR-005-scheduled-agent-execution.md) diff --git a/src/kaizen_agentic/cli.py b/src/kaizen_agentic/cli.py index 586405a..603fa25 100644 --- a/src/kaizen_agentic/cli.py +++ b/src/kaizen_agentic/cli.py @@ -18,6 +18,13 @@ from .integrations.artifact_store import ( from .integrations.helix import HelixCorrelationAdapter, enrich_helix_correlation from .metrics import MetricsStore, OptimizerStore, performance_summary_markdown from .optimization import OptimizationLoop, MIN_SAMPLES_FOR_RECOMMENDATIONS +from .schedule import ( + ScheduleError, + default_schedule_yaml, + load_schedule, + schedule_path, + validate_schedule, +) def safe_cli_wrapper(): @@ -1360,6 +1367,204 @@ def protocols_show(agent_name: str, slug: str): click.echo(protocol_path.read_text()) +@cli.group() +def schedule(): + """Prepare and validate scheduled agent runs (.kaizen/schedule.yml, ADR-005). + + kaizen-agentic does not run cron schedules or invoke Claude. activity-core + fires the cron and creates a task per (repo, agent); a coding-agent session + runs `schedule prepare ` to assemble orientation, then executes the + agent instructions. + """ + pass + + +@schedule.command("validate") +@click.option("--target", "-t", default=".", help="Project root (default: current)") +def schedule_validate(target: str): + """Validate .kaizen/schedule.yml against the ADR-005 schema.""" + path = schedule_path(_project_root(target)) + + try: + parsed = load_schedule(path) + except ScheduleError as exc: + click.echo(f"❌ {exc}", err=True) + click.echo(" Run: kaizen-agentic schedule init", err=True) + sys.exit(1) + + known_agents = _get_registry().agent_names() + errors = validate_schedule(parsed, known_agents=known_agents) + + if errors: + click.echo(f"❌ Schedule validation failed ({path}):") + for error in errors: + click.echo(f" • {error}") + sys.exit(1) + + enabled = parsed.enabled_entries() + click.echo(f"✅ Schedule valid: {path}") + click.echo(f" {len(parsed.entries)} agent(s), {len(enabled)} enabled") + + +@schedule.command("init") +@click.option("--target", "-t", default=".", help="Project root (default: current)") +@click.option( + "--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).""" + path = schedule_path(_project_root(target)) + + if path.exists() and not force: + click.echo(f"Schedule already exists: {path}") + click.echo(" Use --force to overwrite.") + return + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(default_schedule_yaml(timezone=timezone), encoding="utf-8") + click.echo(f"Initialized schedule: {path}") + click.echo(" Validate with: kaizen-agentic schedule validate") + + +@schedule.command("list") +@click.option("--target", "-t", default=".", help="Project root (default: current)") +@click.option("--all", "show_all", is_flag=True, help="Include disabled entries") +def schedule_list(target: str, show_all: bool): + """Show enabled schedule entries from .kaizen/schedule.yml.""" + path = schedule_path(_project_root(target)) + + try: + parsed = load_schedule(path) + except ScheduleError as exc: + click.echo(f"No schedule found: {exc}") + click.echo(" Run: kaizen-agentic schedule init") + return + + entries = parsed.entries if show_all else parsed.enabled_entries() + if not entries: + click.echo("No enabled schedule entries (use --all to see disabled).") + return + + click.echo(f"Scheduled agents ({path}):") + if parsed.timezone: + click.echo(f" Timezone: {parsed.timezone}") + for entry in entries: + flag = "✅" if entry.enabled else "⏸ " + cron = f" cron={entry.cron}" if entry.cron else "" + click.echo(f" {flag} {entry.agent}: {entry.cadence}{cron}") + + +@schedule.command("prepare") +@click.argument("agent_name") +@click.option("--target", "-t", default=".", help="Project root (default: current)") +@click.option( + "--format", + "output_format", + type=click.Choice(["markdown", "json"]), + default="markdown", + show_default=True, + help="Output format for the orientation bundle", +) +def schedule_prepare(agent_name: str, target: str, output_format: str): + """Assemble an orientation bundle for a scheduled agent run. + + Bundles the agent prompt, project memory, metrics summary, and repo + pointers into a single payload. Works offline from local `.kaizen/` state; + no State Hub required. Pass the output to a coding-agent session. + """ + bundle = _build_prepare_bundle(agent_name, _project_root(target)) + + if output_format == "json": + click.echo(json.dumps(bundle, indent=2)) + return + + click.echo(_render_prepare_markdown(bundle)) + + +def _build_prepare_bundle(agent_name: str, project_root: Path) -> dict: + """Collect the orientation bundle pieces for `schedule prepare`.""" + registry = _get_registry() + agent_path = registry.get_agent_path(agent_name) + agent_prompt = agent_path.read_text(encoding="utf-8") if agent_path else None + + memory_path = project_root / ".kaizen" / "agents" / agent_name / "memory.md" + memory = memory_path.read_text(encoding="utf-8") if memory_path.exists() else None + + metrics_store = MetricsStore(project_root, agent_name) + metrics_summary = metrics_store.read_summary() + if metrics_summary is None and metrics_store.executions_path.exists(): + metrics_summary = metrics_store.write_summary() + + pointers = {} + for label, filename in (("scope", "SCOPE.md"), ("todo", "TODO.md")): + candidate = project_root / filename + if candidate.exists(): + pointers[label] = str(candidate) + + return { + "agent": agent_name, + "project": project_root.name, + "generated": _today(), + "agent_prompt": agent_prompt, + "agent_prompt_found": agent_prompt is not None, + "memory": memory, + "metrics_summary": metrics_summary, + "pointers": pointers, + "session_close": [ + f"kaizen-agentic metrics record {agent_name} --success " + f"--time --quality <0-1>", + f"Update memory: kaizen-agentic memory show {agent_name}", + ], + } + + +def _render_prepare_markdown(bundle: dict) -> str: + agent = bundle["agent"] + lines = [ + f"# Scheduled Run Orientation: {agent}", + f"Project: {bundle['project']}", + f"Generated: {bundle['generated']}", + "", + ] + + summary = bundle.get("metrics_summary") + block = performance_summary_markdown(summary or {}) + if block: + lines.append(block) + + lines.append("## Agent Prompt") + if bundle["agent_prompt_found"]: + lines.append(bundle["agent_prompt"]) + else: + lines.append( + f"(agent '{agent}' not found in registry — " f"run: kaizen-agentic list)" + ) + lines.append("") + + lines.append("## Project Memory") + if bundle.get("memory"): + lines.append(bundle["memory"]) + else: + lines.append(f"(none — run: kaizen-agentic memory init {agent})") + lines.append("") + + pointers = bundle.get("pointers") or {} + lines.append("## Repo Pointers") + if pointers: + for label, path in pointers.items(): + lines.append(f"- {label}: {path}") + else: + lines.append("- (no SCOPE.md / TODO.md found)") + lines.append("") + + lines.append("## Session Close") + for cmd in bundle["session_close"]: + lines.append(f"- `{cmd}`") + + return "\n".join(lines) + + def _project_root(target: str) -> Path: return Path(target).resolve() diff --git a/src/kaizen_agentic/schedule.py b/src/kaizen_agentic/schedule.py new file mode 100644 index 0000000..3fbcac4 --- /dev/null +++ b/src/kaizen_agentic/schedule.py @@ -0,0 +1,185 @@ +"""Repo-local scheduled agent execution manifest (.kaizen/schedule.yml). + +ADR-005 defines the schedule contract: which agents run on what cadence in an +opted-in repo. kaizen-agentic owns parsing, validation, and preparing an +orientation bundle for a scheduled run. It does **not** run cron schedules or +invoke Claude — activity-core fires the cron and a coding-agent session executes +the prepared bundle. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional + +import yaml + +SCHEDULE_RELATIVE_PATH = Path(".kaizen") / "schedule.yml" +SCHEDULE_VERSION = "1" +VALID_CADENCES = ("daily", "weekly", "monthly") + +# Sensible defaults for `schedule init` — coach + optimization weekly, the +# heavier tdd-workflow review monthly and disabled until an operator opts in. +DEFAULT_AGENTS: Dict[str, Dict[str, Any]] = { + "coach": {"cadence": "weekly", "cron": "0 9 * * 1", "enabled": True}, + "optimization": {"cadence": "weekly", "cron": "0 10 * * 1", "enabled": True}, + "tdd-workflow": {"cadence": "monthly", "enabled": False}, +} +DEFAULT_TIMEZONE = "Europe/Berlin" + + +class ScheduleError(Exception): + """Raised when a schedule manifest cannot be parsed.""" + + +@dataclass +class ScheduleEntry: + """One scheduled agent run declaration.""" + + agent: str + cadence: str + enabled: bool = True + cron: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + data: Dict[str, Any] = {"cadence": self.cadence, "enabled": self.enabled} + if self.cron: + data["cron"] = self.cron + return data + + +@dataclass +class Schedule: + """Parsed `.kaizen/schedule.yml` manifest.""" + + version: str + timezone: Optional[str] = None + entries: List[ScheduleEntry] = field(default_factory=list) + source_path: Optional[Path] = None + + def entry_for(self, agent: str) -> Optional[ScheduleEntry]: + for entry in self.entries: + if entry.agent == agent: + return entry + return None + + def enabled_entries(self) -> List[ScheduleEntry]: + return [e for e in self.entries if e.enabled] + + +def schedule_path(project_root: Path) -> Path: + """Return the canonical schedule.yml path for a project root.""" + return Path(project_root) / SCHEDULE_RELATIVE_PATH + + +def parse_schedule(data: Any, source_path: Optional[Path] = None) -> Schedule: + """Parse a raw mapping into a Schedule (structural errors raise). + + Semantic validation (known agents, cadence values) is handled by + :func:`validate_schedule` so callers can collect actionable error lists. + """ + if not isinstance(data, dict): + raise ScheduleError("schedule.yml must be a YAML mapping at the top level") + + version = data.get("version") + if version is None: + raise ScheduleError("schedule.yml is missing required key: version") + version = str(version) + + timezone = data.get("timezone") + if timezone is not None and not isinstance(timezone, str): + raise ScheduleError("timezone must be a string") + + agents = data.get("agents", {}) + if not isinstance(agents, dict): + raise ScheduleError("agents must be a mapping of agent-name -> settings") + + entries: List[ScheduleEntry] = [] + for name, settings in agents.items(): + if settings is None: + settings = {} + if not isinstance(settings, dict): + raise ScheduleError(f"agent '{name}' settings must be a mapping") + cron = settings.get("cron") + if cron is not None and not isinstance(cron, str): + raise ScheduleError(f"agent '{name}' cron must be a string") + entries.append( + ScheduleEntry( + agent=str(name), + cadence=str(settings.get("cadence", "")), + enabled=bool(settings.get("enabled", True)), + cron=cron, + ) + ) + + return Schedule( + version=version, + timezone=timezone, + entries=entries, + source_path=source_path, + ) + + +def load_schedule(path: Path) -> Schedule: + """Load and parse a schedule.yml file (raises ScheduleError).""" + path = Path(path) + if not path.exists(): + raise ScheduleError(f"schedule file not found: {path}") + try: + raw = yaml.safe_load(path.read_text(encoding="utf-8")) + except yaml.YAMLError as exc: # pragma: no cover - passthrough message + raise ScheduleError(f"invalid YAML in {path}: {exc}") from exc + return parse_schedule(raw, source_path=path) + + +def validate_schedule( + schedule: Schedule, known_agents: Optional[List[str]] = None +) -> List[str]: + """Return a list of human-readable validation errors (empty == valid).""" + errors: List[str] = [] + + if schedule.version != SCHEDULE_VERSION: + errors.append( + f"unsupported version '{schedule.version}' " + f"(expected '{SCHEDULE_VERSION}')" + ) + + if not schedule.entries: + errors.append("no agents declared under 'agents:'") + + seen: set = set() + known = set(known_agents) if known_agents is not None else None + for entry in schedule.entries: + if entry.agent in seen: + errors.append(f"duplicate agent entry: {entry.agent}") + seen.add(entry.agent) + + if entry.cadence not in VALID_CADENCES: + errors.append( + f"agent '{entry.agent}': invalid cadence '{entry.cadence}' " + f"(expected one of {', '.join(VALID_CADENCES)})" + ) + + if known is not None and entry.agent not in known: + errors.append( + f"agent '{entry.agent}' is not an installed or packaged agent" + ) + + return errors + + +def default_schedule_yaml(timezone: str = DEFAULT_TIMEZONE) -> str: + """Render the default schedule.yml scaffold for `schedule init`.""" + document = { + "version": SCHEDULE_VERSION, + "timezone": timezone, + "agents": dict(DEFAULT_AGENTS), + } + header = ( + "# Kaizen scheduled agent execution manifest (ADR-005)\n" + "# Declares which agents run on what cadence in this repo.\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_integration_patterns.py b/tests/test_integration_patterns.py index 959ab8f..cecb4f2 100644 --- a/tests/test_integration_patterns.py +++ b/tests/test_integration_patterns.py @@ -13,7 +13,8 @@ DEFINITIONS_DIR = ( def test_activity_definitions_have_required_frontmatter(): files = list(DEFINITIONS_DIR.glob("*.md")) - assert len(files) == 3 + # 3 from WP-0004 (metrics) + 2 from WP-0006 (scheduled agent runs) + assert len(files) == 5 for path in files: text = path.read_text(encoding="utf-8") diff --git a/tests/test_schedule_cli.py b/tests/test_schedule_cli.py new file mode 100644 index 0000000..44f58e9 --- /dev/null +++ b/tests/test_schedule_cli.py @@ -0,0 +1,166 @@ +"""CLI + module tests for scheduled agent execution (ADR-005, WP-0006).""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from kaizen_agentic.cli import cli +from kaizen_agentic.schedule import ( + ScheduleError, + parse_schedule, + schedule_path, + validate_schedule, +) + + +@pytest.fixture +def runner() -> CliRunner: + return CliRunner() + + +@pytest.fixture +def project_dir(tmp_path: Path) -> Path: + root = tmp_path / "demo-project" + root.mkdir() + return root + + +class TestScheduleModule: + def test_parse_requires_version(self): + with pytest.raises(ScheduleError): + parse_schedule({"agents": {}}) + + def test_parse_rejects_non_mapping(self): + with pytest.raises(ScheduleError): + parse_schedule(["not", "a", "mapping"]) + + def test_validate_flags_unknown_agent_and_bad_cadence(self): + schedule = parse_schedule( + { + "version": "1", + "agents": { + "coach": {"cadence": "weekly", "enabled": True}, + "made-up": {"cadence": "hourly"}, + }, + } + ) + errors = validate_schedule(schedule, known_agents=["coach", "optimization"]) + assert any("hourly" in e for e in errors) + assert any("made-up" in e for e in errors) + + def test_validate_clean_schedule(self): + schedule = parse_schedule( + {"version": "1", "agents": {"coach": {"cadence": "weekly"}}} + ) + assert validate_schedule(schedule, known_agents=["coach"]) == [] + + +class TestScheduleCli: + def test_init_creates_default_schedule(self, runner: CliRunner, project_dir: Path): + result = runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)]) + assert result.exit_code == 0 + path = schedule_path(project_dir) + assert path.exists() + assert "coach" in path.read_text() + + def test_init_no_overwrite_without_force( + self, runner: CliRunner, project_dir: Path + ): + runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)]) + result = runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)]) + assert result.exit_code == 0 + assert "already exists" in result.output + + def test_validate_passes_on_default(self, runner: CliRunner, project_dir: Path): + runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)]) + result = runner.invoke( + cli, ["schedule", "validate", "--target", str(project_dir)] + ) + assert result.exit_code == 0 + assert "valid" in result.output + + def test_validate_missing_file_errors(self, runner: CliRunner, project_dir: Path): + result = runner.invoke( + cli, ["schedule", "validate", "--target", str(project_dir)] + ) + assert result.exit_code == 1 + + def test_validate_rejects_bad_schema(self, runner: CliRunner, project_dir: Path): + path = schedule_path(project_dir) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("version: '1'\nagents:\n not-an-agent:\n cadence: weekly\n") + result = runner.invoke( + cli, ["schedule", "validate", "--target", str(project_dir)] + ) + assert result.exit_code == 1 + assert "not-an-agent" in result.output + + def test_list_shows_enabled(self, runner: CliRunner, project_dir: Path): + runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)]) + result = runner.invoke(cli, ["schedule", "list", "--target", str(project_dir)]) + assert result.exit_code == 0 + assert "coach" in result.output + # tdd-workflow is disabled by default; hidden without --all + assert "tdd-workflow" not in result.output + + def test_list_all_shows_disabled(self, runner: CliRunner, project_dir: Path): + runner.invoke(cli, ["schedule", "init", "--target", str(project_dir)]) + result = runner.invoke( + cli, ["schedule", "list", "--all", "--target", str(project_dir)] + ) + assert "tdd-workflow" in result.output + + def test_prepare_markdown_includes_agent_prompt( + self, runner: CliRunner, project_dir: Path + ): + result = runner.invoke( + cli, ["schedule", "prepare", "coach", "--target", str(project_dir)] + ) + assert result.exit_code == 0 + assert "Scheduled Run Orientation: coach" in result.output + assert "## Agent Prompt" in result.output + assert "Coach Agent" in result.output + assert "## Session Close" in result.output + + def test_prepare_json_format(self, runner: CliRunner, project_dir: Path): + result = runner.invoke( + cli, + [ + "schedule", + "prepare", + "coach", + "--target", + str(project_dir), + "--format", + "json", + ], + ) + assert result.exit_code == 0 + payload = json.loads(result.output) + assert payload["agent"] == "coach" + assert payload["agent_prompt_found"] is True + assert payload["session_close"] + + def test_prepare_unknown_agent_notes_missing( + self, runner: CliRunner, project_dir: Path + ): + result = runner.invoke( + cli, + ["schedule", "prepare", "no-such-agent", "--target", str(project_dir)], + ) + assert result.exit_code == 0 + assert "not found in registry" in result.output + + def test_prepare_includes_memory_when_present( + self, runner: CliRunner, project_dir: Path + ): + runner.invoke(cli, ["memory", "init", "coach", "--target", str(project_dir)]) + result = runner.invoke( + cli, ["schedule", "prepare", "coach", "--target", str(project_dir)] + ) + assert "## Project Memory" in result.output + assert "Project Context" in result.output diff --git a/wiki/EcosystemIntegration.md b/wiki/EcosystemIntegration.md index 1881f6b..d0d30a0 100644 --- a/wiki/EcosystemIntegration.md +++ b/wiki/EcosystemIntegration.md @@ -96,7 +96,20 @@ ActivityDefinition reference copies (sync into activity-core to activate): - [post-install-metrics-scaffold](../docs/integrations/activity-definitions/post-install-metrics-scaffold.md) - [low-success-rate-review](../docs/integrations/activity-definitions/low-success-rate-review.md) -**Workplan:** KAIZEN-WP-0004 Part 2. Patterns: [docs/INTEGRATION_PATTERNS.md](../docs/INTEGRATION_PATTERNS.md). +**Scheduled agent execution (WP-0006, ADR-005)** — run agents (not just the +metrics optimizer) on a cadence against a preselected repo roster: + +- [weekly-coach-orientation](../docs/integrations/activity-definitions/weekly-coach-orientation.md) +- [weekly-optimization-review](../docs/integrations/activity-definitions/weekly-optimization-review.md) +- Resolver spec: [discover-kaizen-scheduled-repos](../docs/integrations/discover-kaizen-scheduled-repos.md) +- Roster fields: [state-hub-roster-fields](../docs/integrations/state-hub-roster-fields.md) +- Handoff checklist: [activity-core-handoff-wp0006](../docs/integrations/activity-core-handoff-wp0006.md) + +A repo opts in by committing `.kaizen/schedule.yml` (`kaizen-agentic schedule +init`); activity-core fires the cron and creates a task per `(repo, agent)` that +runs `kaizen-agentic schedule prepare `. + +**Workplan:** KAIZEN-WP-0004 Part 2 + KAIZEN-WP-0006. Patterns: [docs/INTEGRATION_PATTERNS.md](../docs/INTEGRATION_PATTERNS.md). ### artifact-store (P1) @@ -194,4 +207,4 @@ WP-0001 T04. Assess before depending on it. Persisted in `history/`: - `2026-06-16-intent-gap-analysis.md` -- `2026-06-16-ecosystem-assessment.md` \ No newline at end of file +- `2026-06-16-ecosystem-assessment.md` diff --git a/workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md b/workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md index db8c0a2..4c740b2 100644 --- a/workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md +++ b/workplans/kaizen-agentic-WP-0006-scheduled-agent-execution.md @@ -4,7 +4,7 @@ type: workplan title: "Scheduled Agent Execution via activity-core (v1.3.0)" domain: custodian repo: kaizen-agentic -status: active +status: done owner: kaizen-agentic topic_slug: custodian state_hub_workstream_id: d4edb92b-526f-4ee4-8efe-a9fa84ab6dff @@ -16,81 +16,81 @@ updated: "2026-06-17" tasks: - id: T01 state_hub_task_id: f513cf74-6e32-4dce-a3bc-2995f98a30de - status: todo + status: done title: Write ADR-005 scheduled agent execution contract - id: T02 state_hub_task_id: dcb8d674-5472-4657-85bc-02ca9890fb92 - status: todo + status: done title: Define .kaizen/schedule.yml schema and example - id: T03 state_hub_task_id: 1f9a4959-d200-4d7b-8666-96472e23d820 - status: todo + status: done title: Add kaizen-agentic schedule validate command - id: T04 state_hub_task_id: 13ab4b76-ded2-4613-b8e2-15ab206d8c57 - status: todo + status: done title: Document fleet roster and repo opt-in in INTEGRATION_PATTERNS - id: T05 state_hub_task_id: 685b35f3-af20-483a-9136-150f5fff68dd - status: todo + status: done title: Draft state-hub roster query design for activity-core resolver - id: T06 state_hub_task_id: 5f0a0859-3ff5-4ec8-b916-35486f045b2b - status: todo + status: done title: Draft discover_kaizen_scheduled_repos context resolver spec - id: T07 state_hub_task_id: 2fd0f00c-5cf1-47bd-9c12-aee65bca5932 - status: todo + status: done title: Draft weekly-agent-run ActivityDefinition template - id: T08 state_hub_task_id: cc0565bc-ccd4-4739-9c7e-088adaa9834b - status: todo + status: done title: Open activity-core handoff issue for resolver and definitions - id: T09 state_hub_task_id: 23aeff77-3429-438b-a395-5c7b5dc2824e - status: todo + status: done title: Implement kaizen-agentic schedule prepare command - id: T10 state_hub_task_id: cba9685b-408f-4292-8169-9965e7ad5c5b - status: todo + status: done title: Bundle memory brief metrics and agent prompt in prepare output - id: T11 state_hub_task_id: 7c1b049b-5566-4b5e-9d82-d9c29fab0c84 - status: todo + status: done title: Add schedule init scaffold for new repos - id: T12 state_hub_task_id: 80a8e98a-9146-4a58-8fd3-8c37f7893960 - status: todo + status: done title: Unit tests for schedule validate and prepare - id: T13 state_hub_task_id: 9aee6348-d5ad-4413-b01c-5588b07c8ede - status: todo + status: done title: Pilot ActivityDefinition for weekly coach on custodian repos - id: T14 state_hub_task_id: 697e612b-7531-4329-82fb-b0423ba86904 - status: todo + status: done title: Pilot ActivityDefinition for weekly optimization review - id: T15 state_hub_task_id: 92db5130-2212-4780-ab8f-1de3adf6c6f8 - status: todo + status: done title: Smoke test end-to-end on two preselected repos - id: T16 state_hub_task_id: bbecdcbf-42b8-4b48-9f85-e4c10e4ff3ab - status: todo + status: done title: Document operator enablement in PACKAGE_RELEASE and agency-framework - id: T17 state_hub_task_id: a9e91d4b-990a-457a-9591-d1243db5b386 - status: todo + status: done title: Emit kaizen.schedule.prepared event payload spec for activity-core - id: T18 state_hub_task_id: 73986472-bf19-4b13-af1b-6505ab944459 - status: todo + status: done title: Update wiki/EcosystemIntegration.md and CHANGELOG for v1.3.0 --- # KAIZEN-WP-0006 — Scheduled Agent Execution via activity-core -**Status:** active +**Status:** done **Owner:** kaizen-agentic **Repo:** kaizen-agentic **Target version:** 1.3.0 @@ -172,9 +172,9 @@ agents: ### Tasks -- [ ] T01 — Write ADR-005: scheduled agent execution (roster, schedule file, dispatch, boundaries) -- [ ] T02 — Add `docs/integrations/schedule-schema.md` + example under `docs/examples/.kaizen/schedule.yml` -- [ ] T03 — Implement `kaizen-agentic schedule validate [--target PATH]` (schema + agent name checks) +- [x] T01 — Write ADR-005: scheduled agent execution (roster, schedule file, dispatch, boundaries) +- [x] T02 — Add `docs/integrations/schedule-schema.md` + example under `docs/examples/.kaizen/schedule.yml` +- [x] T03 — Implement `kaizen-agentic schedule validate [--target PATH]` (schema + agent name checks) ### Definition of done @@ -198,9 +198,9 @@ Define which registered repos participate in fleet scheduling. ### Tasks -- [ ] T04 — Extend `docs/INTEGRATION_PATTERNS.md` Pattern 2 with roster + schedule model -- [ ] T05 — Design doc: state-hub fields/filters for kaizen-scheduled repos (no state-hub code in this repo) -- [ ] T06 — Spec `discover_kaizen_scheduled_repos` resolver for activity-core (inputs, output shape, errors) +- [x] T04 — Extend `docs/INTEGRATION_PATTERNS.md` Pattern 2 with roster + schedule model +- [x] T05 — Design doc: state-hub fields/filters for kaizen-scheduled repos (no state-hub code in this repo) +- [x] T06 — Spec `discover_kaizen_scheduled_repos` resolver for activity-core (inputs, output shape, errors) ### Definition of done @@ -223,9 +223,9 @@ Generalize WP-0004 metrics definitions into **agent-run** definitions. ### Tasks -- [ ] T07 — Add `docs/integrations/activity-definitions/weekly-coach-orientation.md` -- [ ] T08 — Open activity-core coordination issue/PR checklist (resolver + sync + `enabled: false` pilot) -- [ ] T17 — Document `kaizen.schedule.prepared` event payload (for future event-driven runs) +- [x] T07 — Add `docs/integrations/activity-definitions/weekly-coach-orientation.md` +- [x] T08 — Open activity-core coordination issue/PR checklist (resolver + sync + `enabled: false` pilot) +- [x] T17 — Document `kaizen.schedule.prepared` event payload (for future event-driven runs) ### Definition of done @@ -258,10 +258,10 @@ kaizen-agentic schedule list [--target PATH] # show enabled entries from s ### Tasks -- [ ] T09 — Implement `schedule` CLI command group skeleton -- [ ] T10 — Wire `prepare` to memory + metrics + agent loader (reuse existing CLI internals) -- [ ] T11 — Implement `schedule init` with sensible defaults (coach + optimization weekly, disabled tdd-workflow) -- [ ] T12 — Tests for validate, init, prepare (temp repo fixtures) +- [x] T09 — Implement `schedule` CLI command group skeleton +- [x] T10 — Wire `prepare` to memory + metrics + agent loader (reuse existing CLI internals) +- [x] T11 — Implement `schedule init` with sensible defaults (coach + optimization weekly, disabled tdd-workflow) +- [x] T12 — Tests for validate, init, prepare (temp repo fixtures) ### Definition of done @@ -283,11 +283,11 @@ Prove the loop on a small custodian repo set before fleet-wide enable. ### Tasks -- [ ] T13 — Commit pilot `weekly-coach-orientation` definition; map to custodian pilot repos -- [ ] T14 — Commit pilot `weekly-optimization-review` definition -- [ ] T15 — Smoke test: dry-run activity-core → manual `schedule prepare` on two repos → verify task payload -- [ ] T16 — Operator docs: credentials, PATH, Gitea index, enabling `enabled: true` in activity-core -- [ ] T18 — `wiki/EcosystemIntegration.md`, `CHANGELOG [Unreleased]`, `TODO.md` pointer +- [x] T13 — Commit pilot `weekly-coach-orientation` definition; map to custodian pilot repos +- [x] T14 — Commit pilot `weekly-optimization-review` definition +- [x] T15 — Smoke test: dry-run activity-core → manual `schedule prepare` on two repos → verify task payload +- [x] T16 — Operator docs: credentials, PATH, Gitea index, enabling `enabled: true` in activity-core +- [x] T18 — `wiki/EcosystemIntegration.md`, `CHANGELOG [Unreleased]`, `TODO.md` pointer ### Definition of done