Skip to content

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

notices specs/notices/broadcast-alerts.kmd

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.

When this spec applies

All triggers

Specification body

Broadcast Alerts

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)

CampoObrig.DefaultSignificado
titlesimmanchete curta (1 linha)
createdsimdata de autoria YYYY-MM-DD (alias retro: date)
prioritynão509; maior = aparece primeiro
expiresnãoYYYY-MM-DD; não exibe a partir desta data + auto-arquiva (TTL)
fires_onnãoYYYY-MM-DD; não exibe ANTES desta data (disparo agendado)
statusnãoactiveactive | done (done não exibe)
kindnãoalertlock | courtesy | reminder | alert | bug-dep
audiencenãoallall (técnico, toda sessão) | owner (pessoal)
recurringnãononenone | daily (re-arma manual: ao disparar, bumpar fires_on +1 até done)
severitynãonormalretrocompat: se NÃO houver expires, o TTL cai no esquema antigo idade+severidade (high=∞, normal=7d, low=3d a partir de created)
componentnãocomponente alvo (locks)
session_idsim (locks novos)(locks) UUID da sessão Claude Code que colocou o lock ($CLAUDE_CODE_SESSION_ID); permite a uma sessão bloqueada identificar quem detém o lock (mapeável a .session-state/<uuid>.json). koder-lock grava automaticamente. Locks à mão / fallback DEVEM emiti-lo também — nunca um PID (session: 724929 é anti-padrão: morre e não mapeia). Legados sem ele são grandfathered (warn, não erro). Enforçado por koder-spec-audit locks (R3.11).
session_namenão(locks) dica humana legível do dono — nome da worktree/k.claude <nome> quando houver, senão a conta ($CLAUDE_ACCOUNT_NAME). Opcional (não há fonte por-sessão universal); o reason/title do lock já dá o "o quê". O session_id é a identidade canônica.
applies_whennã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 (high=999d, normal=7d, 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 (locks/bug-deps/obrigaçõ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 (pessoal/jurídico/admin 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 ausente/erro/timeout, 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).

  • R3.11 — todo lock novo carrega session_id (dono identificável). Um lock existe pra uma sessão bloqueada responder "quem detém isto, e ainda está vivo?". Isso só funciona se o lock traz um session_id estável (UUID $CLAUDE_CODE_SESSION_ID, mapeável a .session-state/<uuid>.json). O koder-lock place grava automaticamente; toda outra trilha de escrita (fallback prosa do tasks/concurrent-lock.md, skills, à mão) DEVE emiti-lo — nunca um PID no campo ad-hoc session: (session: 724929 morre e não mapeia; anti-padrão real, id#261 2026-07-02). Enforçado por koder-spec-audit locks (advisory por default; --strict = exit 1 pra gate CI): classifica cada lock ativo em ok (session_id UUID) / no-session-id (campo session: ad-hoc) / ad-hoc-session (session_id não-UUID) / anonymous (nada). Legados sem session_id são grandfathered (warn, não erro) até serem re-colocados via koder-lock.

  • R3.12 — kind: courtesy aflora 1× por sessão. Cortesia é informacional (não-acionável); mostrá-la a cada turno é o maior custo de token por-turno remanescente depois que R3.8 cortou o corpo (sobra a linha). O hook mostra a cortesia na primeira vez que ela aparece na sessão e suprime nas seguintes, via um marcador por-sessão ($KODER_ALERTS_STATE/<session_id>, default ~/.claude/state/alerts-courtesy-seen/, criado só quando ao menos uma cortesia foi de fato mostrada). kind: courtesy (match exato) é suprimido — lock, bug-dep, obrigações fires_on, reminder e alert nunca. FAIL-OPEN por contrato: sem session_idnunca suprime (mostra tudo, comportamento pré-R3.12); um bug de supressão jamais pode esconder um lock. Uma sessão nova re-mostra a cortesia (marcador é por-session_id). Motivo: /k-token-audit 2026-07-07 (meta/context#788 Parte B).

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/*.md com kind: reminder, audience: owner, fires_on: <data>, recurring: daily (onde aplicável). reminders.md + reminders-check.sh são aposentados.
  • /k-reminders lista os alertas kind: reminder do novo sistema.

Testes

  • T1 — arquivo com expires no passado → não injetado + arquivado.
  • T2 — arquivo com fires_on no futuro → não injetado; no/após a data → injetado.
  • T3 — dois arquivos com priority distinta → o de maior priority aparece primeiro.
  • T4status: done → nunca injetado.
  • T5 — retrocompat: arquivo sem expires, severity: low, created há 4 dias → não injetado.
  • T6 — arquivo com frontmatter malformado → pulado; hook ainda emite JSON válido p/ os demais.
  • T7recurring: daily vencido → injetado com tag [VENCIDO há Nd].
  • T8kind: bug-dep ativo → injetado normalmente; ao virar status: done (sinal-back do fixer) → não mais injetado (candidato a arquivar).
  • T9kind: courtesy → injetado como uma linha (sem corpo); kind: lock e kind: bug-dep → injetados com corpo (6 linhas); obrigação fires_on disparada → com corpo (R3.8).
  • T10audience: ownernão injetado no bloco; audience: all (ou ausente) → injetado normalmente (R3.9).
  • T11applies_when: component == "koda": foco=koda → injetado; foco=kdb → não injetado; sem foco / motor ausente / regra inválida → injetado (fail-open). Avaliado por koder-spec-audit notices (teste TestNoticeRelevance), não por bash (R3.10).
  • T12kind: courtesy + session_id fixo: turno 1 → injetado; turno 2 da mesma sessão → não injetado; um kind: lock no mesmo dir → injetado nos dois turnos (invariante de segurança); sessão nova → cortesia re-injetada; sem session_id → cortesia injetada todo turno (fail-open). Verificado por meta/tests/regression/020-notices-courtesy-once-per-session.test.sh (R3.12).