activity-core/docs/task-emission-consumer-contract.md
tegwick f0a897e088
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 33s
Harden ops run identity and leases
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
2026-08-23 13:01:46 +02:00

109 lines
4.9 KiB
Markdown

# Task emission consumer contract
**Audience:** agent-harness, per-repo Temporal workers, operators.
**Owners:** activity-core (producer), consumers (executors).
**Related:** ACTIVITY-WP-0022 (sink policy), ACTIVITY-WP-0023-T02 (executor gap),
**ACT-ADR-005 / ACTIVITY-WP-0026** (ops_run claim queue).
activity-core answers **when / what / where**. It does **not** execute work.
Consumers must pick up emitted tasks and produce domain evidence.
## Primary claim path: `ops_run` (ACTIVITY-WP-0026)
For **scheduled / automation** work (FI daily brief, Binky rhythm, etc.), the
authoritative claimable instance is an **`ops_run`** row in activity-core — not
issue-core and not a Forgejo ticket.
| Step | Call | Notes |
| ---- | ---- | ----- |
| List / poll | `GET /ops-runs?state=open` | Filter by labels via claim body |
| Claim | `POST /ops-runs/claim` | `{ worker_id, labels?, limit?, lease_seconds? }` |
| Heartbeat | `POST /ops-runs/{id}/heartbeat` | Extend lease during long runs |
| Complete | `POST /ops-runs/{id}/complete` | `{ worker_id, result }` |
| Fail | `POST /ops-runs/{id}/fail` | `{ worker_id, error, reopen? }` |
The worker token is bound by Activity Core to one configured `worker_id`.
Every worker mutation must name that exact identity and must still hold a lease
whose deadline is strictly in the future. Operator/SSO authentication does not
substitute for worker authentication on these calls.
Full field list, auth, and env: **`docs/ops-run-queue.md`**.
Consumer implementation (rein-aharness): **REIN-A-0002**.
Emit dual-writes: `ops_run` (claim) + existing IssueSink progress (`state-hub`
by default) + `task_spawn_log` audit. Do **not** treat `activity_task_spawn`
progress as a claim queue — it is visibility only.
## Sink matrix (ACTIVITY-WP-0022) — dual-write / projection
| `ISSUE_SINK_TYPE` | Behaviour | When to use |
| --- | --- | --- |
| **`state-hub` (default)** | POST State Hub `/progress/` with `event_type=activity_task_spawn` (override via `STATE_HUB_TASK_EVENT_TYPE`) | Internal fleet findings; no Forgejo issues |
| **`null`** | Synthetic `null-*` refs in `task_spawn_log` only | Dry-run / contract review |
| **`rest`** | POST issue-core `/issues/` (may project to Forgejo) | **Explicit opt-in** only when external tracker issues are intended and backend is healthy |
Unset or unknown values fall back to **`state-hub`** (safe default).
Ops claim does **not** require `rest` or issue-core.
## Payload: `activity_task_spawn` (State Hub)
Produced by `StateHubProgressSink`. Consumers should treat `detail` as the
**visibility** task spec; claim authority is `ops_run` when the queue is enabled.
```json
{
"event_type": "activity_task_spawn",
"author": "activity-core",
"summary": "<task title, max ~240 chars>",
"detail": {
"task_ref": "sh-<uuid>",
"title": "Run Binky daily rhythm (daily_brief) for 2026-07-21",
"description": "...",
"target_repo": "binky-control",
"priority": "medium",
"labels": ["binky", "rhythm", "automated"],
"source_type": "rule",
"source_id": "emit-daily-rhythm-task",
"triggering_event_id": "manual-… or scheduled",
"activity_definition_id": "<activity uuid>",
"backend": "state-hub-progress"
}
}
```
### Required consumer behaviour
1. **Idempotency:** key on `detail.task_ref` or
`(activity_definition_id, triggering_event_id, source_id, title)`.
2. **Routing:** use `target_repo` (and labels) to select checkout / lane.
3. **Completion evidence:** post a domain progress event when work finishes
(e.g. Binky: `event_type=binky_daily_brief` with `detail.repo=binky-control`
and date), so rhythm resolvers can set `due=false`.
4. **Do not** re-implement task lifecycle in activity-core.
## Payload: issue-core REST (`ISSUE_SINK_TYPE=rest`)
See `docs/issue-core-emission-boundary.md`. `task_spawn_log.task_ref` is the
issue-core `issue_id`. Prefer this only for intentional external projection.
## Binky daily brief path (reference)
1. Schedule / one-shot: `Binky Daily Operating Rhythm`.
2. Context: `binky_rhythm_status``due=true` for `daily_brief`.
3. Rule emit → sink (`state-hub` recommended).
4. **Consumer (out of repo):** agent-harness or operator session runs
`binky-control` OperatingRhythm / brief scripts.
5. Completion: post `binky_daily_brief` progress so the next fire is not due.
Until a harness consumer is wired, operators may complete the brief manually
and still post the progress event — spawn without completion leaves `due=true`.
## Anti-patterns
- Adding an in-repo task executor or reviving the retired
`TaskExecutorWorkflow`/`task_instances` surface.
- Global `ISSUE_SINK_TYPE=rest` for all definitions (reintroduces Forgejo spam).
- Treating `task_spawn_log` or State Hub `activity_task_spawn` as claim authority
(use `POST /ops-runs/claim` — ACT-ADR-005).
- Using issue-core or Forgejo as the ops automation claim queue.
- Product references to **Gitea** — self-hosted forge is **Forgejo** only.