Makes the state hub an event publisher so activity-core can drive
maintenance automation declaratively via ActivityDefinitions, rather
than the hub creating tasks itself.
- api/events/: lazy JetStream publisher + EventEnvelope mirroring
activity-core's contract; no-op when NATS_URL unset, fire-and-forget
with logged failures so publishing never breaks an API request.
- Wired publishers on the five v1.0 lifecycle events:
org.statehub.repo.registered (POST /repos/)
org.statehub.workstream.completed (PATCH /workstreams/* on transition)
org.statehub.decision.resolved (POST /decisions/*/resolve)
org.statehub.domain.goal.activated (POST /domain-goals/*/activate)
org.statehub.task.stale (scripts/cleanup_stale_tasks.py)
- docs/nats-event-subjects.md: subject naming convention + catalog.
- docs/cron-migration.md: design stub for replacing custodian-sync
systemd timer and cleanup-stale cron with ActivityDefinitions
(depends on activity-core WP-0003).
- docs/activity-core-delegation.md: protocol, invariants, cutover plan.
- SCOPE.md: declares activity-core as downstream event consumer and
restates that the state hub stays a read model, not a task factory.
Workplan: workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md
242 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4.7 KiB
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 usesstatehub{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.workstream.completed |
A workstream transitions to status completed |
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:
{
"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": "the-custodian/state-hub",
"attributes": { "...": "event-specific" }
}
type matches the subject. publisher is always
the-custodian/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
- Pick a subject following the convention above.
- Add a row to the table in this file (subject, trigger, attributes).
- Add a matching
event-types/entry in activity-core. - Wire
publish_event(subject, EventEnvelope.new(subject, attributes))at the site of the state transition (inside the same DB transaction only afterawait session.commit()— never publish optimistically). - 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.