Broadcast Alerts (cross-session notices with expiry + priority)

mandatory

O sistema único de **alertas de broadcast** da Koder Stack: arquivos em `meta/context/notices/active/*.md` que o hook `notices-check.sh` (`UserPromptSubmit`) injeta no contexto de **toda sessão**, ANTES de qualquer ação, ordenados por prioridade, cada um com **expiração explícita** (auto-some) e/ou **disparo agendado** (`fires_on`, aparece só a partir de uma data futura — substitui `/schedule` para obrigações datadas). Unifica os antigos `notices` (locks/cortesias técnicas cross-session) e `reminders.md` (lembretes do owner com data) num só formato + um só hook. Substitui o TTL implícito por severity+idade (onde `high` = nunca expirava) por um `expires:` explícito.

Por quê

Toda sessão do Claude Code precisa ler, antes de qualquer coisa, os avisos importantes que outras sessões/o owner deixaram: locks de componente, cortesias, lembretes datados, obrigações futuras (ex.: "remover compat-shims em 2026-06-20"). O hook notices-check.sh já injeta notices/active/ no UserPromptSubmit de toda sessão — esta spec formaliza o schema (com expiração + prioridade + disparo agendado) e o comportamento do hook, e unifica o antigo reminders.md aqui.

R1 — Local + formato

  • Um alerta = um arquivo meta/context/notices/active/<slug>.md com frontmatter YAML

    + corpo Markdown. Arquivado = movido para meta/context/notices/archive/.

  • O hook lê active/. archive/ é histórico (nunca injetado).

R2 — Schema (frontmatter)

Campo Obrig. Default Significado
title sim manchete curta (1 linha)
created sim data de autoria YYYY-MM-DD (alias retro: date)
priority não 5 09; maior = aparece primeiro
expires não YYYY-MM-DD; não exibe a partir desta data + auto-arquiva (TTL)
fires_on não YYYY-MM-DD; não exibe ANTES desta data (disparo agendado)
status não active active | done (done não exibe)
kind não alert lock | courtesy | reminder | alert | bug-dep
audience não all all (técnico, toda sessão) | owner (pessoal)
recurring não none none | daily (re-arma manual: ao disparar, bumpar fires_on +1 até done)
severity não normal retrocompat: se NÃO houver expires, o TTL cai no esquema antigo idade+severidade (high∞, normal7d, low=3d a partir de created)
component não componente alvo (locks)
session_id não (locks) UUID da sessão Claude Code que colocou o lock ($CLAUDE_CODE_SESSION_ID); permite a uma sessão bloqueada nomear quem detém o lock. Ausente em locks legados.
applies_when não (R3.10) expressão Kanon (domínio notices) sobre o foco da sessão; ausente ⇒ universal

kind: bug-dep (dependência de bug, component-bug-discovery.kmd): emitido por uma sessão que, usando uma instância instalada de um componente Koder, achou um bug nele e depende da correção para prosseguir. O corpo declara o ticket do bug (<COMPONENT>-NNN) e o componente. Sinal-back: quem corrige o bug flipa este alerta para status: done ao fechar o ticket — é o sinal que re-aciona a sessão descobridora (que então atualiza a instância e retoma). Pareia com priority alta (7–8) + component: + o blocked-by: no ticket da descobridora.

expires vs fires_on: expires = "vale ATÉ" (some depois). fires_on = "aparece A PARTIR DE" (some antes). Os dois podem coexistir: uma obrigação que só importa numa janela. Um lock típico usa expires; uma obrigação datada (ex.: remover shims) usa fires_on (+ opcional expires alguns dias depois).

R3 — Comportamento do hook (notices-check.sh, UserPromptSubmit)

Para cada arquivo em active/, nesta ordem:

  • R3.1 status: done → não exibe (candidato a arquivar).
  • R3.2 fires_on definido e hoje < fires_onnão exibe (ainda não disparou).
  • R3.3 expires definido e hoje >= expiresnão exibe + auto-arquiva

    (mv idempotente p/ archive/, falha silenciosa em corrida entre sessões).

  • R3.4 sem expires → retrocompat: idade = hoje - created; TTL por severity

    (high999d, normal7d, low=3d); se idade > TTL → não exibe + auto-arquiva.

  • R3.5 senão → exibe, com countdown: expires⏳ vence em Nd /

    ⚠️ VENCE HOJE; obrigação fires_on recém-disparada/atrasada ou recurring vencido → 🔔 [VENCIDO há Nd] / 🔔 HOJE / 🔔 em Nd.

  • R3.6 Ordenar os exibidos por priority desc, desempate por urgência

    (menor dias-até-expirar / mais atrasado primeiro).

  • R3.7 Emitir um bloco único [alerts] no additionalContext (JSON válido).

    Saída sempre JSON bem-formado; qualquer erro de parse de um arquivo → pular esse arquivo, nunca abortar o hook (não pode quebrar nenhuma sessão).

  • R3.8 — disciplina de renderização (corpo sob demanda). O bloco é injetado

    em todo turno (custo de token por-turno); por isso o corpo (primeiras 6 linhas) só é incluído para itens que exigem ação a cada turno: kind: lock, kind: bug-dep, e obrigações fires_on já disparadas. Todo o resto (courtesy, alert informativo, reminder) é exibido como uma linha (título + countdown); o corpo vive no arquivo, lido sob demanda (Read) quando o título for relevante à tarefa. Reduziu o bloco de 3.964→1.051 tok/turno (~73%) sem perda de acionabilidade (locksbug-depsobrigações mantêm corpo). A relevância por-sessão (mostrar um notice só a sessões que casam um predicado) é trabalho separado (applies_when: avaliado pelo motor Kanon — ver rfcs/stack-RFC-015), não esta regra de apresentação.

  • R3.9 — audience: owner fora do bloco técnico. Notices audience: owner

    (pessoaljurídicoadmin do owner) não entram no bloco lido por sessões de código — são ruído para trabalho técnico. Continuam acessíveis via /k-reminders (que lê os arquivos direto, não a saída do hook). Default audience: all → só pula quem é explicitamente owner.

  • R3.10 — relevância por-sessão via Kanon applies_when (motor, não bash). Um

    notice pode declarar applies_when: (expressão Kanon do domínio notices — ver specs/kanon/expressions.kmd §4, fatos component/repo do foco da sessão). O hook resolve a relevância chamando koder-spec-audit notices --component <foco> (o motor — nunca reavalia Kanon em bash, RFC-015 R4): exibe os universais (sem applies_when) + os que casam o foco. Opt-in (ausência = universal) e fail-open por contrato: motor ausenteerrotimeout, foco desconhecido, ou regra inválida ⇒ mostra (nunca esconde um lock). Notices não precisam declarar kanon: (default = versão corrente do motor); um kanon: não suportado fail-opens. É o análogo por-turno do applicable (seleção computada de specs).

R4 — Precedência de leitura (CLAUDE.md)

Toda sessão DEVE tratar o bloco [alerts] como leitura prioritária antes de agir: respeitar locks, honrar obrigações fires_on disparadas, considerar cortesias.

R5 — Migração

  • Os itens de meta/context/reminders.md viram arquivos `notices/active