# NATS Event Subjects — State Hub

> Part of CUST-WP-0040. Cross-reference: activity-core's
> `event-types/` registry and ADR-001 (event bridge architecture).

The state hub publishes lifecycle events to NATS JetStream so that
activity-core can drive maintenance and reaction automation declaratively,
via `ActivityDefinition` rules — rather than the state hub creating tasks
itself.

This document is the authoritative subject naming convention for state hub
events. When adding a new event, add a row to the table below first and
keep the activity-core `event-types/` registry in sync.

---

## Naming convention

```
org.{producer}.{noun}.{verb}[.{qualifier}]
```

- **`org`** — top-level namespace shared with activity-core (`org.>`)
- **`{producer}`** — the publisher subsystem; the state hub uses `statehub`
- **`{noun}`** — entity the event is about (`repo`, `workstream`, `task`, …)
- **`{verb}`** — past-tense state transition (`registered`, `completed`, `resolved`, …)
- **`{qualifier}`** — optional refinement (e.g. `goal.activated`)

All segments are lowercase ASCII. No camelCase, no dashes inside segments.

### Why a `statehub` namespace?

Activity-core listens to `activity.>` for its internal task lifecycle and
`org.>` for org-wide lifecycle events. Multiple publishers will eventually
share `org.>` (e.g. railiance, kaizen). The `{producer}` segment keeps
those publishers from colliding on the same `{noun}.{verb}` shape.

---

## Published subjects (v1.0)

| Subject                              | When                                                         | Required attributes                                                                                                          |
| ------------------------------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `org.statehub.repo.registered`       | A new repo is registered via `POST /repos/`                  | `repo_id`, `repo_slug`, `domain_slug`, `remote_url?`, `local_path?`                                                          |
| `org.statehub.workplan.completed`    | A workplan transitions to canonical status `finished`        | `workplan_id`, `legacy_workstream_id`, `slug`, `title`, `topic_id`, `repo_id?`, `repo_goal_id?`                              |
| `org.statehub.workstream.completed`  | Legacy compatibility subject for completed workplans         | `workstream_id`, `slug`, `title`, `topic_id`, `repo_id?`, `repo_goal_id?`                                                    |
| `org.statehub.decision.resolved`     | A decision is resolved via `POST /decisions/{id}/resolve`    | `decision_id`, `title`, `topic_id?`, `workstream_id?`, `decided_by`, `rationale_snippet`                                     |
| `org.statehub.domain.goal.activated` | A domain goal transitions to `active`                        | `goal_id`, `domain_id`, `domain_slug`, `title`, `superseded_goal_ids[]`                                                      |
| `org.statehub.task.stale`            | `scripts/cleanup_stale_tasks.py` cancels an out-of-date task | `task_id`, `workstream_id`, `workstream_status`, `task_title`, `task_status_before`                                          |

### Envelope shape

Each message body conforms to the `EventEnvelope` schema in
`api/events/envelope.py`, mirrored from
`activity-core/src/activity_core/models.py`:

```json
{
  "id": "uuid v4 — stable, used for at-least-once dedup",
  "type": "org.statehub.repo.registered",
  "version": "1.0",
  "timestamp": "2026-05-17T14:00:00Z",
  "publisher": "state-hub",
  "attributes": { "...": "event-specific" }
}
```

`type` matches the subject. `publisher` is always
`state-hub` for events emitted from this repo.

---

## Stream

State hub events are published into the **`ACTIVITY_EVENTS`** JetStream
(subject filter `org.>`). The stream is owned by activity-core; the state
hub will auto-create it on first publish if it does not exist, so the
publisher works in dev environments without bootstrapping activity-core
first. In production both services point at the same NATS cluster and
activity-core's `EventRouter` consumes the stream durably.

---

## Adding a new event

1. Pick a subject following the convention above.
2. Add a row to the table in this file (subject, trigger, attributes).
3. Add a matching `event-types/` entry in activity-core.
4. Wire `publish_event(subject, EventEnvelope.new(subject, attributes))`
   at the site of the state transition (inside the same DB transaction
   only after `await session.commit()` — never publish optimistically).
5. Verify locally: run `nats sub 'org.statehub.>'` while triggering the
   transition.

## Versioning

`version` is bumped only when an attribute is removed or its semantics
change. Adding optional attributes does **not** require a version bump.
Activity-core consumers must tolerate unknown attribute keys.
