Skip to content

Sense 33: The Bell — When the System Notices, the Person Hears

Sense 33 · Steady · Last touched 2026-09-12

  • last_verified: 2026-07-27

Synced from docs/design/sense_33_the_bell.md in the engine repo — that’s the source; this page is a build-time mirror.

The pipeline already notices many things. Today it whispers them into tables nobody reads. The Bell is where the noticing becomes a sound.

Not every noticing wants the same hearing. A wisdom that just confirmed wants a calm pointer in the workshop. A 100%-null column wants a louder note. A new finding may want no sound at all — just a counter. The Bell holds the shape that lets each one ring at the right volume.

Status: implementing (core landed; pretty much done — 2026-05-17) Date: 2026-05-12 Authors: the owner + Claude (morning conversation toward the RMC noon ship) Inherits:

  • Sense 14 — signals fire findings; some findings are noisy
  • Sense 18 — Identity & Passes gates who can dismiss / resolve / act
  • Sense 29 — one of several consumers; the workshop can project rings anchored to artefacts it authors
  • Sense 14 Subject Matter check verdicts — wisdoms with checks already produce confirmed/violated/no_data states Companion impl docs (to come):
  • notification_sources.md — how each of the five feeds gets wired in
  • notification_surfaces.md — the Inbox, JinDesk bell, the Atelier projection, per-page panels

The pipeline already notices a lot. Most of it is buried.

  • Subject Matter wisdoms with checks fire confirmed | violated | no_data on every build. Today they’re stored in smebit_verdicts and surfaced one-at-a-time on Subject Matter pages — not aggregated into “things requiring your attention”.
  • Engine diagnostics (W010, E***, etc.) are baked into _<tenant>.diagnostics. Not surfaced anywhere in the UI today.
  • Silver is_valid = false rows already exist as data-quality flags. Not anchored to a user-facing surface.
  • Source-data gaps (a master export missing, a column 100% null, a signal that can’t fire) are noticed during build but only documented when a human writes a note about them.
  • Notebook actions (Sense 21 / 23) already aggregate to /actions — but only for explicit author-curated items, not for system-noticed concerns.

Five feeds. No common surface. The result: important findings stay buried until somebody happens to land on the right page. When the system notices “your supply-chains table is 100% null on its key column”, the person who could act on it currently has to discover it by browsing.

The recent supply-chains case made this concrete. The structural answer (“SAP IS-H is single-channel — see the wisdom”) is documented, but the human pointer — “hey, this is fishy, please look” — has nowhere to live. Resolutions happen in many places — a note here, a wisdom there, an action cell, a CLI command — but the awareness of “something wants you” needs a single home that every audience can reach, including audiences who never open an authoring tool.


A Notification is a typed record that lets the system speak to the person without forcing the person to come looking.

Notification {
id: <stable hash>
source: wisdom | diagnostic | validity | data_gap | action | build_warning
source_ref: <id of the originating thing — wisdom id, diag code, etc.>
severity: info | heads_up | concern | alarm
polarity: positive | neutral | negative
message: { en, de, fr, ... } # short, human, action-oriented
anchor: { surface, target_id } # where this should also appear inline
suggested_action: { en, de, fr, ... }? # optional, structured for one-click conversion
lifecycle: ephemeral | persistent
created_at: timestamp
resolved_at: timestamp?
resolution: dismissed | snoozed_until | acted_via_<ref> | auto_cleared
}

The Bell is the single aggregator that produces these records and the singleton Inbox that lets them be heard — with secondary surfaces (JinDesk bell, Atelier projection, per-page banners, CLI, future TUI) consuming the same substrate.


Each source publishes notifications during build (or, for some, at runtime). The contract: a source promises that a notification refers to a specific noticing, not a general state — so resolution is meaningful.

SourceWhat it publishesExample
wisdomSmebit checks that flipped to confirmed or violated since last build, where the wisdom declares bell: true”SAP IS-H single-channel confirmed for this tenant — the I/O reconciliation family is N/A”
diagnosticEngine warnings/errors with diagnostic codes (W010, E***); only those marked user-visible”Bake metadata: skipped exposure tile because gold_net_exposure has no leakage rows”
validitySilver is_valid = false clusters above a threshold, grouped by invalid_reason”42 billing events failed validation (invalid_reason: missing case_id)“
data_gapBuild-phase audits noticing structural absences — missing master export, 100%-null key columns, empty bronze tables that should populate”gold_supply_chains is empty because from_location is 100% null”
actionNotebook action cells whose status changed (pending → in_progress, proposed → completed) since last build”a viewer marked the MSEG data request ‘in_progress’”

(A sixth source — runtime-reported errors from JinDesk itself — is plausible but out of scope here. The Bell speaks for build-time noticings; live-app errors belong to a different surface.)

Sources are plugin-shaped: each is a Python module that registers under notification_sources/ and exports produce(con, tenant) → list[Notification]. New sources don’t require Bell core changes — they register, they ring.


The Inbox — a singleton, with many consumers

Section titled “The Inbox — a singleton, with many consumers”

The substrate is one table per tenant: _<tenant>.notifications. The Bell aggregator writes to it; everything else reads from it. The Inbox is the first-class surface over that table — a top-level destination at /[tenant]/inbox, a sibling of Findings / Theses / Observations, reachable without opening any authoring environment.

Three design commitments follow from making the Inbox a singleton:

  1. Audience independence. A Reader with only view:bell should reach the rings without needing the Atelier (which requires authoring capabilities). The Inbox is that route.
  2. Surface diversity. The same substrate is consumed by JinDesk header bell, by the Atelier when projecting onto an authored artefact, by per-page banners, by the CLI, and eventually by a TUI / menu-bar app. No consumer owns the Inbox.
  3. Resolution is shared. Dismissing or snoozing a ring from any surface updates the same record. The history is one history, not one-per-tool.

Lives at /[tenant]/inbox. The primary home for rings.

Properties:

  • Two tabs: Active (unresolved rings — the default landing tab) and History (resolved rings, sorted resolved-most-recently first). History exists for retrospectives, audit (“what did we close last quarter?”), and pattern-finding (“we keep dismissing this — should it stop firing?”). Storage cost is near-zero.
  • Grouped by source. wisdom-rings sit together, data_gap-rings sit together.
  • Filterable by severity, polarity, lifecycle, anchor.
  • Each notification carries its suggested action as a button that, when present, hands off to whatever surface owns the resolution: a new note in the Atelier, a new action cell, an edit-this-wisdom route, a jin <command> recipe. The Inbox launches; it does not author.
  • Resolution updates the notification record: dismissed (with reason), snoozed_until, acted_via (links to the resulting note / commit / action), or auto_cleared (when the next build no longer produces it).
  • Pass-aware (Sense 18). The Inbox respects the role/pass model. view:bell lets a role read the Inbox in read-mode — see rings with explanations and anchors, but no resolution buttons and no suggested-action shortcuts that would open authoring forms. A read-mode user can leave comments on a ring (lightweight markdown thread, one column wider than the ring card) — authors see those comments when they next visit. This is a genuine collaboration primitive that, if it grows, could become its own Sense (the Thread). Resolution itself requires act:bell (or higher).
ConsumerWhat it showsHow it differs from the Inbox
JinDesk status bellCompact icon in the header, count of active concerns, dropdown of the top 3-5 rings with an Open Inbox footerAwareness, not action. Placement: right of the locale switcher, left of the identity menu. Badge dot only for concern (amber) and alarm (red); heads_up is counted but un-painted. Persistent notifications only.
Atelier projectionA filtered panel inside Sense 29 showing rings whose anchors point at artefacts the current Atelier session can author (wisdoms, signals, notes, …)A view, not a home. When the Atelier is closed the rings still exist; when a Reader visits the Inbox they see the same rings without the authoring shell.
Per-page bannersAn inline panel on a specific entity / page driven by an anchor (e.g. the org-units list shows a “this is empty because…” banner driven by a data_gap notification anchored there)Contextual. Renders where the noticing is about, not where rings live in general.
CLIjin bell lists active rings; jin bell resolve <id> / --snooze / --dismiss mutate the same tableFor headless / scripted workflows. Same substrate; no UI dependency.
TUI / menu-bar (future)Persistent system tray awareness across tenantsReads the substrate from every live tenant under the live root. Out of scope here.

The Atelier doesn’t own the Inbox. The Inbox doesn’t depend on the Atelier. They share a substrate.

Each tenant gets its own rings. A pack-level wisdom that fires confirmed on twelve tenants produces twelve rings — one per tenant Inbox — and each tenant’s authors resolve theirs independently. Per-tenant isolation is the default and matches the tenant-blindness invariant: no tenant’s runtime ever sees another tenant’s state.

But pack-level patterns benefit from a pack-level view. “This wisdom fires on these twelve tenants — do we want a bulk authoring action?” That question lives in the packhub clone, not in any tenant’s KLS. A pack-view surface (rendered by the Atelier when opened against the pack repo, or by any other pack-scoped tool) walks all known tenant KLSes and aggregates rings keyed by source-ref. Authors working at the pack level see “this wisdom fires confirmed on 12/15 tenants” and can propose a pack-wide change in one place. Tenant rings remain authoritative; the pack view is a read aggregator.

This pattern is generalisable beyond the Bell — it’s the same shape any pack-level dashboard would want — but the Bell is its first concrete use.

  • Not an alerting system. No emails. No pager. No 3am calls. The Bell is for noticings, not for incidents that require synchronous response. (If we ever want that, it’s a different Sense.)
  • Not a logging system. Build logs continue to live where they live. The Bell only carries items that are meaningfully resolved by a human action.
  • Not an exception catcher. Runtime errors (failed SQL, missing files at request time) belong to operational logs and to the Landscape’s structural-view observability (Sense 42 The Landscape — the Garden), not to the Bell.
  • Not an authoring assistant. The Bell rings on existing noticings; it does not invent new wisdoms or signals. When a pattern recurs across multiple rings (same pattern_id, same source-ref shape, different tenants), the Bell tags those rings so the Atelier’s authoring intelligence (Sense 29 layer 5) can ask “this looks systemic — author a wisdom?”. The Bell creates the hook; the suggestion engine acts on it.

┌─ build ─┐ ┌────────── human ──────────┐
│ │ │ │
source.produce() notifications.aggregate() surfaces.render()
│ ↓ │ │
notification diff with last filter, group, resolve
records (auto_cleared?) rank by severity (dismiss /
snooze /
acted_via)
│ │
└─→ write to _<tenant>.notifications ←───┘
  • At build time: each registered source produces its current notifications. The aggregator diffs against the previous build’s set. Rings that no longer apply get auto_cleared in the KLS (the only resolution kind the aggregator itself writes). New rings get created. Existing rings keep their id, so resolution state in SIS continues to point at them.
  • At read time: surfaces query _<tenant>.notifications LEFT JOIN _<tenant>_sis.bell_resolutions USING (id), filtered by resolutions.resolved_at IS NULL AND (resolutions.snoozed_until IS NULL OR resolutions.snoozed_until <= now()). They group by source, anchor, severity.
  • At resolution: the surface writes to the SIS overlay (_<tenant>_sis.bell_resolutions), never to the KLS. Same write target whether the resolution is a dismiss, a snooze, or an acted_via. The KLS stays read-only after jin make produces it.

Snooze uses calendar time, not build counts. Builds are bursty and meaningless from the user’s perspective: three builds could be three minutes or three weeks. A snooze answers “I don’t want to think about this until next Tuesday”, not “I don’t want to think about this until N builds from now”. Default: +7 days. Per-source override via default_snooze_days in the source registration — some sources are weekly by nature, others want a longer beat.

This trades one property for another. Builds are reproducible; calendar time isn’t. So snooze states are not part of the reproducibility contract — only the produced rings themselves are. Resolution states are operational state, layered on top of the KLS, not baked into the analytical truth.

Abstain — silence is not the same as an all-clear

Section titled “Abstain — silence is not the same as an all-clear”

Amended 2026-09-12, after an incremental build cleared two real rings.

The aggregator’s auto-clear rests on one inference: the source ran, looked, and found nothing. That inference is only sound when the source could actually look.

It often can’t. A lean KLS drops bronze and silver at finalization, so the next incremental build presents the validity source with no silver_* tables at all — not clean data, no data. Under the original contract the source returned an empty list, indistinguishable from “every silver table is valid”, and the aggregator cleared rings describing thousands of genuinely invalid rows. The same hole swallowed any source that crashed or failed to import: its rings were cleared because nothing came back from something that never ran.

A probe that reads a surface where the failure cannot appear will always report health. The fix is to let a source say I could not evaluate:

from jinflow.notifications.engine import SourceAbstained
def produce(con, tenant_id):
tables = _eligible_tables(con, tenant_id)
if not tables:
raise SourceAbstained("no silver_* tables carry is_valid — lean or unbuilt KLS")
...

The contract. A source’s produce() has three outcomes, not two:

OutcomeMeansEffect on that source’s existing rings
returns ringsevaluated; these holdre-fired (content refreshed)
returns []evaluated; nothing holdsauto-cleared
raises SourceAbstainedcould not evaluateleft exactly as they are

Partial abstention. A source that loops over many inputs — audits, silver tables — can establish part of its surface and fail on the rest. It passes what it did establish along:

raise SourceAbstained(f"audit '{aid}' did not run", rings=out)

Those rings are written as normal; only the clearing is withheld. One unreadable table must not suppress the rings the readable ones just earned, and must not clear anything either.

Two failures are folded into abstain, because neither supports the all-clear inference either: a plugin that raises any other exception, and a plugin that failed to load at all. The rule the aggregator applies is therefore positive rather than negative — a ring is only auto-cleared when its own source ran to completion this build — which covers the load-failure case that no in-plugin signal could reach.

For that rule to work, the aggregator has to know whose rings just fell silent, and a source producing nothing tells it nothing. So a source module declares the name its rings carry:

SOURCE = "validity" # defaults to the module name

Get this wrong in the silent direction and rings can never clear; the aggregator warns when a produced ring’s source disagrees with the module’s declaration.

Deliberate disabling is not abstaining. A source turned off via bell.sources.<name>.enabled: false produces nothing and its rings do clear; switching a source off is a statement about what should ring, not a gap in evidence. Re-enabling re-fires them under the same stable ids.

Abstention is reported, never silent. aggregate() returns an abstained count and jin make prints which sources abstained and why. A build that clears nothing because it could see nothing must say so — otherwise the fix reintroduces the original failure one level up.

Re-fired rings refresh their content. A re-fire used to bump last_seen_at and nothing else, so a ring’s message, severity and anchor froze at whatever the first build wrote — an action cell could be rewritten and the Inbox would keep quoting the old text forever. The re-fire now writes the produced ring’s fields over the stored row. The id and created_at stay put: it is the same ring, saying what it says today. Resolution state is untouched by the refresh (human resolutions still stand; only a prior auto_cleared is lifted).

The persistence boundary — rings in KLS, resolutions in SIS

Section titled “The persistence boundary — rings in KLS, resolutions in SIS”

The Bell straddles two stores, on purpose:

  • Rings live in the KLS. _<tenant>.notifications is baked by the aggregator at the end of every jin make. Rings are derived from the AFS (source plugins read engine + tenant artifacts and produce them deterministically), so they belong with the rest of the analytical truth. Reproducible from AFS, immutable for the lifetime of the build.

  • Resolutions live in the SIS. When a human dismisses, snoozes, or acts on a ring, that decision writes to a sibling table in _<tenant>_sis.duckdb. JinDesk reads KLS.notifications LEFT JOIN SIS.bell_resolutions at request time. Resolutions persist across rebuilds — SIS is its own DuckDB file and jin make never touches it on the write side.

    2026-05-17 update. This section’s claim that resolutions “are not flushed to AFS” turned out to be wrong as the design evolved. The current truth: durable resolutions (dismissed, acted_via, comments) ARE flushed to afs/bell/*.yaml and live on a dedicated bell orphan branch alongside notes / bookmarks / recents. Snoozes stay SIS-only. The schema split into bell_resolutions (durable) + bell_snoozes (operational) shipped as schema v5. See /architecture/the_bell/ for the current design. This section is preserved as the original framing.

    Resolutions are operational state, not analytical content: they describe how a particular human treated a particular ring on a particular deployment. They are not flushed to AFS at Phase 1d (the SIS → AFS publish step that handles note edits and bookmark coordinates). That exclusion is deliberate and permanent — make never reads bell resolutions and never should. If it ever does, that’s a bug.

    Schema sits alongside notes / bookmarks / preferences in SIS:

    _<tenant>_sis.bell_resolutions {
    id VARCHAR PRIMARY KEY -- matches notifications.id
    resolved_at VARCHAR -- ISO 8601 UTC, NULL when only snoozed
    resolution VARCHAR -- dismissed | snoozed | acted_via | (NULL)
    resolution_detail VARCHAR -- free-text or JSON
    snoozed_until VARCHAR -- ISO 8601 UTC, NULL when not snoozed
    actor VARCHAR -- who resolved it (identity from Sense 18)
    resolved_via VARCHAR -- ring-card / cli / atelier / per-page banner
    comment_thread_json VARCHAR -- lightweight markdown thread for read-mode collaboration
    }

    The corresponding columns on _<tenant>.notifications (resolved_at, resolution, resolution_detail, snoozed_until) stay in the schema for now but get populated only by auto_cleared — the system’s own way of saying “the source plugin stopped producing this ring in this build.” Human-driven resolutions never touch the KLS. (A future cleanup may drop those columns from the KLS schema once the SIS overlay is the only write path.)

Same rule as notes and bookmarks: there is one SIS per tenant, wherever it lives. In Local mode it’s on the developer’s machine; in Proxy / Cloud / Semi-Cloud / MaaS modes it’s in the cloud. The JinDesk reads + writes whichever SIS its deployment points at.

Local jin make’s Phase 1d already knows how to reach a remote SIS for notes and bookmarks (today partially designed, partially wired). Bell resolutions inherit nothing extra: Phase 1d never reads them, so the transport story for resolutions is “the SIS stays where it is, JinDesk keeps reading it.” No new machinery needed.

Cross-reference. The SIS-as-overlay pattern is re-used by Sense 46 for a different substrate — see Sense 46 §Browser-side writes and the SIS overlay. That case is easier because Clerk (not SIS) is the source of truth for principals, so no ship-back is required — the pattern still works, but the round-trip closes through Clerk instead of through Phase 1d.

This separation is what makes the Bell honest across operating modes.

  • P2P2P: KLS is read-only on the owner’s machine. JinDesk serves remote users; their resolutions write to the cloud SIS, not back to the owner’s KLS. The owner’s jin make continues to produce reproducible rings; resolutions accumulate independently.
  • Cloud (R2): KLS lives in R2 (built locally, uploaded). SIS lives in the cloud beside JinDesk. Resolutions never touch R2. The local make path stays pure.
  • Make-as-a-Service: cloud build, cloud SIS. Same exclusion.

Taxonomy — severity, polarity, lifecycle

Section titled “Taxonomy — severity, polarity, lifecycle”

Three orthogonal axes keep the Bell from collapsing into one giant unsorted bag.

Severity — how loud:

  • info: “you might want to know.” No badge count.
  • heads_up: “this is worth a glance.” Counted, low-color badge.
  • concern: “this needs attention soon.” Counted, amber badge.
  • alarm: “this is blocking or wrong.” Counted, red badge. Rare.

Polarity — what kind of news:

  • positive: a wisdom check just confirmed in a good way; a thesis advanced from plausible to confirmed; a data-quality fix flipped a flag from violated back to confirmed. Rare today; common once the loop tightens.
  • neutral: a state of the system the human should be aware of but doesn’t change anything.
  • negative: the usual case — something is wrong or missing.

Lifecycle — how it dies:

  • ephemeral: cleared automatically by the next successful build (e.g. a build warning that doesn’t recur). Surfaces show them, but they don’t accumulate.
  • persistent: stays until explicitly resolved or until the underlying noticing is gone for N consecutive builds (configurable; default 3).

Anchor — where it lives:

  • Every notification has an anchor: { surface, target_id } pointing at the page / entity / model that it’s about. Surfaces use this to render inline panels. Notifications without a meaningful anchor anchor to tenant:* and show only in the Inbox (and any consumer that doesn’t filter by anchor).

Equally important. Anti-patterns to avoid:

  • Every signal finding. Findings are the analytical surface; they have their own pages, their own lenses. The Bell carries only change-in-state signals (a wisdom flipped, a data-gap appeared, an action moved) — not the millions of normal findings produced by the analytical pipeline.
  • Aesthetic preferences. “This tenant hides the UoM Candidates list” is a config.yml choice, not a Bell ring. The Bell is for things the system noticed, not for things the user configured.
  • Internal pipeline state. dbt run times, query plans, cache hits. Not the Bell’s business.
  • Sales metrics. Engagement, click-throughs, time-on-page. Not the Bell’s business; if such a thing ever exists it has its own surface.

The discipline: every notification answers “what specifically should a human do or decide about this?” If the answer is “nothing”, the ring shouldn’t exist.


The Bell is the pub/sub channel between writers (build, Atelier, evolve) and readers (JinDesk’s Inbox surface). Rings persist in the KLS (so every build carries its build-time notifications); per-Principal resolutions persist in the SIS (so dismissals are per-viewer, not global). Every commit through the Scribe boundary can raise a ring; the Inbox renders them grouped by severity, with snooze + dismiss + history.

  • In the app: Inbox (the live ring surface, grouped by severity and lifecycle stage)
  • CLI: jin bell (subcommands for inspecting + flushing the pub/sub channel)
  • Persistence: rings → KLS table (build-time, deterministic) · resolutions → SIS table (per-viewer state)

Status: shipped. The Inbox surface is the first-class consumer; the Beat 2 path of the Heartbeat (Sense 21) routes through the Bell on every Atelier save.


The first six open questions raised during this Sense’s drafting are settled in-line above (placement, pass-aware inbox, cross-tenant aggregation, calendar-time snooze, history tab, pattern-id hooks). They moved from this section into the body where they belong.

What remains genuinely open — landing in companion docs as the architecture meets implementation:

  • Configurability. Which dimensions are per-tenant config (config.yml) vs pack defaults? Initial cuts: per-source enable/disable per tenant, severity thresholds, default_snooze_days per source. Likely needs a config sub-tree under bell: in config.yml.
  • Aggregator placement. Does aggregation happen as a post-build phase in jin make, or as a separate jin pulse step (Sense 21 / 32)? Lean: in-make for now, factor out only if the Bell starts producing latency.
  • Suggested-action vocabulary. The set of structured actions a notification can carry (open new action cell, open new wisdom, open new note, open data-gap audit query, …). Each needs an Atelier-side handler. Inventory to be drawn before Phase 3.
  • Notification ID stability. A ring’s id should survive cosmetic source changes (a wisdom retitled, an entity renamed) so resolution history stays intact. Likely hash(source, source_ref, anchor) — to be specified.

SurfaceTodayAfter the Bell
Subject Matter detailShows the wisdom’s own check statusUnchanged — Bell aggregates rather than displacing
/actions pageShows note-action cellsContinues; Bell consumes the same source and surfaces the same items
/findings pageShows signal findingsUntouched. The Bell does not summarise findings.
Engine diagnosticsConsole output onlySelected codes become Bell notifications when user_visible: true
Inbox (/[tenant]/inbox)Doesn’t existFirst-class Bell surface — top-level nav, reachable by every audience
Atelier (Sense 29)Doesn’t existOptional projection panel showing rings anchored to authorable artefacts
JinDesk headerLocale switcher, identity, searchPlus a bell icon with severity-colored badge, dropdown linking to the Inbox

The Bell is additive. Nothing existing breaks; existing surfaces continue to serve their narrow purpose; the Bell is the cross-cutting awareness layer.


Three reasons the Bell becomes load-bearing in May 2026:

  1. RMC noon ship surfaced three different “empty” patterns, each with its own resolution path. Without the Bell each one would have its own ad-hoc surface or stay buried. The Bell is the structural answer to the question “how does JinDesk know what doesn’t apply” — which is the question that drove this Sense.
  2. The Atelier (Sense 29) is being designed in parallel, and it’s one of several consumers that will project the Inbox onto authorable artefacts. Defining the Bell as a singleton substrate now keeps the Atelier from accidentally owning a primitive that has to serve many audiences — including Readers who never open an authoring tool.
  3. Multi-tenant has tipped past the threshold where a person can keep “what’s interesting on tenant X” in their head. Five tenants is fine; fifty is a different game. The Bell is the surface that scales attention.

Implementation phases (sketch, for the companion docs)

Section titled “Implementation phases (sketch, for the companion docs)”

Not part of this Sense, but the rough sequencing:

  1. Phase 0 — table. Define _<tenant>.notifications schema and the aggregator skeleton. No sources wired yet.
  2. Phase 1 — first wire. Smebit verdicts → notifications. Renders in a stub Inbox at /[tenant]/inbox.
  3. Phase 2 — second wire. Data-gap detection (the supply-chains-shaped finding) → notifications. The first real “the system noticed something fishy” use case.
  4. Phase 3 — the Inbox surface. Real route with grouping, filtering, suggested-action handoff, read-mode for view:bell, resolution for act:bell. Top-level nav entry.
  5. Phase 4 — JinDesk bell. Header icon with badge, opens the Inbox.
  6. Phase 5 — Atelier projection. Filtered panel inside Sense 29 showing rings anchored to authorable artefacts, with one-click handoff to the relevant authoring form.
  7. Phase 6 — remaining sources. Diagnostics, validity, actions wire in one by one.
  8. Phase 7 — CLI / TUI. jin bell and downstream headless consumers.

Each phase ships independently. The architecture is the contract; the wiring is the work.


“The Bell” was chosen for:

  • Compact. One syllable. Pairs naturally with verbs (“ring the Bell”, “the Bell rang”, “an unrung notice”).
  • Workshop register. Pairs with the Atelier (a workshop bell, not a phone notification).
  • Tone-honest. A bell is heard, not pushed. The Bell rings; the person decides whether and when to answer.
  • Not overloaded. “Inbox” carries email baggage; “Watch” is ambiguous; “Concern” is too negative for the positive-polarity case.

Alternatives considered: The Concern, The Watch, The Bulletin. Each had a corner where it fit better than the Bell — and a bigger corner where it fit worse. (The Inbox is the name of the primary surface, not of the primitive. The Bell is what rings; the Inbox is where you read the rings — same separation as a notification system and its mailbox.)


Numerical neighbors:Sense 32: The Chores — The Pulse That Doesn’t Beat Itself · Sense 34: The Breeze — Lateral Signalling Through the Jinflow Void

jazzisnow jinflow is a jazzisnow product
v0.64.7 · built 2026-09-20 19:48 UTC