Pular para o conteúdo

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.

Quando esta spec se aplica

Todos os triggers

Corpo da especificação

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).