Commit Graph

6358 Commits

Author SHA1 Message Date
Rodribm10
fc0105785b
feat: allow FAQ management via knowledge-base role
Some checks failed
Build and Push to GHCR (multi-arch) / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / merge (push) Has been cancelled
Allow Captain FAQ management through the existing knowledge_base_manage custom role while keeping plain agents read-only for FAQ actions.
2026-06-10 13:45:20 -03:00
Rodribm10
cbbfccaf42 fix(captain): resolve Hermes quoted replies by internal id
Some checks failed
Build and Push to GHCR (multi-arch) / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / merge (push) Has been cancelled
2026-05-24 17:22:05 -03:00
Rodribm10
572b9ccd10 fix(captain): send WhatsApp reply context to Hermes 2026-05-24 16:51:28 -03:00
Rodribm10
358114d04d fix(captain): respect report date filters 2026-05-17 14:00:22 -03:00
Rodribm10
e94cadbdf6 feat(captain): pix_mode manual_static pra Padova e Express
Adiciona caminho paralelo de PIX manual estático pra unidades sem
integração Inter (Padova, Express AL). Mudança 100% aditiva — todas as
outras unidades continuam no fluxo Inter inalterado (default
pix_mode=inter_dynamic aplicado pela migration).

Backend (sem SOUL/SKILL ainda — Frente 7 vem depois):
- Migration concurrent: pix_mode + 4 campos manual_pix_* em captain_units;
  provider + manual_proof_payload + manual_review_reason em captain_pix_charges
- Captain::Unit: enum pix_mode (prefix), validação condicional manual_*
- Captain::PixCharge: status estendido (awaiting_proof, pending_review),
  scope manual/inter, nota interna ramificada por modo
- GeneratePixTool MCP: branch manual_static (chave fixa, mensagem direta
  sem QR/Inter), preserva fluxo Inter intacto
- 4 tools MCP novas: verificar_comprovante_pix (vision gpt-5.3-codex),
  criar_nota_interna (genérica), confirmar_reserva_pix_manual (wrapper
  do ConfirmationService), marcar_reserva_pendente
- ConfirmationService: source_label cobre 'manual_pix_proof'

Próximos passos manuais (não inclusos neste commit):
1. Rodar migration em prod (entrypoint não roda no boot)
2. Seed Padova/Express com pix_mode=manual_static + chaves Stone
3. Deploy nova imagem via docker service update
4. Editar SOUL/SKILL Padova/Express na VPS Hermes + kill+boot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:01:01 -03:00
Rodribm10
c7d5bbff99 merge: align main with hermes production branch 2026-05-05 17:39:56 -03:00
Rodribm10
4f488ca842 fix(inbox): remove captain dependencies before delete
Some checks failed
Build and Push to GHCR (multi-arch) / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Build and Push to GHCR (multi-arch) / merge (push) Has been cancelled
2026-05-05 17:09:08 -03:00
Rodribm10
f6a96f9d4e fix(inbox): remove captain dependencies before delete 2026-05-05 16:41:09 -03:00
Codex CLI
f50c6addc6 fix(captain): allow agents to access gallery settings 2026-05-05 14:54:18 +00:00
Codex CLI
1cbc9f1123 fix(captain): keep faq policy patch scoped 2026-05-04 13:21:38 +00:00
Codex CLI
689cc114f8 fix(captain): allow agents to manage FAQs 2026-05-04 13:16:47 +00:00
Codex CLI
d670c5644b fix(captain/reservations): prioritize confirmed bookings 2026-05-03 11:03:38 +00:00
Rodribm10
c954f0fab4 feat(profile): UI pra ligar/desligar alerta de conversa parada
Nova seção em Configurações → Perfil "Alerta de conversa parada" com:
- Checkbox principal "Ativar alerta de conversa parada" (OFF salva
  ui_settings.aggressive_alert_inbox_ids = []).
- Sub-checkbox "Aplicar em todas as caixas" (ON salva null = todas).
- Lista de inboxes (visível quando não é "todas") pra selecionar caso
  por caso (salva [id, id, ...]).
- Persiste a cada change via updateUISettings (sem botão "salvar").

Antes só dava pra mexer via Rails runner. Cada admin agora controla
sozinho sem mexer em DB.

i18n: PROFILE_SETTINGS.FORM.AGGRESSIVE_ALERT_SECTION.* em pt_BR e en.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:04:14 -03:00
Rodribm10
a14fd4ed83 fix(banner): clique no nome navega pra conversa (snake_case param)
O openConversation passava `conversationId` (camelCase) como param do
$router.push, mas a rota `inbox_conversation` declara `:conversation_id`
(snake_case). Vue Router ignorava o param e route.params.conversation_id
ficava undefined → tela "selecione uma conversa no painel".

Fix: passar `conversation_id` snake_case (mesmo padrão dos demais
callsites: SLAReportItem, captain/responses/Pending).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:56:08 -03:00
Codex CLI
f4255cff97 fix(captain/hermes): improve deterministic auto reactions 2026-05-03 00:21:45 +00:00
Rodribm10
069e464ee4 feat(construtor): SKILL.md inclui regras de parsing de payload (Valentina-style)
A skill da Valentina (referência, 353 linhas) tem regras explícitas que
faltavam nas Hermes geradas pelo Construtor (107 linhas). Sem elas, o
LLM mistura turns no mesmo payload, repete info já dada, ou alucina em
texto de outro suporte.

save_agent_spec_tool.build_skill_md agora gera bloco final com:
- "Última fala manda" (responder só a mais recente quando vier rajada)
- "Burst repetido" (mesma fala 2x → 1 resposta)
- "Texto de outro fluxo" (operadora/menu externo → frase canônica)
- "Mensagem vazia" (frase canônica)
- "Loop" (mesma entrada → mesma saída, sem variação)
- "Nunca vazar bastidor" (sem [ctx]/CONTEXT COMPACTION/meta-texto)

Aplicado também manualmente nas SKILL.md das 3 Hermes existentes
(Nina/Lara/Juliana) que tinham a versão curta. Valentina já tinha a
versão completa na skill dela.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:43:21 -03:00
Rodribm10
b561aa8451 revert(hermes): remove camadas 2/3/4 + reabilita memória
A causa raiz dos bugs de "info repetida em turns anteriores" era o
default_scope ASC do Message conflitando com .order(desc) no debounce
(ver commit f1d3a124d). Como já corrigi com .reorder, as Camadas 2, 3 e
4 viraram peso morto que adicionava latência/false positive sem ganho.

Removido:
- Camada 2 (factual sem tool → retrigger force_factual_tool)
- Camada 3 (strip de linhas repetidas com pool de outgoings anteriores)
- Camada 4 (topic gating: bloqueio quando resposta tem tópico não pedido)
- Tracker de tool calls em McpController (suportava Camada 2)
- Snapshot baseline em OutgoingJob (suportava Camada 2)
- Regra "🚨 NÃO CONFIE NA SUA MEMÓRIA" das 4 SOUL.md Hermes

Mantido:
- Camada 1: handoff intencional ("Um momento — vou verificar") +
  loop detection (Jaccard >= 0.50 ou pergunta reformulada com 3+
  keywords). Genuíno pra bot externo (Claro/Vivo) e loops óbvios.
- Label-guard em OutgoingJob (não dispatch se conv tem triagem_humana).
- Auto-react ambient (feature original).
- Reorder fix no combined_incoming_content (causa raiz).

Memory + user_profile reabilitados nos 4 Hermes (config.yaml) e no
template do hermes-provision pra futuros agentes. Sem memória, cliente
precisa repetir nome/CPF/contexto a cada turn — UX horrível.
Contaminação cross-unit que justificava desligar vinha de outro bug
(X-Captain-Assistant-Id apontando pro parent), já corrigido.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:39:43 -03:00
Rodribm10
f1d3a124d5 fix(hermes): debounce agrupava msgs de turns ANTIGOS (default_scope ASC)
Bug raiz que causou todos os problemas de "Hermes repetindo info de
turns anteriores" hoje:

Message tem default_scope order(created_at: :asc). O combined_incoming_
content fazia .order(created_at: :desc) que GERA SQL com 2 orderings
em conflito ("ORDER BY created_at ASC, created_at DESC") — no Postgres
o ASC ganha quando a primeira coluna é igual. Resultado:
last_real_outgoing virava a MAIS ANTIGA outgoing (a saudação inicial),
não a mais recente. Aí o scope incoming agrupava TODAS as msgs do
cliente desde o "Oi" da Juliana, juntando wifi+pet num turn só.

Caso real conv 6064 (2026-05-02 21:18):
  T1: cliente "Preciso senha wifi" → Hermes "Prime2025"
  T2: cliente "Posso levar animais?"
  → debounce agrupou ["Preciso senha wifi", "Posso levar animais"]
    como se fossem MSGS DO MESMO TURN, mandou pro Hermes como
    "Cliente acabou de dizer: Preciso senha wifi.\nPosso levar animais?"
  → Hermes consultou faq_lookup com query "senha wifi e animais"
  → Resposta: "Senha Prime2025 e pode levar animais"

Fix: usa .reorder(created_at: :desc) que sobrescreve default_scope,
gerando SQL limpa com só "ORDER BY created_at DESC".

Camadas 3 (strip linhas) e 4 (topic gating) que adicionei antes são
paliativos válidos como defesa em profundidade, mas o problema raiz
era esse default_scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:30:12 -03:00
Rodribm10
ebf98c90f4 feat(captain/hermes): camada 4 — topic gating contra info não pedida
Camada 3 (strip de linhas repetidas) não cobre quando LLM mistura tópico
antigo + atual numa LINHA SÓ (caso real: cliente pergunta sobre pet,
Hermes responde "A senha é Prime2025 e pode levar animais sim" —
overlap baixo, strip não dispara).

Camada 4 detecta tópicos factuais (Wi-Fi/senha/pet/estacionamento/preço/
cancelamento) presentes NA RESPOSTA mas AUSENTES da última pergunta do
cliente. Quando detectado, bloqueia entrega + dispara
[SISTEMA: force_topic_focus] no Hermes mandando responder
EXCLUSIVAMENTE sobre o tópico atual. 1 retry; persistindo, entrega.

Validado: cliente "Posso levar animais?" + resposta "Senha + pet" →
detecta [:wifi] como off-topic. Cliente "Qual senha wifi?" + resposta
"Senha é X" → vazio (passa normal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:22:41 -03:00
Rodribm10
cc58805722 feat(captain/hermes): camada 3 — strip de linhas repetidas + check FAQs
LLM tende a "resumir" info de turns anteriores em toda nova resposta.
Camada 3 strip linhas onde >=70% das palavras significativas já apareceram
nas últimas 3 outgoings (filtra reactions). Saudações curtas preservadas.

Caso real Juliana 2026-05-02 (turn 3 ela ia repetir "Senha Prime2025 +
pet" mesmo cliente só dizendo "valeu"). Após strip: vira só "Imagina,
Rodrigo 😊".

Validator UI: novo check "FAQs próprias aprovadas > 0" — alerta quando
zero (faq_lookup cai no parent, risco de info desatualizada igual ao
bug do X-Captain-Assistant-Id que vazou Wi-Fi do parent hoje cedo).

Filtro SQL `content_attributes ->> 'external_source'` não casava (coluna
json, não jsonb); migrado pra filtro Ruby.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:16:39 -03:00
Rodribm10
30fc2460bb fix(hermes-provision): X-Captain-Assistant-Id usa id próprio, não do parent
Antes: header MCP era setado como PARENT_ASSISTANT_ID (Captain interno).
Tools como faq_lookup buscavam dados do parent — quando Hermes id=10
tinha FAQ "senha=Prime2025" mas parent id=1 tinha FAQ "senha presencial",
o Hermes respondia com a do parent (errada).

Agora: usa ASSISTANT_ID (id próprio do Hermes recém-criado). FAQs e
qualquer outra tool que filtra por assistant.id pegam os dados certos.

Migração manual aplicada nos 4 Hermes existentes (valentina/nina/lara_h/
juliana_qnn1) trocando 6→7, 4→8, 3→9, 1→10. Sessions e state.db dos 4
foram limpos pra evitar contaminação do histórico anterior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:04:03 -03:00
Rodribm10
ed99f67525 feat(captain/hermes): camada 2 — gating de saída factual sem tool call
Detecta alucinação de memória: se resposta do Hermes contém info
factual (preço/senha/horário/regra/política) E o LLM NÃO chamou
nenhuma tool MCP entre dispatch e callback, bloqueia entrega + dispara
system_message forçando consulta a tool. 1 retry; persistindo, escala.

Implementação:
- McpController: incrementa Rails.cache hermes_tool_calls:<conv_id>
  em cada tools/call.
- OutgoingJob: snapshot do contador como hermes_tool_calls_baseline
  ANTES de despachar pro Hermes.
- HermesCallbackController.gate_factual_no_tool!: compara baseline vs
  current; se igual + FACTUAL_PATTERNS bate, intercepta. Patterns
  cobrem R$, %, "senha", check-in/out + horário, política de
  cancelamento, "permitido", "pode levar pet/animal".

Caso real: cliente pede senha do Wi-Fi → Hermes responde de cabeça
"é passada presencialmente" sem chamar faq_lookup → callback intercepta,
não entrega pro cliente, manda [SISTEMA: force_factual_tool] pro Hermes
com instrução de chamar faq_lookup. Se faq_lookup vier vazio → frase-
âncora handoff.

Auto-react ambient: removido filtro de "?" que barrava em prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:58:17 -03:00
Rodribm10
9f2a08f478 fix(hermes-provision): substitui localização Ponta Negra/Natal pela do spec
Sem isso, todo agente novo herdava "em Ponta Negra, Natal/RN" da SOUL.md
template (Valentina é Dolce Amore — Natal). Caso real: Juliana Qnn01
respondia "em Ponta Negra, Natal/RN" sendo de Ceilândia/DF.

Adiciona campo city ao spec e sed que substitui pela localização correta
quando setado. Spec já tinha "city" no header docstring, só não era lido.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:40:32 -03:00
Rodribm10
c960dc7e1e feat(captain/hermes): handoff por loop + label-guard em outgoing
Substitui o interceptor agressivo de "Um momento — vou verificar"
(que bloqueava silenciosamente) por handoff explícito + loop detection:

- HANDOFF_PATTERNS: detecta a frase-âncora ("Um momento — vou
  verificar", "Aguarde um instante") e ENTREGA pro cliente,
  marcando conv com label triagem_humana.
- looped_response?: detecta 2 outgoing similares (Jaccard >= 0.50)
  OU pergunta reformulada sobre mesmo tópico (>= 3 keywords em comum
  + ambas inquisitivas via "?" / "me confirma" / "qual prefere"
  etc). 1ª resposta passa, 2ª escala. Cobre o caso "endereço ou
  link?" → "apenas link ou link + endereço?".
- OutgoingJob: guard que pula dispatch se conv tem label
  triagem_humana ou hermes_placeholder. Hermes não responde mais →
  não gasta token + não gera loop.

Cobre 2 casos do Rodrigo:
1. Bot da Claro insistindo em menu → 2ª resposta similar escala.
2. Hermes pedindo confirmação 2x sem entregar → escala.

Tokenize normaliza acentos (transliterate) pra stopwords baterem
"voce/você", "endereco/endereço", etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:30:58 -03:00
Rodribm10
10fec1d2cb feat(captain/hermes): auto-react ambiente — 20% de chance em msgs neutras
Cobre o gap entre saudação e despedida: quando msg do cliente não bate
nenhum dos regex determinísticos (thanks/confirm/greet/farewell), rola
dado e reage com emoji discreto (😊/💕//💯/🤗) em ~1 a cada 5 msgs.

Filtros pra não reagir em momentos errados (ambient_eligible?):
- Tamanho 6..180 chars (evita "oi" e narrativas longas).
- Sem "?" (perguntas esperam resposta de texto, não emoji).
- Sem keywords de fluxo de reserva (cpf, valor, hora, data, suite).
- Não começa com número (CPF, telefone, valor).

Determinístico continua igual — agente parece mais vivo sem virar
robozinho que reage em tudo. Atende pedido do Rodrigo de "reagir 20%
da conversa, não só nas saudações".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:20:36 -03:00
Rodribm10
42ada8100e feat(captain/mcp): tool check_suite_availability — bridge PlugPlay
Tool nova reusa as Captain::CustomTool 'custom_status_suites_<unidade>'
já cadastradas no painel (Ferramentas) que apontam pro PlugPlay
(oxpi.com.br/PlugPlay/SuitesStatus) com auth headers PLUG-PLAY-ID +
PLUG-PLAY-TOKEN específicos por unit.

Hermes consulta antes de generate_pix pra confirmar disponibilidade.
Resolve unit via Assistant.captain_unit_id; mapping unit → CustomTool é
por substring no nome (qnn/dolce/express/primeal/primevl).

Filtro opcional por suite_category. Retorno textual agrupado por classe
com contagem livre/ocupada e número das suítes livres (max 8).

Validado em Qnn01: 4 hidromassagens livres (101,102,103,108) /
15 Master livres / 5 Standard livres — bate com painel PlugPlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:10:57 -03:00
Rodribm10
c8785b999c fix(captain/hermes): intercepta placeholder e força tool call
Quando o LLM emite " Um momento — vou verificar." (ou variantes:
"deixa eu verificar", "aguarde um instante") sem chamar nenhuma tool,
o callback agora:

1. NÃO entrega a msg pro cliente (UX terrível ver "vou verificar" e
   ficar esperando indefinidamente).
2. Dispara notify_event pro Hermes com [SISTEMA: force_tool_call]
   instruindo a retomar e chamar a tool relevante (generate_pix,
   send_suite_images, faq_lookup) com base na última msg do cliente.
3. Limita 2 retries por conversation via Rails.cache (TTL 5min). Após
   esgotar, marca labels hermes_placeholder + triagem_humana e descarta.

Caso real do Rodrigo: cliente confirmou reserva ("Para hoje 23h por 4h")
e o LLM respondeu apenas o placeholder (api_calls=1 no daemon, sem
tool). Cliente ficava esperando sem resposta. Agora Captain força o
LLM a chamar a tool, ou cai pra triagem humana após 2 tentativas.

PLACEHOLDER_PATTERNS cobre as variações observadas. SKILL.md já proibia
"Um momento", mas o LLM ignorava — defesa em camadas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:01:18 -03:00
Rodribm10
bd494c424d fix(captain/mcp): generate_pix dispatcha fallback em mais cenários
Antes: fallback (link da página de reserva) só disparava quando Inter API
explodia (rescue StandardError). Erros estruturais (categoria não existe
na unit, período/dia inválido, sem credencial Inter cadastrada) caíam em
error_response e o LLM travava em " Um momento — vou verificar." sem
mandar nada pro cliente.

Agora: dispatch_no_pricing_fallback! cobre os casos onde Pix nem foi
tentado. Mesma UX do fallback existente — link pré-preenchido + label
pix_falhou_fallback pra triagem da gerência. Tool retorna isError=false
com instrução curta pro LLM ("só confirme que o link foi enviado, não
fale do problema técnico").

Caso real: Rodrigo pediu hidromassagem na inbox da Dolce Amore (unit que
não tem essa categoria). PricingTables retornou "Categoria não
reconhecida"; antes o LLM ficava no placeholder. Agora cliente recebe
link da página oficial automaticamente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:56:52 -03:00
Rodribm10
28e880d7b6 feat(captain/hermes-builder): aba Verificação com 22+ checks + reparo automático
UI nova dentro do Construtor (Hermes) — TabBar com Chat e Verificação.
Verificação roda HermesBuilder::Validator (DB+runtime) e exibe resultado
agrupado por categoria, com botão Refazer inline em FAIL/WARN reparáveis.

Backend (porta dos checks DB do CLI bin/hermes-validate):
- HermesBuilder::Validator com 22+ checks: engine, profile, port,
  secret, parent, unit, Brand, CaptainInbox sync (o bug que travou
  Juliana), pricing dry-run, Inter creds, typing/response_delay,
  registry MCP completo.
- HermesBuilder::Repairer com 4 handlers automáticos: set_engine_hermes,
  sync_captain_inbox_unit, set_default_typing_delay,
  set_default_response_delay.
- Endpoints novos: GET assistants, GET validate?slug=, POST repair.

Frontend:
- builder/Index.vue: wrapper com TabBar.
- builder/BuilderChat.vue: extraído do Index original.
- builder/BuilderVerification.vue: dropdown + Conferir agora + lista
  agrupada por categoria com badges + botão Refazer inline.

i18n: keys em pt_BR e en sob CAPTAIN_HERMES_BUILDER.VERIFY.*.

Filesystem/systemd checks ficam pro CLI hermes-validate (Rails container
não enxerga /root/.hermes/profiles do host).

Validado HTTP: GET /validate?slug=juliana_qnn1 → 28 PASS / 0 FAIL / 1 WARN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:27:40 -03:00
Rodribm10
47f32b540b fix(captain/mcp): generate_pix prefere assistant.captain_unit + 3 checks runtime
resolve_unit agora prioriza Captain::Assistant.captain_unit_id sobre o
mapping legado CaptainInbox (que falha quando 2 agentes — interno e
Hermes — compartilham a mesma inbox).

Caso real: Juliana Hermes (unit Qnn01) compartilhava inbox 1 com Juliana
captain_interno (unit Recanto), mas o CaptainInbox da inbox 1 estava
mapeado pra unit Dolce Amore (id=4) por contaminação anterior. Tool
resolvia unit errada, generate_pix retornava "categoria não reconhecida"
e o agente travava em " Um momento — vou verificar." sem retomar.

bin/hermes-validate ganha 3 checks novos:
- CaptainInbox.unit == Assistant.unit (FAIL — exatamente o bug acima)
- Pricing dry-run (calcula preço da 1ª categoria sem erro)
- Credenciais Inter completas (WARN se faltar cert/key — cai no fallback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:09:04 -03:00
Rodribm10
9cfd131dcf feat(hermes): script de validação + sed anti-vazamento Dolce Amore
- bin/hermes-validate <slug>: 44 checks de saúde (DB, filesystem, systemd,
  routing, MCP tools, humanização). Saída textual ou --json. Exit 0 sem FAIL.
- bin/hermes-provision: sed adicional substitui exemplos hardcoded de
  categorias Dolce Amore (Mini Chalé 45 etc) pelas 3 primeiras categorias
  da unidade nova; evita resíduo em descrições de tools.
- Fix bash: trocar `|| echo 0` por `|| true` em greps (evita "0\n0" quando
  grep -c não acha e ainda imprime contagem).

Validado em juliana_qnn1: 43 PASS / 0 FAIL / 1 WARN (gallery seed pendente).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:53:35 -03:00
Rodribm10
bc87b496a4 fix(captain/hermes): SOUL.md vem da Valentina (template) com sed identity + Pix flow no skill 2026-05-02 14:39:36 -03:00
Rodribm10
d02cb72336 fix(captain/hermes): anti-leak entre unidades — desliga memória + guard SOUL.md
Bug: Juliana de Qnn01 oferecia categorias do Dolce Amore (Apartamento,
Mini Chalé 45, Suíte Ouro). Vazamento via:
1. Hermes daemon memory_enabled+user_profile_enabled — acumula contexto
   entre turnos no mesmo profile.
2. Codex/ChatGPT memória user-level — todos os profiles compartilham o
   mesmo OAuth (borbamachadoo@gmail.com), então ChatGPT lembra de
   conversas com Valentina e bleeda no LLM da Juliana.
3. Tool descriptions com exemplos motel-flavored (já corrigido em commit
   anterior).

Fix permanente no script hermes-provision:
- sed do config.yaml zera memory_enabled e user_profile_enabled
- append no SOUL.md adiciona "REGRA CRÍTICA — IGNORE OUTRAS UNIDADES"
  com lista explícita de categorias válidas (extraída do spec.categories)
  e instrução pro LLM ignorar memória de outras unidades.

Aplicado retroativamente em juliana_qnn1 + sessions/state.db limpos.

Próximos agentes do Construtor nascem com essa proteção por padrão.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:19:37 -03:00
Rodribm10
f0f8322cce fix(captain/mcp): tool descriptions sem exemplos motel-flavored + brand match
Bug 1: send_suite_images_tool description mencionava 'Master, Luxo, Mini
Chalé 45' como exemplos. Quando outros agentes (ex: Juliana de Qnn01)
faziam tools/list, o LLM via essas categorias e usava como referência —
respondia oferecendo Mini Chalé 45 e Suíte Ouro pra cliente do 1001
Noites Ceilândia (que não tem essas categorias). Removidos exemplos.

Bug 2: lookup_brand fazia fuzzy match permissivo demais. 'Hoteis 1001
Noites' e 'Hotel 1001 Noites Prime' ambos contêm '1001 Noites' — quem
vinha primeiro no .find ganhava (Prime, ID menor). Juliana de Qnn01
(brand 'Hoteis 1001 Noites') saiu do Construtor com SOUL.md dizendo
'Hotel 1001 Noites Prime'. Fix: prioriza brand do parent_unit (fonte
canônica), depois exact-match casecmp, depois fuzzy.

Tudo isso vale pra Construtor entregar agente certo da próxima vez —
foram os 2 erros de provisionamento da Juliana hoje.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:07:15 -03:00
Rodribm10
ed21722dc4 fix(captain/hermes): callback aceita ?slug= além de ?inbox_id=
Hermes daemon faz POST /webhooks/captain/hermes_callback?slug=<profile>
mas controller só conhecia ?inbox_id. 404 → resposta do LLM nunca chegava
ao Captain. Cliente via só auto-react.

Fix: fetch_inbox resolve via Captain::Assistant.find_by(hermes_profile_name)
quando slug está presente. Inbox é a primeira CaptainInbox associada a
esse assistant. Suporta o pattern admin de re-apontar uma inbox de teste
(ex: Angelina) entre vários agentes Hermes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:53:12 -03:00
Rodribm10
88a5adb65e fix(captain/hermes): subscription_name e secret usam profile_name (slug)
Bug: Captain dispatchava pra /webhooks/captain-inbox-<inbox.id>, mas o
script hermes-provision criava subscription com nome captain-inbox-<slug>.
Mismatch → daemon retornava 404, Sidekiq retentava, AutoReact firava
N reactions sem nunca dispatchar pro LLM.

Fix:
- subscription_name_for(inbox): se o assistant tem hermes_profile_name,
  usa "captain-inbox-<slug>" (estável por agente). Fallback pra
  "captain-inbox-<inbox.id>" só se não tiver slug.
- subscription_signing_secret(inbox): lê de
  assistant.hermes_subscription_secret primeiro (DB-driven, gravado pelo
  script). Fallback pra env var legacy CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id>.

Resultado: admin pode apontar Angelina (inbox 1) pra qualquer agente
Hermes (Valentina · Hermes / Nina · Hermes / Lara.H / Juliana · Hermes)
e o roteamento funciona — não depende mais de inbox.id no path.

Renomeei manualmente as subscriptions de Valentina e Nina nos profiles
da VPS (eram captain-inbox-1 e captain-inbox-5 legado) pra
captain-inbox-valentina e captain-inbox-nina.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:42:22 -03:00
Rodribm10
ec50496d21 fix(captain/hermes): hermes-provision usa hermes_profile_name como chave
Bug: find_or_initialize_by(name: ...) batia em captain_interno existente
com mesmo nome (ex: 'Juliana') e SOBRESCREVIA engine='hermes'. Resultado:
Juliana captain_interno virou Hermes, atendimento legado quebrou.

Fix: chave passa a ser (account_id, hermes_profile_name=slug). Auto-sufixa
nome com ' · Hermes' se colidir com captain_interno do mesmo nome (a não
ser que já tenha 'Hermes' no nome). Também grava captain_unit_id no
record (relação direta agora que existe a coluna).

Reverti manualmente Juliana id=1 + criei id=10 'Juliana · Hermes' fix in
DB. Future provisionamentos pelo Construtor usam o caminho corrigido.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:34:07 -03:00
Rodribm10
955cb824b3 fix(captain/mcp): save_agent_spec — caminho de sucesso retorna tupla [hash, errors]
Bug: expand_spec retornava [{}, errors] no erro mas só {hash} no sucesso.
Destruturação 'expanded, errors = expand_spec(...)' deixava errors=nil
→ errors.any? → undefined method any? for nil.

Fix: extraído build_expanded_hash + sucesso retorna [hash, []].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:26:25 -03:00
Rodribm10
828e2e6af3 fix(captain/mcp): save_agent_spec — passa categorias já serializadas pra build_skill_md
Bug: build_skill_md recebia AR Captain::PricingCategory objects mas o
format_pricing_block fazia cat['amounts'] (índice de hash). AR respondem
a .amounts, não a ['amounts'] → nil → undefined method 'group_by' for nil.

Fix: serializa primeiro, reusa em ambos os lugares.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:17:38 -03:00
Rodribm10
5ef59ecc12 fix(captain): captain_unit_id em captain_assistants — fim do lookup quebrado
Captain::Assistant agora aponta direto pra Captain::Unit. Antes a relação
ia via CaptainInbox, mas isso quebrou quando re-apontamos as inboxes pros
Hermes — assistants captain_interno (Juliana, Bianca, Lara, Nina,
Valentina) ficaram SEM CaptainInbox associada e o lookup
unit_for(assistant) retornava nil.

Resultado: get_assistant_pricing(3) (Lara) caía no fallback de scenario
text. Construtor reportava "veio cenário/prompt, não tabela estruturada".

Migration adiciona captain_unit_id (FK opcional). Backfill explícito:
- 1 Juliana → unit 3 (Qnn01)
- 2 Bianca → unit 2 (PrimeAL)
- 3 Lara → unit 2 (PrimeAL — mesmo brand)
- 4 Nina → unit 5 (Express)
- 6 Valentina → unit 4 (Dolce Amore)
- 9 Lara.H → unit 2 (via parent_assistant_id=3)

Tools get_assistant_pricing_tool e save_agent_spec_tool atualizados pra
usar assistant.captain_unit primeiro (nova relação direta), com fallback
pro CaptainInbox se nulo (pra retrocompatibilidade).

Validado live: tool retorna grid markdown com Stilo/Alexa/Hidromassagem
em Seg-Qua + Qui-Dom corretamente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:45:53 -03:00
Rodribm10
22eab86302 fix(captain/mcp): get_assistant_pricing lê do DB + save_agent_spec expande
ROOT FIX (não paliativo) das 3 lacunas que travavam o Construtor:

1. get_assistant_pricing_tool: lia de Captain::Mcp::PricingTables::TABLES
   (hash Ruby) que NÃO EXISTE MAIS desde a migração pra DB. Caía no
   fallback de scenario raw. Refactor: lê de Captain::PricingCategory +
   Captain::PricingAmount, formata grid markdown agrupado por day_bucket.

2. save_agent_spec_tool: Construtor salvava REFERÊNCIAS
   (pricing_source.copied_from_assistant_id) mas hermes-provision script
   espera DADOS EXPANDIDOS (categories[] com amounts, soul_md+skill_md).
   Refactor: tool agora EXPANDE server-side — busca PricingCategory do
   parent, monta categories array, gera SOUL.md (template + identity +
   disclosure_policy) e SKILL.md (template + pricing + rules + identity).
   Output já é spec consumível pelo script.

3. Captain::PricingAmount::PERIODS: adicionado '1h' (Prime tem 1h).

4. Seed pras 3 units faltando: Hotel Recanto (1) + PrimeAL (2) + Qnn01
   (3). Agora os 6 units existentes têm pricing em DB.

Hot-patched ambos tools + USR1 no Puma. Construtor pronto pra criar
Bianca/Juliana etc end-to-end sem intervenção manual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:37:53 -03:00
Rodribm10
0f39945f43 feat(captain/mcp): get_assistant_scenario tool — Construtor copia identidade de outro agente
Construtor atende 'copiar maps/endereço/telefone/wifi da Lara' sem o admin
redigitar. Tool retorna o markdown bruto do scenario (default
Daniela_Reservas) do assistant fonte; LLM extrai os campos relevantes.

Cobre o gap entre get_assistant_pricing (preços estruturados) e
get_assistant_faqs (Q&As): essa retorna prompt CRU pra LLM interpretar
campos não estruturados (contatos, links, wifi, persona).

Hot-patched + USR1 no Puma e Construtor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:55:13 -03:00
Rodribm10
dd9e11da14 fix(captain/mcp): get_assistant_faqs — não filtrar por documentable_type
Filtro original só retornava FAQs com documentable_type=NULL, mas a maior
parte das FAQs aprovadas das Jasmines (Juliana, Bianca, Lara, Nina) tem
documentable_type='User' ou 'Conversation' (origem: histórico de
conversas). Resultado: tool retornava "0 FAQs aprovados" pra todos exceto
Valentina (única com FAQs criadas direto sem origem documentável).

Removido o filtro. Status='approved' já é suficiente — admin reviewou.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:49:54 -03:00
Rodribm10
59747e5400 fix(captain/mcp): tools do Construtor — typo _context: → context:
Tool registry chama tool.call(args, context: foo) mas as 3 tools read-only
do Construtor (get_assistant_faqs, get_assistant_pricing, save_agent_spec)
estavam declaradas como def call(args, _context:), com underscore. No
Ruby isso muda o nome do parâmetro keyword — ArgumentError:
'missing keyword: :_context' quando o Construtor tentava copiar FAQs/
pricing de um assistant existente.

Corrigido pra context: (sem underscore) com rubocop disable de
Lint/UnusedMethodArgument já que essas tools não usam o context.

Hot-patched via docker cp + Puma USR1 antes do deploy pro Rodrigo seguir
testando o Construtor sem esperar build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 11:41:23 -03:00
Rodribm10
280d250983 feat(captain/hermes): script hermes-provision pra Construtor autônomo
Bash script em bin/hermes-provision (instalado em /usr/local/bin/ na VPS)
que recebe spec JSON via stdin e provisiona um agente Hermes ponta-a-ponta:
- Valida spec (slug regex, preços 0-5000, períodos/buckets do catálogo)
- Aloca porta livre no range 8650-8699
- Gera HMAC secret via openssl rand
- Cria Captain::Unit (find_or_create), Captain::PricingCategory/Amount,
  Captain::Assistant (engine=hermes) via docker exec rails runner
- Copia template profile da Valentina, patcheia config.yaml com porta +
  X-Captain-Assistant-Id (parent_id se setado, senão self id)
- Escreve SOUL.md/SKILL.md do spec
- Gera webhook_subscriptions.json com secret
- Cria systemd unit hermes@<slug>.service e enable+start
- rsync profile pra repo de backup git local
- Idempotente: re-rodar com mesmo slug não duplica nada (find_or_create)
- --dry-run valida sem escrever
- --rollback <slug> destrói tudo (DB + systemd + filesystem)

Construtor (Hermes daemon) chama via terminal skill nativa:
  echo '<spec>' | /usr/local/bin/hermes-provision

Próximo passo: atualizar SOUL.md/SKILL.md do Construtor pra invocar
o script ao final do fluxo socrático.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:59:30 -03:00
Rodribm10
7995bc6fe6 feat(captain): pricing em DB + colunas de provisionamento Hermes
Migra a tabela de preços do PricingTables.rb hardcoded pras tabelas
captain_pricing_categories + captain_pricing_amounts no DB. Mantém a
mesma API pública Captain::Mcp::PricingTables.calculate(...) — código
chama o banco via novos modelos Captain::PricingCategory e
Captain::PricingAmount.

Seed db/seed_pricing_tables.rb faz backfill idempotente pra Dolce Amore
(unit 4) e Express (unit 5) com a mesma estrutura que tava no Ruby.

Adiciona em captain_assistants:
- hermes_subscription_secret (gerado pelo script de provisionamento)
- hermes_port (alocado no range 8650-8699)
- parent_assistant_id (link informativo Hermes → captain_interno parent
  pra sombrear FAQs/scenarios via header X-Captain-Assistant-Id)

Adiciona em captain_units: extra_person_fee + currency.

Primeiro milestone do roadmap arquitetural pro Construtor autônomo
(decisões em memory/project_construtor_autonomo_decisions.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:04:50 -03:00
Rodribm10
a2bb613e68 feat(captain/hermes): debounce — inbox.typing_delay como buffer + agrupar msgs
Antes: inbox.typing_delay funcionava só pro Captain interno
(schedule_internal_response). Hermes ignorava o campo e disparava
OutgoingJob na hora — campo da UI era cosmético pra inboxes Hermes.

Agora:
- schedule_hermes_response cancela jobs OutgoingJob pendentes pra mesma
  conversa e enfileira com wait=inbox.typing_delay (debounce window).
- OutgoingJob agrupa todas msgs incoming entre a última resposta real
  do agente (ignora reactions) e a msg âncora; dispatch envia o texto
  concatenado pro Hermes via novo content_override no Client#dispatch.

Resultado: cliente que digita "Oi" + "quero pernoite Master" em segundos
vê o agente esperar até o buffer vencer e responder UMA vez cobrindo
ambas as falas, em vez de 2 respostas atropelando o pensamento.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:46:24 -03:00
Rodribm10
e662913b21 fix(captain/hermes): auto-react idempotente — bloqueia retry duplicate
OutgoingJob faz retry no DispatchError (até 3x ActiveJob + Sidekiq).
Cada retry chamava AutoReactService.maybe_react! e criava uma reaction
nova — observado em prod 02/05 quando o env var
CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_5 estava faltando, gerando
401 → 6 reações duplicadas no inbox EXPRESS.

Adiciona guard already_reacted? que checa se já existe Message outgoing
com external_source='hermes_auto_react' e in_reply_to=msg.id antes de
criar uma nova. Defesa contra futuro 5xx/timeout do Hermes daemon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:32:47 -03:00
Rodribm10
3182002bd9 feat(captain): engine column + DB-driven Hermes routing + Express pricing
Marca cada Captain::Assistant com engine ('captain_interno' | 'hermes')
e move o roteamento Hermes do env var pro banco — admin troca engine
re-apontando a inbox no painel, sem deploy. Mantém fallback pras env
vars antigas (CAPTAIN_HERMES_INBOX_IDS etc) durante a migração gradual,
pra não quebrar Valentina antes da re-associação.

Frontend: badge "Hermes" (âmbar) ou "Interno" (cinza) ao lado de cada
assistant no dropdown switcher e no card da listagem, com chaves i18n
em en + pt_BR.

Tabela de preço (pricing_tables.rb): adiciona unit Express (id=5) e
estende a estrutura pra aceitar preço por dia da semana
(mon_wed/thu_sun) — necessário pro Express, retrocompatível com Dolce
Amore (preço único).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 07:54:01 -03:00
Rodribm10
5f6aed05c9 fix(captain/hermes-builder): callback resolve session via last_session
Bug: Hermes nao propaga chat_id no metadata do callback. Callback
controller nao conseguia resolver qual session armazenar a resposta.
WARN "no session_key resolvable — ignorando" descartava todas as
respostas do Construtor.

Fix: HermesBuilder::Storage.remember_last_session() grava ultima
session por account quando admin chama /start ou /create. Callback
le essa key via last_session_for(account_id). MVP-safe pra 1 admin
por conta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:53:43 -03:00