Files
state-hub/docs/nats-event-subjects.md
tegwick 9dd71af8f9 feat(state-hub): CUST-WP-0040 — NATS lifecycle event publishing for activity-core
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>
2026-05-17 05:49:29 +02:00

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 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.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

  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.