Skip to content

Fix provider routing and Telegram memory - #109

Open
sistemabritto wants to merge 220 commits into
evolution-foundation:mainfrom
sistemabritto:feat/chat-openclaude-provider-routing
Open

Fix provider routing and Telegram memory#109
sistemabritto wants to merge 220 commits into
evolution-foundation:mainfrom
sistemabritto:feat/chat-openclaude-provider-routing

Conversation

@sistemabritto

Copy link
Copy Markdown

This PR carries the provider routing work plus the Telegram runtime fixes:\n\n- route Telegram through the active provider / override config\n- add per-chat memory for the direct Telegram bot\n- harden heartbeat invocation for OpenClaude-backed providers\n- keep provider/model answers deterministic from config\n- add coverage for Telegram memory behavior\n\nValidation:\n- python3 -m unittest -q tests.backend.test_telegram_provider_bot\n- python3 -m py_compile scripts/telegram_provider_bot.py tests/backend/test_telegram_provider_bot.py\n- python3 dashboard/backend/heartbeat_runner.py --heartbeat-id zara-2h --triggered-by manual

DavidsonGomes and others added 23 commits June 12, 2026 06:14
…volution-foundation#52)

Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate
limiting. Add flask-limiter (in-memory, single-process MVP) with:
- 60 req/min/IP on view_share (Vault §2.S1)
- Global default 600 req/min on all other routes (non-blocking baseline)
- Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options
  headers on every public share response (Vault §2.S2)

The Limiter singleton lives in rate_limit.py to break the circular-import
chain between app.py (which imports route blueprints) and the blueprints
that need @limiter.limit() decorators.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ortals (evolution-foundation#53)

* feat(security): rate-limit public share endpoint + security headers

Vault audit §2.S1 CRITICAL: /api/shares/<token>/view had zero rate
limiting. Add flask-limiter (in-memory, single-process MVP) with:
- 60 req/min/IP on view_share (Vault §2.S1)
- Global default 600 req/min on all other routes (non-blocking baseline)
- Referrer-Policy, Cache-Control no-store, Pragma, HSTS, X-Content-Type-Options
  headers on every public share response (Vault §2.S2)

The Limiter singleton lives in rate_limit.py to break the circular-import
chain between app.py (which imports route blueprints) and the blueprints
that need @limiter.limit() decorators.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(plugins): B2.0 public_pages capability — read-only token-bound portals

Add the public_pages plugin capability (B2.0 scope, read-only):

plugin_schema.py:
- Add Capability.public_pages and Capability.safe_uninstall enum values
- Add PluginPublicPageTokenSource + PluginPublicPage Pydantic models
  (bundle must be under ui/public/, revoked_when disallowed in v1 to prevent SQL injection)
- Extend ReadonlyQuery with public_via + bind_token_param fields
- Add 4 PluginManifest model validators: capability required, table slug-prefix,
  unique ids/route_prefixes, readonly_data references valid page

routes/plugin_public_pages.py (new):
- GET /p/<slug>/<route_prefix>/<token>       — serve portal bundle (60 req/min/IP)
- GET /p/<slug>/<route_prefix>/<token>/data  — serve token-bound readonly query (120/min)
- GET /p/<slug>/public-assets/<path>         — serve ui/public/ static assets
- Token validation via parametric SQL (identifiers validated at install by schema)
- Module-level _PLUGIN_PUBLIC_PREFIXES cache for install/uninstall lifecycle
- Vault §B2.S2: Referrer-Policy, Cache-Control no-store, HSTS, X-Content-Type-Options on all responses
- CSP: default-src 'self' on portal bundles
- Rate limiting via rate_limit.limiter (imported from PR evolution-foundation#52)

app.py:
- Import and register plugin_public_pages_bp
- /p/... paths already bypass auth_middleware (non-/api/ paths are passthrough)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…vation, sandboxed hook (evolution-foundation#54)

- plugin_schema.py: PluginSafeUninstall, PluginPreUninstallHook, PluginUserConfirmation
  models + validators (block_uninstall enforcement, preserved_tables slug-prefix check,
  safe_uninstall_enabled_requires_confirmation, no _orphan_* refs in readonly SQL)
- routes/plugins.py: uninstall gate (admin-only, confirmation_phrase, exported_at check,
  zip_password); sandboxed pre-hook subprocess (no secrets, read-only DB copy, 600s timeout);
  cascade-DELETE filtering for preserved_host_entities; orphan table rename
  (_orphan_{slug}_{table}); EVONEXUS_ALLOW_FORCE_UNINSTALL=1 escape hatch with audit;
  reinstall SHA256 check against plugin_orphans before orphan recovery
- app.py: plugin_orphans table migration (id, slug, tablename, orphaned_at,
  orphaned_by_user_id, original_plugin_version, original_sha256, original_publisher_url,
  recovered_at, UNIQUE(slug, tablename))
- PluginUninstall.tsx: 3-step wizard (regulatory reason+checkbox → ZIP password → typed
  phrase); force-uninstall orange banner; integrated in PluginDetail.tsx
- docs/plugin-contract.md: full plugin.yaml contract for public_pages + safe_uninstall

Vault §B3 mitigations: S1 block_uninstall gate, S2 admin enforcement, S3 sandboxed hook,
S4 no _orphan_* SQL refs, S5 AES-256 ZIP password, S6 force-uninstall audit trail.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er auto-injection (evolution-foundation#55)

Two related Wave 2.1.x extensions to the plugin contract that close the last
two gaps blocking endpoint-level RBAC for plugin authors (gap inventory in
evonexus-plugin-nutri Step 3 RBAC decision).

Changes
- PluginWritableResource.requires_role: Optional[List[str]] — when set, the
  POST/PUT/DELETE handler returns 403 if current_user.role is not in the list.
  'admin' role always passes (super-user override). Backwards compatible:
  resources without the field accept any authenticated user (legacy default).
  Validator enforces kebab-case role names (^[a-z][a-z0-9-]*$).

- routes.plugins.writable_data: enforces requires_role at the endpoint, with
  a 403 message naming the required roles and the actor's current role.

- routes.plugins.readonly_data: auto-injects :current_user_id and
  :current_user_role as bind params on every readonly query. Plugins can
  reference them directly in SQL for server-enforced scoping without an
  app-layer wrapper. The two parameter names are reserved — client requests
  carrying them in the query string get 400 (no identity spoofing).

Tests
- tests/backend/test_plugins_rbac_and_scoping.py — 13 cases covering Pydantic
  acceptance/rejection, 403/200 paths for writable, scoping/spoofing for readonly,
  backwards compat for resources without requires_role and queries without
  :current_user_id refs.

Compat
- Existing plugins (PM Essentials) continue to work unchanged — the new field
  defaults to None and the auto-injected bind params are silently ignored if
  the SQL doesn't reference them.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ser, raw bundle for clients (evolution-foundation#56)

The portal_page handler at /p/{slug}/{route_prefix}/{token} previously served
the plugin's JS bundle with mimetype application/javascript regardless of the
caller. Browsers navigating to a portal URL saw the JS source instead of a
rendered page. Discovered during evonexus-plugin-nutri Step 5.

Changes
- portal_page: when the Accept header includes text/html and NOT
  application/javascript, render a minimal HTML shell that loads the bundle
  as <script type="module" src="/p/{slug}/public-assets/{file}"> and
  instantiates the plugin's declared custom_element_name. Token reaches the
  element via data-token attribute (no need for the bundle to re-parse
  window.location).
- _serve_html_shell: new helper. Defensive: validates bundle path is inside
  ui/public/ and custom_element_name matches alphanum-dash before emitting.
  Sets X-Content-Type-Options: nosniff + a tight CSP (default-src 'self',
  frame-ancestors 'none', no external scripts).
- Programmatic clients (curl, fetch with Accept: application/javascript)
  keep getting the raw bundle — backwards compatible.
- Bundle is fetched from the existing /p/{slug}/public-assets/{path} route
  (no token), which is safe because the bundle contains no patient data —
  data lives behind the token-gated /data endpoint.

Tests
- tests/backend/test_plugin_public_pages_html_shell.py — 9 cases:
  HTML accept renders shell, JS accept returns bundle, default Accept (*/*)
  returns bundle, invalid token returns 404 even with HTML accept, CSP +
  X-Content-Type-Options present, custom element name appears exactly once,
  XSS-safe (single <script> tag, token only inside data-token), legacy
  programmatic fetches unchanged.

Compat
- Existing public-page consumers using fetch() with Accept: application/json
  or default see no behaviour change. Plugins that already shipped a bundle
  start rendering correctly in browsers without any plugin update.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…volution-foundation#57)

Bumps version to 0.33.0 so the plugin nutri (which requires this version)
can install. Bundles the five plugin-contract PRs (evolution-foundation#52evolution-foundation#56) merged today
into a single release. Plus a UX fix on the install wizard so 409s say why
they conflicted.

The fix
- lib/api.ts buildError now falls back to data.conflicts[0] when the
  standard error/message fields are absent. The plugin preview endpoint
  returns {conflicts: string[], manifest, ...} on 409 — without this fix
  the wizard showed only "409 CONFLICT" with the actual reason hidden.
- PluginInstallModal: conflicts type was Record<string, unknown>, backend
  always returned string[]; the JSON.keys() coercion produced index strings.
  Now typed as string[] and rendered as a list.

Tested
- Frontend tsc --noEmit clean
- Plugin nutri 200 pytest still pass after the 11 `# nosec B603` markers
  added to subprocess.run calls (false positives from regex security scan —
  all calls use list args, no shell=True)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… mode

Non-Anthropic providers in the agent chat previously bypassed openclaude
entirely — a raw /chat/completions fetch with no tool calling, no session
history, and a hard API-key requirement that broke Codex OAuth. The
terminal was separately blocked by the isCodeModel() name heuristic for
agentic models like openrouter/owl-alpha.

- chat-bridge: external providers in code mode now go through the Agent
  SDK with pathToClaudeCodeExecutable pointing at the openclaude binary
  and a clean whitelisted env (mirrors ClaudeBridge). Restores tool
  calling, structured streaming, UI tool approval, and session resume.
  Chat-only models keep the REST path; missing binary falls back with a
  clear log.
- chat-bridge: external providers get full system-prompt replacement for
  agent personas (append is too weak for GPT models), matching the
  terminal behavior.
- provider-config: new per-provider `mode: code|chat` field overrides the
  model-name heuristic; the terminal no longer refuses agentic models
  whose names don't match the code regex.
- providers.example.json: mode field on openrouter/omnirouter + new
  NVIDIA NIM provider entry (OpenAI-compatible endpoint).
- tests: provider-config mode override coverage (node --test).

Verified end-to-end against OpenRouter: text streaming and Read tool
call complete with all UI events (tool_use_start, tool_input_delta,
block_stop, result).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OpenClaude >=0.18 detects integrate.api.nvidia.com and demands the key
in NVIDIA_API_KEY, exiting 1 when only OPENAI_API_KEY is set ("Claude
Code process exited with code 1" in the chat). Derive NVIDIA_API_KEY
from OPENAI_API_KEY in loadProviderConfig so the UI keeps a single key
field, and allowlist the var in both config layers.

Verified: chat session via Agent SDK + openclaude against NVIDIA NIM
(stepfun-ai/step-3.7-flash) completes with result: success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the NVIDIA provider beyond chat/code into image generation, and
fixes three bugs found while dogfooding the multi-provider flow:

- ai-image-creator: NVIDIA NIM provider with FLUX models — flux.2-klein-4b
  (default, best composition, ~2.5s), flux.1-dev (30 steps), flux.1-schnell.
  Aspect-ratio mapping constrained to the dimension set and 1.06MP pixel
  budget the API validates; per-model cfg_scale/steps defaults (schnell
  requires cfg=0, klein requires cfg>=1); JPEG output auto-converted to
  PNG via ImageMagick with ffmpeg fallback (temp files beside the output —
  snap-confined ffmpeg cannot read /tmp). Uses NVIDIA_API_KEY from .env.

- backend/providers: saving provider config with a blank key field no
  longer wipes the stored secret — the UI submits empty password inputs
  when editing other fields (e.g. model), silently erasing the API key
  and breaking every session afterwards with auth errors.

- claude-bridge: terminal agent personas now resolve from WORKSPACE_ROOT
  with cwd fallback, matching chat-bridge (cab8966) — sessions started
  outside the workspace root were silently falling back to a generic
  "You are the X agent" persona.

- Makefile: `make telegram` fails loudly when screen/bun are missing
  instead of printing a false success message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le providers

Enforce a minimum interval between chat completion requests and retry
429/503 responses with exponential backoff, honoring Retry-After.
Configurable via CHAT_MIN_INTERVAL_MS, CHAT_MAX_RETRIES, CHAT_BASE_DELAY_MS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
onclose only cleared the keepalive and never reconnected — a dropped
socket left the terminal/chat dead until the component remounted
(switching tabs). Now both components reconnect with capped exponential
backoff and rejoin immediately on visibilitychange, replaying the
session buffer on a cleared terminal to avoid duplicate output. If the
process died while disconnected, surface it instead of silently
restarting the agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r fallback

- Pin the CLI conversation to the terminal-server session UUID:
  first start uses --session-id, restarts use --resume when a persisted
  conversation file exists — provider crashes and server restarts no
  longer lose the conversation.
- Per-agent model tiers: agents declare model: opus|sonnet|haiku in
  frontmatter; providers.json maps each tier to a provider model via
  the new model_tiers field.
- Pass --fallback-model from the new fallback_models chain (first entry
  distinct from the primary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… totals

Older skill versions logged image cost entries without
token_usage.total_tokens — the unguarded access in the Image Generation
table threw a TypeError and unmounted the whole page. Normalize the
image-costs payload defensively, same pattern as normalizeCostData.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eator

New 'openai' provider hitting api.openai.com/v1/images/generations
directly, with the 'image2' model keyword (auto-detects provider).
Requires a platform API key (AI_IMG_CREATOR_OPENAI_KEY or
OPENAI_API_KEY) — ChatGPT Plus/Codex OAuth tokens lack the
api.model.images.request scope (verified: 401), so they cannot be used
for image generation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ude 0.18

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e cost estimate

Images API bills text input and image output at different per-token
rates — use the prompt/completion split when available instead of
total_tokens at the input rate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reds-bearing test script

Top-level business folders (finance/, meetings/, social/, etc.) sit
outside the workspace/** rule and were trackable — never commit private
operational data, especially in a public PR. Also ignore local backups
and scripts/ghost_integration_test.py (has hardcoded Ghost API keys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dation

- Heartbeats can now run an in-process Python handler instead of
  spawning a Claude agent: new 'handler' field (max_turns=0), DB
  migration, schema validation, dispatcher sync, runner execution path.
- provider_fallback.py: 429/quota detection + model/provider rotation
  with cooldown tracking (foundation; not yet wired into the runner).

Tests: tests/heartbeats 25/26 pass (the 1 failure is a data-driven seed
assertion against the gitignored local heartbeats.yaml, not a regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
step7 now routes through provider_fallback.invoke_with_fallback so a
429/quota error rotates model→provider (NVIDIA chain → Codex → native
claude) instead of failing the run. Disable with
HEARTBEAT_PROVIDER_FALLBACK=0; unavailable engine falls back to the
native claude path (preserved as _step7_invoke_claude_native).

Also in provider_fallback.py:
- fix NameError on 429 (DEFAULT_COOLDOWN → DEFAULT_COOLDOWN_SECONDS)
- align default NVIDIA model chain with validated models; drop OpenRouter
  (stealth models 404 intermittently)
- derive NVIDIA_API_KEY for NVIDIA base URLs (openclaude ≥0.18 needs it)
- parse token usage + cost from the CLI JSON envelope so heartbeat runs
  land real numbers on /costs instead of nulls

Tested: invoke_with_fallback + step7_invoke_claude run end-to-end via
NVIDIA glm-5.1; tests/heartbeats 25/26 (the 1 failure is the pre-existing
data-driven seed assertion on the gitignored local heartbeats.yaml).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bot ran on native claude (Anthropic) and got stuck on an Anthropic
usage-limit prompt — it stopped responding because the bun MCP polling
loop was blocked behind it. Route the channel agent to Codex via
CLAUDE_CODE_USE_OPENAI=1 + OPENAI_MODEL=codexplan (native claude keeps
--channels support; openclaude does not expose that flag).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @sistemabritto, your pull request is larger than the review limit of 150000 diff characters

@sistemabritto
sistemabritto force-pushed the feat/chat-openclaude-provider-routing branch from 0bc34bf to 4e97443 Compare June 14, 2026 14:07
sistemabritto and others added 30 commits July 15, 2026 14:57
…o HUD

Ticket pill, indicador de conexão/erro e badge de aprovação pendente eram
overlays `absolute top-3 ...` fixos no topo do container. Funcionava
enquanto não existia HUD, mas a linha do painel LCD (TerminalHudPanel) é
conteúdo normal que também começa no topo do mesmo container — assim que
o hud chega (sessão ativa normal), os badges renderizam por cima do
readout ("No ticket" sobrepondo o tok/s, reportado ao vivo 2026-07-15).

Fix: quando hud existe, os 3 badges viram filhos flex normais dentro da
própria linha do HUD (com flex-wrap, pra reflow em mobile em vez de
cortar) em vez de overlay absoluto. Sem hud, mantém o comportamento
original (overlay, útil durante a fase de conexão antes do primeiro
hud_update chegar).
…do bot

notifications.py (roda no container dashboard) manda mensagens pro
Telegram direto via HTTP — heartbeat alerts, outcome notifications,
aprovações pendentes — sem nunca tocar no arquivo de memória que
scripts/telegram_provider_bot.py (container telegram, processo separado)
usa pra montar o contexto de conversa em build_prompt(). Resultado
reportado ao vivo 2026-07-15: usuário perguntou sobre algo que uma
notificação tinha acabado de mandar, e o bot não tinha nenhuma memória
de ter enviado aquilo.

_send_telegram() agora espelha toda mensagem enviada com sucesso no mesmo
arquivo JSONL que telegram_provider_bot.py usa (mesmo formato,
append_chat_memory importado diretamente — mesmo binário, mesmo repo,
sem side-effect perigoso no import). Funciona porque /root/.claude é o
volume evonexus_claude_auth, montado no mesmo caminho em dashboard,
telegram E scheduler — não precisa de chamada de rede entre os serviços.
Best-effort: falha ao espelhar nunca derruba o envio real da notificação.
…icações do sistema entram na memória do bot)
…lved/closed

A sincronização ticket->goal_task só ia numa direção (marcava done ao
resolver, nunca desfazia ao voltar pra blocked/open). Isso deixou a Goal 7
com progresso falso depois que um "publish post" foi auto-resolvido com
claim fabricado pelo orquestrador do Telegram e um humano corrigiu o
ticket de volta pra blocked — o goal_task e o goal.current_value nunca
refletiram a correção. Agora _sync_goal_task_from_ticket reabre a task e
recalcula o goal (inclusive rebaixando 'achieved'->'active' se regredir)
quando o ticket sai de resolved/closed. update_ticket() e bulk_action()
agora disparam a sync em qualquer mudança de status, não só ao entrar em
resolved/closed.
… direto

O serviço telegram não monta o volume evonexus_dashboard_data (só o
dashboard monta) — abrir dashboard/data/evonexus.db direto sempre
resultava em "Banco de tickets não encontrado." quando o usuário
respondia (reply) a uma notificação de bloqueio pra desbloquear o
ticket. Confirmado ao vivo 2026-07-15/16 em duas ocasiões. Reescrito
para usar EVONEXUS_API_URL + DASHBOARD_API_TOKEN via HTTP, mesmo padrão
já usado por _nexus_api_get/fetch_mempalace_context neste arquivo.
… Mission/Project + Projects overview

Feature kanban-ux-redesign (Feature B do plano goal-ticket-unification/kanban
gerado via /ultraplan) — 7 steps:

- Kanban.tsx: fix mobile (min-w-[1440px] incondicional forçava scroll
  horizontal em telefone; agora só aplica a partir de md:); refresh visual
  do card (rail de prioridade, tipografia Space Grotesk/JetBrains Mono
  escopada via tokens Tailwind aditivos, sem tocar font-mono global).
- CreateTicketModal.tsx (novo): modal real (título/descrição/prioridade/
  assignee/goal opcional) reusando o padrão Modal/Field/Input/Select de
  Goals.tsx, substituindo window.prompt tanto no botão "Novo ticket" do
  Kanban quanto na ação "Create new ticket" do pill de chat (AgentChat.tsx).
- Goals.tsx: modais de criação de Mission/Project (mesmo padrão local),
  botões no header/empty-state/card de Mission — hoje a página era beco sem
  saída sem nenhuma Mission seedada.
- ProjectsOverview.tsx (novo) + App.tsx + Sidebar.tsx: nova página de
  overview de projetos (rollup client-side do payload existente de
  GET /api/missions, sem endpoint novo), rota /projects deixa de ser alias
  de <Goals /> e ganha entrada própria na nav.

tsc -b --noEmit e npm run build verificados limpos.
…amento (Steps 4/5/6)

NÃO É PRONTO PRA MERGE — commit de checkpoint urgente antes de desligar a
máquina, pra não perder trabalho de 2 agentes ainda rodando em paralelo.

Fase 1 (Steps 2+3 — migração + fonte única de current_value): CONCLUÍDA E
VERIFICADA com evidência real (ver tests/goals/test_goal_ticket_rollup.py,
5 casos passando; migração testada contra cópia do DB de produção; trigger
trg_task_done_updates_goal confirmado morto após reboot simulado).

Fase 2 (Step 4: infra de aprovação Telegram + condições V10/V11 do Vault;
Steps 5+6: heartbeat goal-planner + review loop self-healing): estado
incerto no momento deste commit — 2 agentes (goal-ticket-build-step4,
goal-ticket-build-step56) estavam rodando em paralelo e não reportaram
conclusão antes da interrupção. NÃO rodei py_compile nem a suíte de testes
neste checkpoint — verificar isso é o primeiro passo ao retomar.

Detalhes completos do estado, do que falta, e do ADR aprovado (com as
condições vinculantes V10/V11/R1) em memória:
~/.claude/projects/-home-sistemabritto-Documentos-evo-nexus/memory/goal-ticket-unification-build-state.md
…sobre 3a15ad1

- bug_002 (grave): DASHBOARD_API_TOKEN não deve ser removido do env do
  subprocess do agente — ele nunca autoriza /api/approvals/*, e removê-lo
  quebrava o EvoClient SDK usado por 13 skills, incluindo o próprio
  goal-planner. Denylist agora só derruba APPROVAL_BRIDGE_TOKEN + chaves
  de publicação, nos dois caminhos (provider_fallback.py e o fallback
  inline do heartbeat_runner.py).
- bug_020: fallback inline de env no heartbeat_runner.py também seta
  DISABLE_AUTOUPDANTER=1, como o caminho principal já fazia.
- bug_013 (corrupção de dados): _recompute_goal_from_tickets agora ignora
  goals com metric_type != 'count', evitando sobrescrever valores de
  currency/percentage/boolean com uma contagem crua de tickets.
- bug_006: /api/goals/{id}/recalculate e _recalculate_goal_value agora
  chamam _recompute_goal_from_tickets (fonte única) em vez de ler da view
  legada goal_progress_v (baseada em goal_tasks, congelada).
- merged_bug_014 (drift): update_ticket, bulk_action (delete/relink_goal)
  e o soft-close de delete_ticket agora recomputam tanto o goal de origem
  quanto o de destino sempre que status OU goal_id mudam.
- bug_007: sweep_pending_approvals só grava nudged_at quando o envio ao
  Telegram realmente teve sucesso, preservando a janela de retry.
- bug_005: approval_approvers() agora também lê APPROVAL_APPROVER_IDS do
  env (união com access.json, não substituição).
- bug_023: o teclado de aprovação só é removido quando a decisão foi de
  fato registrada (sucesso), não em qualquer callback (evita apagar os
  botões numa falha transitória ou tentativa não autorizada).

Verificação: py_compile limpo nos 7 arquivos; suíte tests/goals/ (16/16)
e tests/tickets/ (3 falhas + 3 erros, confirmadas pré-existentes via
stash comparativo antes destes fixes) sem regressão nova; 2 testes
adhoc confirmando bug_013 (goal currency preserva valor) e
merged_bug_014 (relink_goal recomputa origem e destino corretamente).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-gate, fecha reserva R1

Step 7 (ADR SPEC 3g/3h):
- Publish-gate: _maybe_park_for_publish em heartbeat_outcome.py intercepta
  qualquer ticket de agente em PUBLISHING_AGENTS (pixel/mako/pulse) que iria
  pra resolved/closed com publish_intent != False, parqueia em
  pending_approvals (gate_type='publish') e notifica no Telegram em vez de
  resolver direto. publish_target fora do conjunto fechado bloqueia sem
  criar aprovação (Vault V9). _OUTCOME_SCHEMA ganha publish_intent/
  publish_target, publish_intent agora required (fail-closed, Vault V5).
- _run_publish_action: NUNCA declara published=True sem confirmação real —
  hoje nenhum client script (instagram/linkedin) expõe publish, só
  analytics, então sempre retorna published=False com detail explicando que
  requer ação manual. Isso é o núcleo de segurança da ADR (o incidente real
  que motivou a feature foi um orquestrador fabricando "publicado com
  sucesso" sem publicar de fato).
- Decomposition-gate: novo endpoint POST /api/approvals (goal-planner chama
  via EvoClient) cria a linha pending_approvals com idempotency_key
  calculada server-side (Vault V7). routes/approvals.py::decide_approval
  agora fecha os 2 TODOs do Step 4: branch publish invoca
  _run_publish_action; branch decomposition cria os tickets DIRETAMENTE do
  payload aprovado.
- R1 (reserva do Raven, ADR Sign-off) FECHADA: aprovar uma decomposition
  cria tickets inline do payload já aprovado — nunca re-despacha
  goal_created pro sub-goal, o que bypassaria o guard parent_goal_id IS NULL
  de create_goal (esse guard só cobre criação, não re-dispatch).
- goal-planner.md habilitado a propor sub-goals (Step 3) quando o Goal é
  amplo demais pra virar 2-6 tickets diretos.
- Fix de idempotência encontrado na revisão: goal-planner só checava
  GET /api/tickets?goal_id= pra decidir se já decompôs — não pega o caminho
  de sub-goals, cujos tickets ficam sob o goal_id do sub-goal, não do pai.
  Um re-wake criaria sub-goals duplicados. Adicionado filtro
  parent_goal_id em GET /api/goals + Step 2 do goal-planner.md agora checa
  os dois caminhos.

Step 8 (AC9): já coberto pelo fix do bug_006 em 6a530cc
(/api/goals/{id}/recalculate e _recalculate_goal_value repontados pra
_recompute_goal_from_tickets). Confirmado por grep: nenhum outro leitor
backend ou frontend consome goal_progress_v/done_tasks/pct_complete — só a
própria VIEW (que a ADR manda manter) e os comentários explicando o freeze.

Verificação: py_compile limpo; tests/goals/+tests/tickets/ (57 passed, as
mesmas 3 falhas + 3 erros pré-existentes confirmadas sem relação com esta
mudança); novo tests/goals/test_step7_publish_decomposition.py (10/10,
cobre publish-gate parking, publish_intent=False bypass, publish_target
inválido, publish_intent ausente fail-closed, decomposition approve/reject,
e o teste crítico de R1 — mock em heartbeat_dispatcher.dispatch confirma
zero chamadas durante a aprovação de decomposition); fixture de teste
corrigida para nunca enviar Telegram real (delenv TELEGRAM_BOT_TOKEN/
TELEGRAM_CHAT_ID) após confirmar que a primeira versão vazou mensagens de
teste pro nexuslocalbot local.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… em silêncio se o loop falhar

pending_approvals.status e goals.decomposition_state já commitam 'approved'
ANTES do loop que cria os tickets do payload (CAS ponto sem volta — uma 2ª
decisão no mesmo approval_id dá 409, e o goal-planner se recusa a
redecompor um goal cujo decomposition_state já é não-nulo). Se o loop
morresse no meio (payload malformado do goal-planner, ex: "tickets"
serializado como string em vez de array), o resultado era aprovação
consumida pra sempre + zero tickets + ninguém avisado — perda silenciosa
de dados sem caminho de recuperação automático.

Fix: guarda isinstance(t, dict) pula item malformado sem perder o resto do
lote; try/except em volta do loop nunca engole a exceção — sempre manda
um alerta explícito via send_telegram_alert citando approval_id/goal_id e
propaga a exceção (o chamador, o bot do Telegram, já trata resposta não-200
mantendo os botões de aprovação na mensagem, ver bug_023). Alerta também
dispara se o payload tinha tickets mas nenhum sobreviveu à validação.

Testes novos: payload com entradas malformadas mistas mantém as válidas;
falha genuína no meio do loop dispara alerta com approval_id/goal_id na
mensagem, propaga a exceção, zero tickets ficam no banco. 12/12 em
tests/goals/test_step7_publish_decomposition.py; zero regressão na suíte
completa de goals+tickets (59 passed, mesmas 3 falhas + 3 erros
pré-existentes sem relação com esta feature).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…provider-routing

# Conflicts:
#	config/heartbeats.example.yaml
#	dashboard/backend/heartbeat_runner.py
…te-goal

Implementado autonomamente pelo Hermes Agent (gateway local, dispatch
próprio) enquanto esta sessão trabalhava em paralelo na goal-ticket-
unification — revisado a fundo linha por linha antes deste commit
(ver conversa: confirmação do Felipe de que o Hermes rodando em
paralelo é esperado).

## _run_publish_action agora publica de verdade (heartbeat_outcome.py)
- Chama a API pública do Postiz (POST /public/v1/posts) e faz POLLING em
  GET /public/v1/posts até confirmar state=PUBLISHED em todos os post ids
  antes de retornar published=True — nunca fabrica sucesso (o núcleo de
  segurança desta feature). ERROR/timeout continuam fail-closed.
- publish_content (texto exato) e publish_media (URLs) adicionados ao
  _OUTCOME_SCHEMA, obrigatórios quando publish_intent=true — nunca publica
  o resumo `result` por engano.
- Seleção de integração por plataforma falha fechado em ambiguidade (2+
  contas conectadas pro mesmo canal sem POSTIZ_INTEGRATION_*_ID configurado
  → recusa, nunca adivinha).
- _is_safe_publish_media_url: mídia só aceita HTTPS + host explicitamente
  listado em POSTIZ_ALLOWED_MEDIA_HOSTS — allowlist vazia = tudo recusado.
  Fecha um vetor de SSRF via URL controlada pelo agente/LLM.

## Isolamento de credencial (mesma lógica do V10)
- POSTIZ_API_KEY adicionado à denylist do env do subprocesso do agente nos
  dois caminhos (provider_fallback.py::_AGENT_ENV_DENYLIST_EXACT e o
  fallback inline em heartbeat_runner.py) — simétrico, nenhum agente lê a
  chave e publica direto contornando a aprovação.
- Bot do Telegram: timeout de decisão sobe de 15s pra
  APPROVAL_DECISION_TIMEOUT_SECONDS (105s default), coerente com o polling
  síncrono de até 90s no dashboard.

## AC2 fechado — create-goal via API (.claude/skills/create-goal/)
nexus_goal.py::create_goal agora chama POST /api/goals (com
DASHBOARD_API_TOKEN) em vez de INSERT sqlite direto — Goals de nível
superior criados pela skill humana documentada agora disparam
goal_created/goal-planner, igual à rota HTTP. Demais helpers de hierarquia
continuam DB-local (mission/project/task não precisam disparar nada).

## Infra de deploy (Postiz como stack separada no Portainer/Swarm)
- postiz-vps.stack.yml: reaproveita o postgres_postgres compartilhado do
  swarm (bootstrap idempotente via serviço one-shot), Redis dedicado leve,
  Temporal com Elasticsearch padrão (mantido supportado, dado que o disco
  do VPS já foi resolvido — prune de 34GB + upgrade de plano em andamento),
  limites de recurso em todo serviço, Traefik configurado pra
  post.workflowapi.com.br.
- scripts/bootstrap_postiz_database.sh: idempotente, SQL parametrizado,
  localiza o container do Swarm por label de serviço.
- docs/postiz-portainer-swarm.md: passo a passo de deploy em pt-BR.
- evonexus-vps.stack.yml: injeta as envs POSTIZ_*/APPROVAL_DECISION_TIMEOUT
  só nos serviços dashboard/telegram, onde são de fato consumidas.

## Verificação
19 testes novos/atualizados, todos passando:
tests/goals/test_step7_publish_decomposition.py (3 novos: confirma
PUBLISHED via polling, rejeita integração ambígua, rejeita mídia fora da
allowlist), tests/goals/test_create_goal_script.py (novo, valida payload
exato do POST /api/goals), tests/heartbeats/test_publish_env_isolation.py
(novo, confirma POSTIZ_API_KEY nunca chega no subprocesso do agente).

Suíte completa comparada contra o HEAD limpo (07ef424, via git stash):
exatamente as mesmas falhas pré-existentes em ambos os lados (mempalace,
plugins preview cache, telegram bot memory test, heartbeat schema seeds,
heartbeat runner timeout) — zero regressão nova introduzida por este commit.

Co-Authored-By: Hermes Agent <noreply@nousresearch.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mobile

Container forçava min-w-[1180px]/min-w-[1440px] mesmo em breakpoints que
não garantem essa largura de viewport — qualquer desktop/laptop entre
1280-1439px (faixa muito comum) ficava cortado com scroll horizontal
forçado. No mobile, empilhava as 6 colunas verticalmente (~3000px de
scroll só pra ver todos os status).

Fix (padrão validado pelo skill ui-ux-pro-max — Priority 5 Layout &
Responsive, anti-pattern "Fixed px container widths"):
- <xl (mobile/tablet): scroll horizontal com snap, uma coluna por vez
  (w-[85vw]), com espiada da próxima — evita o stack vertical gigante.
- xl+ (1280px): grid-cols-6 sem min-width forçado, colunas dividem o
  espaço disponível naturalmente — cabe em qualquer desktop comum.

Verificação: build + typecheck limpos. Testado visualmente via harness
estático com o CSS compilado real (Playwright screenshot, já que sem
backend local rodando pra autenticar) em 390px (mobile), 1280px e 1366px
(exatamente as larguras que cortavam antes — agora mostram as 6 colunas
completas sem scroll) e 1920px (aproveita o espaço extra sem ficar
esparso).

Instalado também .claude/skills/ui-ux-pro-max (NextLevelBuilder, MIT) —
biblioteca de design intelligence usada pra diagnosticar e validar este
fix, copiada de um projeto irmão (ZapCart/EVO-METHOD) pra ficar disponível
neste workspace também.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Feedback do Felipe: não dava pra distinguir Project de Goal, nem Mission
de Goal — e o pior, a página de Goals mostrava dados desatualizados.

## O problema real (não só visual)
Mission e Goal têm a mesma forma estrutural (target_metric/current_value/
target_value/due_date/status) — na UI liam como "a mesma coisa em
tamanhos diferentes". Project não tem métrica própria mas mostrava uma
barra de progresso agregada com a MESMA cor de destaque do Goal, reforçando
a confusão. Pior: a página de Goals expandia cada Goal mostrando
`goal_tasks` — a tabela legada, congelada desde o Step 8 da
goal-ticket-unification — nunca os tickets reais que o goal-planner de
fato cria. Resultado: criar um Goal, o goal-planner decompor em tickets
reais (visíveis no Kanban), e a própria página de Goals mostrar "nenhuma
task" — os parents/responsável/deadline/prioridade que o Felipe pediu pra
rastrear eram literalmente invisíveis.

## Fix
- Goals.tsx: busca tickets reais via GET /api/tickets?goal_id=X (endpoint
  que já existia) ao lado de goal_tasks; tickets aparecem primeiro e são
  o que renderiza por padrão, goal_tasks vira seção "(legado)" só quando
  não-vazia. TicketRow novo mostra prioridade (ícone, igual ao Kanban),
  responsável, deadline, e linka pra /tickets/{id}.
- Rótulos de nível explícitos: "Missão" (eyebrow no header, já tinha
  tratamento visual de faixa/gradiente), "Projeto" (eyebrow + progresso
  agregado agora em cinza neutro com o texto "agregado", não mais o verde
  de destaque que competia visualmente com o Goal), "Meta"/"Sub-meta" (pill
  inline na linha do Goal, distingue automaticamente sub-goals do
  goal-planner via parent_goal_id).
- PriorityBadge compartilhado entre TaskRow (legado, numérico) e TicketRow
  (label), normaliza os dois pro mesmo ícone visual.
- TicketDetail.tsx: novo bloco "Goal chain" na grid de metadados —
  ticket.goal_id nunca era mostrado em lugar nenhum antes disso. Busca
  lazy goal→project→mission (3 GETs simples) e renderiza o breadcrumb
  completo Mission › Project › Goal, clicável, de volta pra /goals.

## Agentes
Conferido .claude/agents/goal-planner.md — não referencia Mission/Project
em lugar nenhum (opera só na camada Goal→Ticket), terminologia já
consistente, nada a corrigir ali.

Verificação: build + typecheck limpos (2 rodadas, uma por página).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… mídia (Opção A)

Felipe queria configurar Postiz/MinIO pela UI em vez de mexer em env no
Portainer, e ligar o MinIO pro fluxo de publicação (Opção A: agentes sobem
a mídia e passam a URL pública).

## Cards de integração na UI (config sem Portainer)
- integrations.py: adiciona Postiz e "MinIO / S3 Media" ao registro
  INTEGRATIONS (first-class, rastreado no git — não custom-int gitignored que
  some no redeploy). Aparecem como cards em /integrações; preencher os campos
  grava no .env do volume e aplica na hora (load_dotenv override + os.environ,
  já existente no PUT /api/config/env). Zero Portainer, zero restart.

## Helper de upload pro MinIO (skill int-minio, rastreado)
- .claude/skills/int-minio/scripts/upload.py: boto3 (já no pyproject/uv.lock,
  presente nas duas imagens), path-style addressing pro domínio custom,
  content-type por extensão, sem ACL por padrão (bucket já é público via
  policy). Recebe um arquivo local, sobe pro bucket, imprime a URL pública
  https://s3.workflowapi.com.br/post/... na última linha pra captura em shell.
  Erros claros no stderr + exit codes.
- SKILL.md documenta o fluxo de publicação pros agentes publicadores.

## Wiring
- pixel-social-media.md: nova seção "Publishing" — gerar imagem → upload via
  int-minio → URL em publish_media → publish_intent/target/content → aprovação
  humana → dashboard publica via Postiz. Nunca marca como publicado sozinho.
- evonexus-vps.stack.yml: MINIO_* em dashboard + telegram + scheduler (qualquer
  processo que dispara agente precisa das chaves pro upload). Ao contrário do
  POSTIZ_API_KEY, MINIO_SECRET_KEY NÃO é denylisted dos agentes — eles precisam
  pra upload, e subir imagem num bucket não é publicar (o post no Instagram
  ainda exige aprovação). Documentado como o tradeoff consciente da Opção A.
- .env.example + docs/postiz-portainer-swarm.md: bloco MINIO_* + seção
  explicando UI vs Portainer e o fluxo de mídia.

Verificação: py_compile em integrations.py e upload.py OK; YAML do stack
válido; boto3 importa (1.42.88); helper testado nos caminhos de erro
(arquivo inexistente → exit 2, env faltando → mensagem clara). Não testei
contra o MinIO real pra não usar as credenciais em produção.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… = Mission→Wing/Project→Room

## Bug real: "não dava pra criar projeto" (Felipe)
/projects era explicitamente somente-leitura ("Read-only overview — click a
project to open it in Goals") — zero botão de criar, apesar de agora ser o
1º item do menu. CreateProjectModal (antes local em Goals.tsx, exigia
missionId obrigatório) virou componente compartilhado
(components/CreateProjectModal.tsx) com seletor de Missão opcional +
criação inline de Missão nova, usado tanto em ProjectsOverview.tsx (nova UI:
botão "Novo projeto" no header + estado vazio) quanto em Goals.tsx (mantém
o fluxo existente por Missão específica).

## Reordena o menu (Sidebar.tsx)
Grupo "operations": Projetos, Metas, Kanban, Agentes, Habilidades,
Heartbeats, Rotinas, Gatilhos, Atividades, Modelos — ordem pedida. Removido
"Tasks" do menu (as tarefas de trabalho real são tickets, visíveis no
Kanban desde a goal-ticket-unification; a rota /tasks continua existindo,
só não aparece mais na navegação).

## MemPalace: Mission→Wing, Project→Room (confirmado com Felipe)
Investigado o pacote mempalace real: hierarquia é Wing→Room→Drawer (Drawer
= chunk individual, não um conceito que se cria manualmente — descarta a
proposta original Mission=Drawer, que invertia cardinalidade: poucas
Missions, milhares de drawers). Direção corrigida: Mission é o contêiner
mais amplo dos dois lados (poucas), Project aninha dentro (como já aninha
dentro de Mission). Goal/Ticket não ganham nível próprio — MemPalace só tem
2 níveis navegáveis de fato.

- _mempalace_worker.py: ensure_mempalace_yaml ganha room_override — o mine
  de uma pasta nova escreve o mempalace.yaml já com o room nomeado pelo
  Project em vez do "general" genérico.
- mempalace.py: nova sync_project_source(workspace_folder_path,
  mission_title, project_slug) — upsert em sources.json (wing=Missão,
  room=Project) + patch idempotente do mempalace.yaml já existente SE ainda
  for o "general" auto-gerado intocado (nunca sobrescreve config
  hand-customizada com múltiplas rooms). Best-effort: nunca lança, uma
  falha do MemPalace não pode quebrar criação/edição de projeto.
- goals.py: create_project/patch_project chamam o sync depois do commit
  quando workspace_folder_path está setado.
- CreateProjectModal.tsx: novo campo opcional "Pasta do workspace".

Verificação: 5 cenários manuais cobrindo sync novo/reencontrado, geração de
yaml com room correto, patch idempotente do "general" antigo, preservação
de config customizada (múltiplas rooms), upsert sem duplicar — todos OK.
Suíte tests/backend/test_mempalace_routes.py comparada via git stash:
22 failed/21 passed idêntico antes e depois (pré-existente, sem relação).
py_compile + build do frontend limpos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, com aprovação humana

Generaliza o padrão já existente de goal-planner (Goal->Ticket com
aprovação Telegram) pros dois degraus acima: Mission->Project e
Project->Goal. Ver quick-spec em
workspace/development/features/ai-hierarchy-suggestions/ (gitignored).

## Novo: cascata com aprovação em cada degrau
Mission criada → project-planner sugere Projects → aprovação Telegram →
Projects criados → goal-suggester sugere Goals (pra cada Project) →
aprovação → Goals criados → goal-planner (já existente) sugere Tickets →
aprovação → Tickets criados. Cada degrau exige um novo tap humano — a
cascata nunca avança sem aprovação explícita, então não tem o mesmo risco
de recursão que o R1 (Raven) endereçou pro caso de re-wake sem aprovação.

## Schema (app.py, migração aditiva)
- pending_approvals ganha mission_id/project_id (FKs próprios — não
  reaproveita goal_id, que aponta pra goals(id) com ON DELETE CASCADE;
  um id de mission/project colidindo numericamente causaria cascade errado).
- gate_type CHECK ganha 'project_suggestion'/'goal_suggestion'. SQLite não
  altera CHECK constraint in-place — rebuild via rename+create+copy+drop,
  detectado por sniff do SQL vivo em sqlite_master (idempotente, sem flag
  de versão), preserva dados de instalações já rodando em produção.
- Espelhado em PendingApproval (models.py).

## Dois agentes novos, mesmo molde do goal-planner
- project-planner (wakes on mission_created): propõe 1-4 Projects.
- goal-suggester (wakes on project_created): propõe 2-6 Goals.
Ambos: idempotência (skip se o pai já tem filhos), nunca criam direto —
sempre via POST /api/approvals, enabled:false por padrão, listados em
STATE_MONITOR_AGENTS (zero-inbox, event-only).

## routes/goals.py
create_mission/create_project disparam mission_created/project_created
incondicionalmente (inclusive pra Projects/Goals criados pela própria
aprovação — é a cascata desejada).

## routes/approvals.py
POST /api/approvals aceita mission_id/project_id. decide_approval ganha 2
branches novos (project_suggestion, goal_suggestion) espelhando a mesma
disciplina do branch de decomposition já existente: isinstance guard +
skip de slug duplicado (não crasha o lote todo), try/except com alerta via
send_telegram_alert + re-raise (nunca perde dado em silêncio), e dispatch
em cascata só depois do commit dos filhos criados.

## heartbeat_schema.py
VALID_WAKE_TRIGGERS/WakeTrigger ganham mission_created, project_created.

## Verificação
9 testes novos (tests/goals/test_ai_hierarchy_suggestions.py): dispatch na
criação, approve/reject de cada gate_type, cascata verificada via mock de
dispatch, resiliência a payload malformado/slug duplicado, goal boolean sem
target_value, goal sem target_value e não-boolean é pulado, double-press
409. Todos passando. Suíte completa (goals+tickets isolados): mesma
baseline pré-existente (3 failed + 3 errors), 72 passed. Confirmado por
comparação com/sem stash que uma leitura combinada de múltiplos arquivos de
teste tem vazamento de estado pré-existente não relacionado a esta feature
(27 failed sem minhas mudanças vs 26 failed com — minhas mudanças não
pioram, só adicionam testes que passam). py_compile e validação
pydantic do heartbeats.example.yaml (incluindo os 2 heartbeats novos) OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oject

projects.status ganha CHECK constraint (estava sem, ao contrário de
missions/goals) com estado terminal 'completed'; missions/projects/goals/
goal_tasks ganham completed_at, goal_tasks ganha started_at. Quando todos os
Goals de um Project ficam terminais (achieved/cancelled), o Project é
auto-completado — implementado nos dois caminhos que podem levar um Goal a
'achieved' (patch_goal via ORM e _recompute_goal_from_tickets via SQL cru,
o caminho real de resolução de tickets). Migração idempotente via rename+
copy+drop, mesmo padrão já usado para pending_approvals.

.claude/rules/goals.md atualizado: removida a menção ao trigger
trg_task_done_updates_goal (dropado no goal-ticket-unification) em favor da
documentação real do mecanismo (_recompute_goal_from_tickets) e do novo
rollup de projeto.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…leigo

Adiciona seção "Como funciona, passo a passo" explicando a cascata
Missão→Projeto→Meta→Ticket, a sugestão automática por IA em cada degrau
(com aprovação humana), heartbeats, e um exemplo concreto de geração de
clientes via conteúdo em redes sociais publicado pelo Postiz. Corrige
também a menção desatualizada "Goal → Task" pra "Goal → Ticket" na seção
do que vem do upstream.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ix crítico de confiança nas aprovações do Telegram

Heartbeat de intervalo (6h) pro pixel-social-media que verifica, por Goal já
tocada por ele, se a fila de tickets de conteúdo está baixa (<3 open/in_progress)
e cria novos tickets concretos — sem agente novo, sem gate de aprovação novo
(mesmo precedente do goal-planner: decomposição de Goal humano-aprovado em
Tickets é confiável sem gate). pixel-social-media entra em STATE_MONITOR_AGENTS
porque o cost-guard pularia a invocação justamente no caso (fila vazia) que
esse heartbeat existe pra resolver.

Achado crítico da auditoria desta sessão sobre a UX de aprovação no Telegram,
corrigido:
- A aprovação de publicação mostrava outcome["result"] (resumo livre do
  agente) mas o Postiz publica outcome["publish_content"]/publish_media —
  campos diferentes. Um humano podia aprovar um resumo sem nunca ver o texto/
  mídia exatos que iam ao ar. Agora a mensagem mostra o conteúdo real.
- Aprovações de decomposition/project_suggestion/goal_suggestion dependiam
  100% do agente ter sido caprichoso no resumo em texto livre pra a lista
  real de itens propostos aparecer. Agora a lista estruturada (títulos) do
  payload é sempre renderizada, e o cabeçalho ganha a Missão/Projeto/Meta de
  contexto — não se perde o fio da meada com vários projetos do Sistema
  Britto em aprovação em paralelo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ada + operacional diário via WhatsApp + weekly review reativado

Achado de maior impacto: "Publicar Posts Sociais (X)" (config/routines.yaml,
enabled:true) falhava toda vez, silenciosamente — o scheduler procurava
scripts/publish_scheduled.py em ADWs/routines/custom/, caminho errado, sem
nenhum log visível fora do console. run_adw() agora tenta 3 localizações
(custom/<nome> → ADWs/routines/<nome> → scripts/<nome>) sem precisar mover
nenhum script (mover quebraria o path resolution ROOT-relative deles).

Operacional diário (Review Todoist, Email Triage, Sync Meetings, Dashboard
Consolidado): notificação agora é via WhatsApp (instância Evolution Go
"sistema-britto", não mais "nature" hardcoded) em vez de e-mail — decisão do
usuário. notifications.send_whatsapp() parametriza a instância;
ADWs/runner.py::run_skill() ganha notify_whatsapp simétrico ao
notify_telegram já existente.

Weekly Review reativado no scheduler.py — estava comentado alegando ter sido
substituído por uma entrada em routines.yaml que nunca existiu; sem ele,
nada verificava Goal/Ticket vencido. Heartbeat deadline-check (in-process,
zero custo de Claude) complementa checando a cada 4h, não só nas sextas.

.claude/rules/routines.md reescrito pra refletir a realidade: ~16 das ~20
rotinas antes documentadas como agendadas não têm script algum por trás —
agora marcadas como "invoque via skill manualmente" em vez de prometer
automação inexistente.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Antes, pending_approvals só existia como mensagem no Telegram — sem lugar
nenhum no dashboard pra consultar o que está pendente, principal lacuna
apontada pela auditoria (panorama 2026-07-17, item 1), ainda mais relevante
rodando vários projetos do Sistema Britto em paralelo.

Backend: GET /api/approvals (lista, filtro por status/gate_type) e
GET /api/approvals/<id> (detalhe, com o contexto de Missão/Projeto/Meta e a
lista estruturada de itens já usados na correção do Telegram). Decisão via
dashboard é um endpoint NOVO e deliberadamente separado do /decision
existente: POST /api/approvals/<id>/dashboard-decision exige sessão de
cookie real (current_user.role == "admin") e explicitamente rejeita
DASHBOARD_API_TOKEN — reabrir esse caminho pra qualquer chamada Bearer
reintroduziria o exato problema que o V1 original fechou (agente aprovando
seu próprio gate de publish/decomposição). app.py marca
g.auth_via_api_token = True só quando o login veio do token; o novo endpoint
checa essa flag antes de checar o role.

Frontend: nova página Approvals.tsx (lista com filtro de status, badges por
gate_type, link pro ticket/meta relacionado, aprovar/rejeitar com confirm
dialog), rota /approvals, entrada no menu (depois de Kanban) e chaves de
i18n nos 3 idiomas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…osts

Panorama 2026-07-17, item 2 — não existe hoje nenhum jeito de responder
"quanto custou a Missão X". O schema não liga custo (por rotina/heartbeat) a
um Ticket/Goal específico, então isso é uma ESTIMATIVA por alocação
proporcional: o custo total de cada agente é dividido pelos tickets desse
agente, e cada fatia soma na Missão/Projeto do Ticket correspondente — nunca
apresentado como ledger exato (campo methodology explica isso na resposta).

Bridging necessário: custo de rotina usa o alias curto do agente ("pixel",
extraído do docstring do script), enquanto Ticket.assignee_agent usa o slug
completo ("pixel-social-media") — resolvido por prefixo contra os slugs que
realmente aparecem em tickets, nunca por mapa hardcoded (evita drift se um
agente for renomeado). Prefixo ambíguo (2+ matches) ou custo de agente sem
ticket algum caem em unallocated_cost, nunca um palpite.

Frontend: novo card "Gasto por Missão / Projeto (estimado)" em Costs.tsx.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…slash command

Panorama 2026-07-17, item 5 (resto do achado #1 e evolution-foundation#19):

- 36 novos arquivos em .claude/commands/ (ex.: clawdia.md, apex.md, helm.md)
  — .claude/rules/agents.md documentava /clawdia, /apex, /helm etc. pra 38
  agentes, mas quase nenhum arquivo real usava o nome curto (só slug
  completo existia: clawdia-assistant.md, apex-architect.md...). Quem
  seguisse a doc recebia "command not found". Só /oracle e /lumen-learning
  já batiam com a doc, nada foi sobrescrito.

- GET /api/overview ganha needs_attention: heartbeats cuja ÚLTIMA execução
  falhou, tickets com lock vencido ainda visível (o janitor deveria ter
  limpado — se ainda aparece, é bug), aprovações pendentes há mais de 24h.
  Antes, responder "tem algo errado agora?" exigia visitar Heartbeats +
  Kanban + histórico de aprovações no Telegram separadamente. Card
  "Precisa de atenção" na Overview consolida os 3 num só lugar, com link
  direto pra cada item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dge + media-worker service

Ships the full brief -> HyperFrames render -> Postiz draft/schedule
pipeline: MediaJob state machine and REST API, a single PostizClient used
by both the new pipeline and the legacy ticket-publish flow, an isolated
media-worker Swarm service (OpenCode composition + HyperFrames render +
ffprobe validation, never inline in the dashboard web process), the
social-media-production skill, and dashboard UI (Mídias page, Postiz core
config card). Adds the media-worker matrix entry to the production
Docker Hub publish workflow (docker-publish-britto.yml) so the new image
builds alongside the existing runtime/dashboard ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was written alongside the social-media-production PR but never staged
before that PR merged — Pixel still only knew the legacy text/image
publish_intent flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants