Documentation
¶
Overview ¶
Package commons — Branding factory per spec V3 §6.3 + §11.0.
Package commons — clock abstraction per spec V3 §3.5.
Herald never calls time.Now() directly outside this file. The Clock abstraction lets tests fast-forward time, which is essential for quiet-hours, batching windows, retry backoff, idempotency TTLs, and escalation chains.
Participant identity model per docs/design/PARTICIPANT_ATTRIBUTION.md §1.
A Participant is one logical person/agent (a Subscriber/User) who may carry a DIFFERENT @username on every messenger channel. Workable items store the canonical, messenger-neutral handle in their created_by / assigned_to fields; the IdentityResolver bridges between that canonical world and the per-channel runtime (inbound sender resolution + outbound @username lookup for tagging).
This is the contract surface other streams (inbound, workflow, channel adapters, storage) code against — the Go signatures here are load-bearing and match the §1/§3 contract exactly.
Notification-tagging matrix per docs/design/PARTICIPANT_ATTRIBUTION.md §3.
Package commons is Herald's L0 foundation. See spec V3 §10 + §11.0.
This file defines the Channel interface every adapter implements and its complete set of value types. Adapters under commons_messaging/ channels/<name>/ consume these types directly; no adapter is allowed to invent its own equivalent (spec §11.0).
Package commons — UUIDv7 generator per spec V3 §4.3 / §9.2 (uuidv7 primary keys).
UUIDv7 is naturally time-ordered (the first 48 bits are a Unix-millisecond timestamp), giving us B-tree-friendly indexes without the locality problems of UUIDv4. The implementation here is a thin wrapper around google/uuid v1.6+ which ships UUIDv7 native.
Index ¶
- Constants
- func MentionsFor(createdBy, assignedTo, operatorHandle, channel string, r IdentityResolver) []string
- func MustUUIDv7() uuid.UUID
- func NewUUIDv7() (uuid.UUID, error)
- func OperatorHandleFromEnv(channel string) string
- func ProjectName() string
- type Action
- type ActionType
- type Attachment
- type Body
- type Branding
- type Capabilities
- type CategoryPref
- type Channel
- type ChannelID
- type Clock
- type CloudEventEnvelope
- type ConversationRef
- type DeliveryEvidence
- type FakeClock
- type IdentityResolver
- type InboundEvent
- type InboundHandler
- type MemoryResolver
- type OutboundMessage
- type Participant
- type PreferenceSet
- type Priority
- type QuietHours
- type RealClock
- type Receipt
- type Recipient
- type Subscriber
- type SubscriberAlias
- type ThreadMessage
- type Timer
- type TraceContext
- type WorkflowPref
Constants ¶
const SystemAgentHandle = "Claude"
SystemAgentHandle is the reserved created_by/assigned_to sentinel for the system agent (Claude). It is NEVER @-tagged in any notification (it is the system, not a human participant). See §1 + §3.
Variables ¶
This section is empty.
Functions ¶
func MentionsFor ¶
func MentionsFor(createdBy, assignedTo, operatorHandle, channel string, r IdentityResolver) []string
MentionsFor returns the canonical handles to @-mention for a workable-item event dispatched to `channel`, implementing the §3 matrix exactly:
mentions = {}
if assigned_to is a human handle AND assigned_to != Operator: mentions += assigned_to
if created_by is a human handle AND created_by != Operator AND created_by != "Claude":
mentions += created_by
# "Claude" is NEVER tagged (it is the system).
# Operator is NEVER tagged (no self-ping).
# de-dup; resolve UsernameFor(handle, channel) — skip if not on that channel.
The returned slice contains the canonical handles (NOT the resolved @usernames) that have a valid alias on `channel`, in a stable order (assigned_to before created_by), de-duplicated. A handle with no alias on `channel` is skipped — you cannot tag someone who is not on that messenger.
operatorHandle is passed explicitly (rather than read from r) so callers that already know the operator for a non-primary channel can override; pass r.OperatorHandle() for the default.
func MustUUIDv7 ¶
MustUUIDv7 panics on the (extremely rare) generator failure path. Use only at process boot or in test fixtures; production code should call NewUUIDv7 and propagate the error.
func NewUUIDv7 ¶
NewUUIDv7 returns a fresh time-ordered UUID.
Use this everywhere Herald generates primary keys (idempotency keys, dead-letter rows, inbound message ids, …) so that index inserts append to the rightmost leaf and stay cache-hot.
func OperatorHandleFromEnv ¶
OperatorHandleFromEnv reads HERALD_<CHANNEL>_OPERATOR_USERNAME for the given channel (e.g. channel="tgram" -> HERALD_TGRAM_OPERATOR_USERNAME) and returns the operator's canonical handle. The channel is upper-cased; the value is returned verbatim (trimmed) — an empty string means "no operator configured for this channel". Per §1 the Telegram operator username is the operator's canonical handle, so callers building the roster typically pass channel "tgram".
func ProjectName ¶
func ProjectName() string
ProjectName resolves the Claude Code session name per the Wave 6 operator-locked decision (2026-05-22):
- HERALD_PROJECT_NAME env var wins when non-empty after TrimSpace.
- Otherwise filepath.Base(os.Getwd()) — the current working directory's basename.
- Otherwise (cwd unreadable) the final fallback is "Herald".
Every pherald subcommand that touches the Claude Code dispatcher MUST call commons.ProjectName() rather than hardcoding "Herald" — pins the session name to the operator's project context.
Types ¶
type Action ¶
type Action struct {
Type ActionType
Label string
URL string // for ActionView / ActionURL / ActionHTTP
Method string // "GET" | "POST" (ActionHTTP)
Body []byte // ActionHTTP
Data string // ActionCallback payload (provider-defined)
}
Action is an interactive UI hint (URL button, callback button, Adaptive Card action, ntfy X-Action, etc.).
type ActionType ¶
type ActionType int
ActionType selects how the channel adapter renders an Action.
const ( ActionView ActionType = iota // open a URL ActionURL // synonym for ActionView; provider-specific styling ActionCallback // round-trip back into Herald via inbound handler ActionHTTP // fire-and-forget HTTP from the recipient device (ntfy) ActionCopy // copy text to clipboard (ntfy) )
type Attachment ¶
type Attachment struct {
Filename string
MIMEType string
SizeBytes int64
Reader func() (io.ReadCloser, error)
CID string // optional inline-image Content-ID (Email)
}
Attachment is a single attached file. Reader is lazy so the adapter streams without buffering the entire payload (spec §11.0).
type Body ¶
type Body struct {
Plain string
Markdown string
HTML string
Native map[string]any // adapter-specific (Slack blocks, Adaptive Card JSON, ...)
}
Body carries one or more rendered representations; the adapter picks the best match for its Capabilities (Slack Block-Kit JSON, HTML email, Markdown for Telegram, etc.).
type Branding ¶
type Branding struct {
AppName string // "Project Herald", "System Herald", ...
BinaryName string // "pherald", "sherald", ...
IconURL string // for rich embeds
AccentColorHex string // "#2C7BE5"
// Wave 2 per-flavor identity fields (design §3.5). Populated by
// DefaultBranding and consumed by the shared commons/cli/ scaffold
// to render --version / --help and to bind the default HTTP port.
Flavor string // single-letter (or short) flavor key: "p", "s", "b", "sc", ...
Prefix string // 3-letter prefix per §8.2 (e.g. "PHR", "SHR")
DisplayName string // human-readable display name (typically == AppName)
DefaultPort int // default HTTP listen port (per-flavor, 70XXX range)
Mission string // one-line mission statement for --help / about
}
Branding is the per-flavor visual identity (spec §6.3 + §11.0).
func DefaultBranding ¶
DefaultBranding returns a sensible Branding for the named flavor. Each <prefix>herald binary calls this at startup and threads the returned Branding through every OutboundMessage; channel adapters use it to render rich-message accents.
Wave 2 r1 (2026-05-21): also populates the per-flavor identity fields (Flavor / Prefix / DisplayName / DefaultPort / Mission) consumed by the shared commons/cli/ scaffold. Per design §3.5 the DefaultPort for serving flavors lives in the 247xx range; CLI-only flavors set DefaultPort=0 to signal "no HTTP serve mode".
type Capabilities ¶
type Capabilities struct {
Text bool
Markdown bool // adapter's native markdown flavor
HTML bool
Attachments bool
AttachmentMaxMiB int
Threads bool // first-class reply threading
InteractiveURL bool // URL buttons / link actions
InteractiveCall bool // callback handlers (advanced tier)
DeliveryCeiling DeliveryEvidence // see §17 / R-05
}
Capabilities advertises what an adapter supports at runtime (spec §11.0). Routers consult Capabilities before dispatching; mismatches are logged and the message is dropped or downgraded per the adapter's choice.
type CategoryPref ¶
CategoryPref is the opt-in / channels for one category (spec §7.2).
type Channel ¶
type Channel interface {
Name() string // "tgram", "slack", ...
Capabilities() Capabilities // declarative feature flags
Send(ctx context.Context, msg OutboundMessage) (Receipt, error)
Subscribe(ctx context.Context, h InboundHandler) error // long-running, called by `serve`
HealthCheck(ctx context.Context) error
}
Channel is the interface every channel adapter implements (spec §11.0).
type ChannelID ¶
type ChannelID string
ChannelID is the canonical channel identifier (spec §11.0). It MUST match the scheme used in `channel_addresses.address_url` and in the URL scheme registered by the adapter.
const ( ChannelTelegram ChannelID = "tgram" ChannelMax ChannelID = "max" ChannelSlack ChannelID = "slack" ChannelDiscord ChannelID = "discord" ChannelTeams ChannelID = "teams" ChannelLark ChannelID = "lark" ChannelWhatsApp ChannelID = "whatsapp" ChannelViber ChannelID = "viber" ChannelEmail ChannelID = "mailto" ChannelNtfy ChannelID = "ntfy" ChannelGotify ChannelID = "gotify" ChannelWebhook ChannelID = "webhook" ChannelDiary ChannelID = "diary" ChannelNull ChannelID = "null" // §11.14 sandbox/no-op adapter for tests )
type Clock ¶
type Clock interface {
Now() time.Time
Since(t time.Time) time.Duration
Sleep(d time.Duration)
After(d time.Duration) <-chan time.Time
NewTimer(d time.Duration) Timer
}
Clock is the time-source abstraction (spec §3.5).
type CloudEventEnvelope ¶
type CloudEventEnvelope struct {
SpecVersion string
ID string // UUIDv7
Source string // URI
Type string // reverse-DNS
Time time.Time
Subject string // tag:/channel:/empty
DataContentType string // e.g. "application/json"
Data []byte // opaque payload
Extensions map[string]string // heraldtenant, heraldidempotencykey, heraldpriority, ...
}
CloudEventEnvelope is Herald's typed projection of a CloudEvents v1.0 payload (spec §4.1 + §11.0). The boundary type is cloudevents.Event from github.com/cloudevents/sdk-go/v2; this struct is the in-process canonical form after parsing / validation.
type ConversationRef ¶
type ConversationRef struct {
Channel ChannelID
ThreadID string // Slack thread_ts; Telegram message_thread_id (forum); Email References[0]
ParentMessageID string // Slack ts; Telegram reply_to_message_id; Email In-Reply-To
RootMessageID string // first message in the thread
}
ConversationRef is the per-channel thread identifier (§12 + §11.0).
type DeliveryEvidence ¶
type DeliveryEvidence int
DeliveryEvidence enumerates the strongest reachable signal per channel (spec §11.0 + §17.4.1).
const ( DeliveryUnknown DeliveryEvidence = iota DeliveryAccepted // hop-by-hop ack (SMTP 250) DeliveryRouted // platform stored & broadcast (Telegram/Slack API ok) DeliveryDelivered // recipient transport confirmed (Email DSN, WA delivered) DeliveryRead // recipient read (Email MDN, Telegram Business read marker) )
func (DeliveryEvidence) String ¶
func (d DeliveryEvidence) String() string
String returns the lowercase enum name (matches the spec wording).
type FakeClock ¶
type FakeClock struct {
// contains filtered or unexported fields
}
FakeClock is the test implementation. Advance(d) moves the clock forward by d and fires any pending After/Timer channels whose deadlines the advance crosses (spec §3.5).
func NewFakeClock ¶
func NewFakeClock() *FakeClock
NewFakeClock returns a FakeClock anchored at a deterministic instant.
func (*FakeClock) Advance ¶
Advance moves the fake clock forward by d, firing any timers whose deadlines the advance crosses.
func (*FakeClock) NewTimer ¶
NewTimer returns a fake Timer that fires when Advance crosses its deadline.
type IdentityResolver ¶
type IdentityResolver interface {
// ResolveSender maps a received message's sender to a canonical handle.
// Unknown senders resolve to their raw "@username" (so first-contact users
// are still attributable).
ResolveSender(channel, channelUserID, username string) (handle string)
// UsernameFor returns the @username for a canonical handle on a target
// channel. ok=false if the participant has no alias on that channel —
// you cannot tag someone who is not on that messenger.
UsernameFor(handle, channel string) (username string, ok bool)
// OperatorHandle returns the canonical operator handle (sourced from
// HERALD_<CHANNEL>_OPERATOR_USERNAME).
OperatorHandle() string
}
IdentityResolver bridges the per-channel runtime world to canonical handles. See §1 contract.
type InboundEvent ¶
type InboundEvent struct {
EventID string // UUIDv7
CloudEvent CloudEventEnvelope // §4.1
Sender Recipient // who sent it
Subscriber *Subscriber // resolved via subscriber_aliases, nil if unknown
Body Body
Attachments []Attachment
Thread *ConversationRef
// ThreadContext carries the PRIOR messages of the thread this event belongs
// to (oldest→newest), populated by the channel adapter when the inbound
// message is itself inside a thread (Slack thread_ts present; Telegram a
// reply_to chain). Empty for a fresh/top-level message. The dispatcher feeds
// this to Claude so a reply is bound by the thread's MEANING and only made
// when the thread's context warrants one — replies are contributions to a
// thread, not isolated answers (operator mandate 2026-06-02). It excludes the
// current inbound message itself (that is in Body).
ThreadContext []ThreadMessage
Raw map[string]any // adapter-specific raw payload (for diary)
}
InboundEvent is a CloudEvent constructed from a subscriber message.
type InboundHandler ¶
type InboundHandler interface {
Handle(ctx context.Context, ev InboundEvent) error
}
InboundHandler receives messages emitted by the adapter's Subscribe loop. Implementations enqueue events back into the router (§5).
type MemoryResolver ¶
type MemoryResolver struct {
// contains filtered or unexported fields
}
MemoryResolver is a concrete in-memory IdentityResolver built from a participant list plus an operator handle. It is the resolver used by tests and by runtime paths that have loaded the participant roster into memory.
func NewMemoryResolver ¶
func NewMemoryResolver(operatorHandle string, participants []Participant) *MemoryResolver
NewMemoryResolver builds a MemoryResolver from the given participant roster and operator handle. The operator handle is messenger-neutral (the operator is a normal Participant whose handle equals the operator env value); pass it from OperatorHandleFromEnv for the primary channel.
func (*MemoryResolver) AddSenderIndex ¶
func (r *MemoryResolver) AddSenderIndex(channel, channelUserID, handle string)
AddSenderIndex registers a (channel, channelUserID) -> canonical handle mapping so a sender identified by chat/user id (not just @username) resolves. channelUserID is the chat/user id (subscriber_aliases.channel_user_id), which is distinct from the @username. Safe to call repeatedly.
func (*MemoryResolver) OperatorHandle ¶
func (r *MemoryResolver) OperatorHandle() string
OperatorHandle implements IdentityResolver.
func (*MemoryResolver) ResolveSender ¶
func (r *MemoryResolver) ResolveSender(channel, channelUserID, username string) string
ResolveSender implements IdentityResolver. It matches by (channel, channelUserID) first, then by (channel, @username). An unknown sender returns the raw "@username" (normalized to a single leading "@") so attribution still works for first-contact users.
func (*MemoryResolver) UsernameFor ¶
func (r *MemoryResolver) UsernameFor(handle, channel string) (string, bool)
UsernameFor implements IdentityResolver. It returns the participant's @username on the target channel; ok=false if the handle is unknown or the participant has no alias on that channel.
type OutboundMessage ¶
type OutboundMessage struct {
EventID string // CloudEvents id (§4)
IdempotencyKey string // explicit; falls back to EventID
TenantID string // UUID; matches `subscribers.tenant_id`
To []Recipient // resolved from preferences + tag fan-out
Subject string // optional (Email, RSS-like channels)
Body Body // rendered per-channel template output
Attachments []Attachment
Thread *ConversationRef // optional; per §12
Priority Priority // ntfy-compatible 1..5
Actions []Action // optional interactive buttons
Branding Branding // per-flavor (§6.3)
Trace TraceContext // OTel propagation
}
OutboundMessage is the value passed to Channel.Send (spec §11.0).
type Participant ¶
type Participant struct {
// Handle is the canonical, messenger-neutral handle stored in items'
// created_by / assigned_to (e.g. "@milos85vasic" or "Claude").
Handle string
// DisplayName is the human-readable name.
DisplayName string
// Kind is "human" | "agent" | "service" (spec §7.5).
Kind string
// Usernames maps a channel name ("tgram", "slack", …) to the participant's
// "@username" on that channel. A participant with no entry for a channel
// cannot be @-tagged there.
Usernames map[string]string
}
Participant is a logical Subscriber/User: one person/agent with potentially a different username on every messenger. See §1.
type PreferenceSet ¶
type PreferenceSet struct {
Categories map[string]CategoryPref // category_id → pref
Workflows map[string]WorkflowPref // CloudEvents type → pref
QuietHours *QuietHours // nil ⇒ no quiet hours configured
ChannelData map[ChannelID]any // provider-specific routing data
}
PreferenceSet is a typed view of the per-subscriber preferences JSON stored in `subscribers.metadata.preferences` (spec §7.2 + §11.0).
type QuietHours ¶
type QuietHours struct {
TZ string // IANA TZ
Start string // "HH:MM" 24h
End string // "HH:MM" 24h
ExemptCategories []string // categories that override quiet hours
}
QuietHours encodes the TZ-aware silence window (spec §7.3).
type RealClock ¶
type RealClock struct{}
RealClock wraps the stdlib time package. Used in production.
type Receipt ¶
type Receipt struct {
Evidence DeliveryEvidence
ChannelMsgID string // Slack ts; Telegram message_id; SMTP queue-id; ...
SentAt time.Time
LatencyMillis int64
Native map[string]any // adapter-specific raw response (for diary)
}
Receipt is what Channel.Send returns on success — the adapter's evidence of acceptance/routing/delivery so the router can decide whether to retry (spec §11.0).
type Recipient ¶
type Recipient struct {
Channel string // "tgram", "slack", "mailto", ...
ChannelUserID string // chat_id, U0xxx, email address, ...
DisplayName string // best-effort, for templating
}
Recipient is a resolved (channel, channel_user_id) pair (spec §11.0).
type Subscriber ¶
type Subscriber struct {
ID uuid.UUID
TenantID uuid.UUID
Handle string // empty if not operator-mapped
DisplayName string
Locale string // BCP-47, e.g. "en-US", "sr-Latn-RS"
Timezone string // IANA, e.g. "Europe/Belgrade"
Roles []string
Metadata map[string]any
Aliases []SubscriberAlias
Preferences *PreferenceSet
}
Subscriber is the in-memory projection of `subscribers` + its linked `subscriber_aliases` (spec §7.1 + §11.0).
type SubscriberAlias ¶
type SubscriberAlias struct {
Channel string
ChannelUserID string
VerifiedAt *time.Time
LastSeenAt *time.Time
}
SubscriberAlias is one row from `subscriber_aliases`.
type ThreadMessage ¶
type ThreadMessage struct {
SenderHandle string // resolved/raw sender handle; "Claude" for this bot's own prior messages
SenderIsBot bool // true when authored by a bot (incl. this Herald bot)
Text string // the message text
Timestamp time.Time // when it was sent (zero if the adapter cannot determine it)
}
ThreadMessage is one prior message in a thread, supplying the conversational context that binds a reply to the thread's meaning (operator mandate 2026-06-02). Adapters populate it from the channel's thread-history API (Slack conversations.replies; Telegram the reply_to chain).
type TraceContext ¶
type TraceContext struct {
TraceID string // 32-hex chars (W3C Trace Context)
SpanID string // 16-hex chars (the parent span at handoff)
TraceFlags byte // sampling flags (W3C)
TraceState string // vendor-specific (W3C tracestate header)
Baggage string // W3C baggage header value
}
TraceContext carries OpenTelemetry trace propagation across Herald boundaries (spec §11.0 + §17.3).
type WorkflowPref ¶
type WorkflowPref struct {
Channels []ChannelID // may be empty (= use Category default)
Muted bool
}
WorkflowPref is the opt-in / channels for one CloudEvents type.
Directories
¶
| Path | Synopsis |
|---|---|
|
Wave 4b Task 3 — Accept-header content negotiation primitive.
|
Wave 4b Task 3 — Accept-header content negotiation primitive. |
|
Package gitops provides the REAL git/repo primitives the Herald §43 flavor command bodies build on.
|
Package gitops provides the REAL git/repo primitives the Herald §43 flavor command bodies build on. |
|
Package stresschaos is Herald's shared, dependency-light scaffold for the §11.4.85 stress + chaos test suite (GAP-3, plan 2026-05-27-stress-chaos-suite).
|
Package stresschaos is Herald's shared, dependency-light scaffold for the §11.4.85 stress + chaos test suite (GAP-3, plan 2026-05-27-stress-chaos-suite). |