.md

Sense

How a pack announces that the world changed and how the runtime delivers it, under Policy and a separate grant, with no power to mint or act.

On this page

Sense is the outbound loop of HCP: Execute says "do this", and Sense says "this happened". A pack declares triggers, the runtime delivers them into a sink while the sense.enable grant is on, and an Invoker decides what happens next, which may include running Execute under its own grants. Pack authors start at Triggers, runtime builders at Sense delivery.

world changes ──► pack announces `new_item` ──► runtime sink records
                                                     │
                                               (optional wake)
                                                     ▼
                                       Invoker runs Execute: list → get → gated write

Isolation#

Sense is emit-only. An event that could execute could also spend, so the path from observation to action passes through every gate a typed command would.

Sense mustSense must not
Share Policy, Binding, and land with Execute on the same packMint credentials or bypass Binding
Fail closed on an unknown ref or when sense.enable is offTreat polling as Sense
Be optional on most packsReplace pull commands (status, follow)
Wake Execute only after the sink has recordedBecome a second pack catalog

Triggers#

A trigger is a world change your pack may announce. Declare one only when the upstream system really pushes, because what an Invoker has to pull is Execute.

AskIf yes
Does the upstream system push this to you (webhook, event stream, vendor notification)?Trigger
Do you have to ask for it with status, follow, show or list?Read command. Not a trigger
Is it your runtime's uptime check or a UI refresh timer?Not a pack trigger at all
Did the pack's own credential go stale?Trigger: binding.degraded

Agents may poll observation commands, and that polling is never Sense. If the source cannot push yet, ship pull commands and add the trigger once it can, without overloading a command such as plan to fake one.

Declare#

Declare each trigger in two places that agree:

surface.json (fragment)
"stimulusTriggers": [
  { "ref": "new_item" },
  { "ref": "binding.degraded" }
]
policy.json (fragment)
"stimuli": {
  "new_item":         { "operation": "read", "privacy": { "land": "personal" } },
  "binding.degraded": { "operation": "read", "privacy": { "land": "personal" } }
}

Lint fails on a ref without a row or a row without a ref. Refs are lower case, dot-separated and name the fact: new_item, never please_list. Land follows the data, so a personal mailbox event is personal, a shared project stream brain and a watched workspace directory local. The sink records each event under its Principal, Binding and land, so a brain trigger still needs project membership to be surfaced.

Kinds#

Sense has one loop and four kinds. Mail, calendar, chat and tickets are all channel events on that loop, and only the Binding family and the payload differ.

KindRefsThen
Channel eventnew_item (a new item on the linked stream), item.updated (an existing item changed)Execute list / get; optional gated write
Binding healthbinding.degraded, with a stateA human runs connect. doctor reports; it does not mint
Domain observationrun.completed (an upstream job finished), a settlement postedshow / follow, or a gated write
Emit-onlyAny ref on a sense_only packNo user command required

Design each trigger together with its reaction, which is Execute under its own Policy row and grant:

new_item        ──►  sink  ──►  Invoker: mail.get <id>  ──►  (maybe) mail.send --confirm
binding.degraded ──► sink  ──►  human: mail connect
run.completed   ──►  sink  ──►  Invoker: show <run>

Composition#

A linked channel carries Execute (list, get, send) and Sense (new_item, binding.degraded) on one pack id, and a public read pack is Execute only until a real push exists. An emit-only pack is a pure ingress source that declares kind: "sense_only", at least one trigger and no verbs, with a connect only if the upstream needs a registration call. Control is Execute on a second pack, because operator health is not a user inbox.

A pack that declares Sense adds two lines to its proof script:

one announce with sense.enable on   → sink records
one announce with sense.enable off  → dropped, Execute unchanged

Sense delivery#

The runtime activates Sense per pack, independently of Execute, and turning it off never touches the CLI wrap.

ActivationRouteEffect on Execute
oninbox (surface to the user) or silent (record only)None
offdrop: acknowledge the producer, do not deliverNone

Route selection is runtime code, and the reference shape is:

TypeScript
function resolveSenseRoute(activation: 'on' | 'off', ref: string): 'inbox' | 'silent' | 'drop' {
  if (activation === 'off') return 'drop'
  return isUserFacing(ref) ? 'inbox' : 'silent'
}

Delivery requires sense.enable for the Principal and pack. Without the grant the runtime drops the trigger, an unknown ref is dropped and reported, and with both checks passed the runtime records under Principal + Binding + land:

ingress → validate ref is declared → check sense.enable → resolve route → sink

A sink is wherever recorded triggers go: an inbox table, a queue, a webhook fan-out or a UI badge counter. Sinks are runtime machinery with no HCP type. The protocol requires only that each record is attributable (Principal, Binding, land, pack, ref), that recording never mints, spends or runs a command, and that an Invoker reading the sink later runs Execute under Execute's own grants. A runtime must not shortcut that last step, since Sense recording and Execute authorisation are separate code paths with separate grants.

How a trigger reaches the runtime has no HCP type either. Choose a mechanism per pack family and document it in your runtime's own docs, because the pack does not carry it.

MechanismExample
Signed ingressHMAC-verified webhook from the upstream provider
Vendor push mappingProvider change notification → pack ref
QueueInternal event bus
Pack processA sense_only pack emitting on stdout

The runtime checklist lists what conformance needs from delivery, including binding.degraded from the hub.

What Sense is not#

  • A product ledger, database schema, or inbox UI
  • A requirement that every pack emit events
  • A way around Binding
  • Polling with a nicer name, such as an agent polling status
  • Uptime monitors, UI refresh timers or control-plane ticks, which are runtime machinery and never go through the sink as pack triggers