activity-core/docs/recurring-automations-playbook.md
tegwick 6ce77a72bb
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Document ACTIVITY-WP-0026-T07 railiance deploy checklist.
Add deploy-ops-run-queue-railiance.md (image, migrate 0007, smoke, dual-path
residual), wire OPS_RUN_* into runtime ConfigMap, and cross-link runbook/README.
T07 stays open until prod smoke is executed.
2026-08-03 19:30:00 +02:00

174 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Recurring automations playbook — activity-core as the organizer
**Audience:** operators and coding agents adding or fixing recurring work
**Principle:** activity-core answers **when / what / where** only. It does **not** execute.
**Architecture decision:** [ACT-ADR-005](adr/adr-005-ops-runs-vs-dev-work-records.md) —
ops runs (claimable DB) vs development work records (repo files + State Hub).
**Implementation stack:** ACTIVITY-WP-0026 · REIN-A-0002 · ISSUE-WP-0006 · STATE-WP-0078.
This is the org standard. Prefer this path over workstation cron, ad-hoc shell
timers, or “Ill remember to run it.”
**Self-hosted forge:** **Forgejo** only (not Gitea). External tickets via
issue-core projection — never as the default ops claim queue.
---
## The three layers (do not collapse them)
```text
┌─────────────────────────────────────────────────────────────┐
│ 1. activity-core — WHEN / WHAT / WHERE │
│ Temporal schedule · context resolvers · rules │
│ → INSERT ops_run (open) + dual-write activity_task_spawn │
│ Claim API: POST /ops-runs/claim (docs/ops-run-queue.md) │
└────────────────────────────┬────────────────────────────────┘
┌────────────────────────────▼────────────────────────────────┐
│ 2. rein-aharness (or domain executor) — DOES THE WORK │
│ claim ops_run · llm-connect · checkout · complete/fail │
│ Host timers on railiance are *interim* until REIN-A-0002 │
│ claim loop is live (state-hub spawn alone is not claimable)│
└────────────────────────────┬────────────────────────────────┘
┌────────────────────────────▼────────────────────────────────┐
│ 3. domain repo — INSTANCE STATE │
│ activity-definitions/ · playbooks · briefs/ · catalogs │
│ completion event types (fi_daily_brief, binky_daily_brief)│
└─────────────────────────────────────────────────────────────┘
```
| Layer | Owns | Must not own |
| ----- | ---- | ------------ |
| **activity-core** | Triggers, due checks, task specs, audit of spawns | Brief writing, git commits, LLM prompts for domain content |
| **rein-aharness** | Execution session, credentials at runtime, hub completion | Inventing schedules, domain policy |
| **domain repo** | Definition markdown, playbook, artifacts | Scattered crons that bypass activity-core |
**Anti-pattern:** workstation `crontab` that “does the daily brief” while
activity-core also schedules it — dual cadence and forgotten design.
**Correct interim (Binky / FI):** activity-core schedule is authority for *when
due*; railiance user-systemd timer invokes rein-aharness a few minutes later as
executor. Document both; never pretend the host timer *is* the scheduler.
---
## How to add a recurring automation (checklist)
### A. Domain definition (source of truth)
1. Create `{domain-repo}/activity-definitions/{slug}.md`
- `trigger.type: cron` + timezone + `misfire_policy`
- `context_sources` with a **due resolver** when work is once-per-day
- `rule` with `target_repo`, labels, description pointing at the playbook
- Start with `enabled: false` until dry-run
2. Implement completion event shape in domain playbook
(e.g. `fi_daily_brief` with `detail.repo` + `detail.date`)
3. If needed, add resolver in activity-core (`context_resolvers/`) + tests
### B. Register with activity-core
| Environment | How |
| ----------- | --- |
| **Dev** | `ACTIVITY_DEFINITION_DIRS=/path/to/domain-repo` + `make sync-activity-definitions` |
| **Railiance** | Project definition into ConfigMap `actcore-external-activity-definitions` (and keep `k8s/railiance/20-runtime.yaml` in git) |
Then:
```bash
# after ConfigMap / files update
kubectl -n activity-core exec deploy/actcore-api -- python3 -c \
'import urllib.request as u; print(u.urlopen(u.Request(
"http://localhost:8010/admin/sync?definitions=true&schedules=true",
method="POST")).read().decode())'
```
Verify:
```bash
# definition enabled + cron
# Temporal schedule: activity-schedule-<definition-uuid> not paused
kubectl -n activity-core exec deploy/actcore-worker -- python3 -c '...' # describe schedule
```
Ops UI: `activity.coulomb.social` → automations status / trigger (SSO).
### C. Executor (rein-aharness) — target vs interim
**Target (ACT-ADR-005 / REIN-A-0002):** continuous **claim loop** on
`ops_run` rows from activity-core; approach table selects adapter
(`fi-research-brief`, `brief-daily`, mail-scan, …).
**Interim (until WP-0026 + REIN-A-0002 cutover):** railiance user-systemd
timer may call rein-aharness wall-clock — **break-glass / dual-clock**, not
cadence authority.
1. Prefer a **named rein-aharness command** + **llm-connect**, not Claude CLI on railiance01
2. On success, post the **domain completion event** so the resolver sets `due=false`
3. Close the **ops_run** (when claim queue is live)
### D. Prove the loop
1. `POST /activity-definitions/<id>/trigger` (ops UI or API)
2. Confirm `activity_task_spawn` progress event (when sink=state-hub)
3. Run executor once (timer or manual rein-aharness)
4. Confirm domain completion event → resolver `due=false` for that day
5. Set `enabled: true` if still false; commit domain + activity-core CM projection
---
## Where definitions live
| Kind | Location |
| ---- | -------- |
| Org-wide (SBOM, consistency, inventory) | `activity-core/activity-definitions/` and/or ConfigMap |
| Domain-specific (Binky, FI, …) | `{domain}/activity-definitions/` + ConfigMap projection |
ADR-002: domain repos own domain automations; activity-core discovers them via
`ACTIVITY_DEFINITION_DIRS` or the external ConfigMap mount.
---
## Sinks (ACTIVITY-WP-0022)
| `ISSUE_SINK_TYPE` | Use |
| ---------------- | --- |
| **`state-hub`** (default) | Internal fleet work → `activity_task_spawn` progress |
| **`null`** | Dry-run |
| **`rest`** | Explicit external issue-core / Forgejo only |
Do not flip production to `rest` globally.
---
## Reference implementations
| Domain | Definition | Executor |
| ------ | ---------- | -------- |
| **Binky** | `binky-daily-rhythm` in activity-core / binky | `rein-aharness brief-daily` + `binky-control/scripts/railiance-rhythm/` |
| **Freedom Intelligence** | `fi-daily-research-brief` | `rein-aharness fi-research-brief` + `freedom-intelligence/scripts/railiance-rhythm/` |
---
## Operator daily habit
1. Ops console: did automations run? (`/ops/automations/status`) — includes
`ops_runs.counts` and `stuck_open_or_claimed` (SLA, default 1h)
2. Claim queue: `GET /ops-runs?state=open` (or failed) — harness backlog
3. State Hub: `activity_task_spawn` + domain completion events
4. Domain repo: brief/artifact files + git history
5. If something is missing, **fix activity-core first** (schedule paused?
definition disabled? resolver due stuck? open ops_run with no claim?) —
do not invent a new cron
---
## Related
- `INTENT.md` — when/what/where boundary
- `docs/adr/adr-005-ops-runs-vs-dev-work-records.md` — ops vs dev work
- `docs/ops-run-queue.md` — claim API
- `docs/deploy-ops-run-queue-railiance.md` — T07 railiance rollout checklist
- `docs/task-emission-consumer-contract.md` — spawn payload + consumer duties
- `docs/runbook.md` — sync, trigger, ops UI
- `docs/adr/adr-002-definition-format.md` — definition files