Implements the actual send branch: Selecionar Top N -> Montar Mensagem ->
Guard de Envio -> Slack real -> registrar resultado (only after
confirmed success/failure) -- previously simulated/absent.
logic/slack.js (new): shouldSendByConfig (SEND_ENABLED gate, defaults
false), computeMessageHash (djb2Hash of the exact message text, reused
from logic/hash.js), alreadySentSuccessfully (idempotency check: only a
prior status='success' record for the same report_date+run_type+
message_hash blocks a resend -- a prior 'failed' record does not, so
retries after a real failure aren't stuck), evaluateSendGuard (single
auditable decision combining both), buildEnvioDigestRecord.
logic/slack.test.js: 13 tests covering all of the above, explicitly
labeled against the mission's required tests #13-#16.
New node drivers:
- guard-envio-slack.node.js: reads the digest + recent envio records
(fetched by report_date), computes the send decision.
- Slack node itself (n8n-nodes-base.slack, native, not a raw webhook):
reuses the real "Slack account" credential (slackApi) and
$vars.SLACK_CHANNEL_TECNICO channel variable already used by this
org's own "Notificacao de Erros Globais" workflow on the same n8n
instance -- V1's own Slack webhook URL was never actually deployed
(its Config value is literally "SEU/WEBHOOK/AQUI", same class of
placeholder as the old GLPI URL from the prior cycle), so this reuses
the one REAL, credentialed Slack mechanism that exists in this
environment rather than inventing a new destination. onError:
continueErrorOutput splits success/failure into two branches.
- registrar-envio-sucesso.node.js / registrar-envio-falha.node.js: build
the success/failure record; only the success path continues to the
existing per-ticket send-counter logic (rewired below).
- lancar-erro-envio.node.js: after the failure record is written,
explicitly throws so the n8n execution shows status=error (never
silently swallow a send failure).
- pular-envio.node.js: terminal no-op for the "should not send" branch
(SEND_ENABLED=false or already sent today), for observability only.
n8n/nodes/prepare-envio-record.node.js: rewired to read the Top10
selection by name ($('Code - Selecionar Top 10').all()) instead of
$input, since it now runs downstream of the Slack-success registration,
not directly off Selecionar Top10 -- registering per-ticket send counts
before Slack ever confirmed anything was the exact "don't register
before confirmation" bug the mission called out.
logic/message.test.js: added a full-Top10 message-length regression test
(mission requirement: message stays short, no leaked JSON/score/evidence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real bug confirmed in the prior cycle's Run A: WAITING_VENDOR tickets
entered the Top10 solely because contextChanged=true (a fresh AI call
happened this run) -- contextChanged should only decide whether to
reanalyze (logic/cache.js), never whether an already-still-blocked
ticket can compete.
logic/cooldown.js: removed the `&& !contextChanged` condition from the
BLOCKING_CLASSIFICATIONS check in computeCooldown -- a ticket whose
CURRENT classification (fresh or cached) is in BLOCKING_CLASSIFICATIONS
is now excluded unconditionally. Also expanded BLOCKING_CLASSIFICATIONS
to the full list required this cycle: WAITING_REQUESTER, WAITING_VENDOR,
WAITING_APPROVAL, SCHEDULED, DEPENDENCY_BLOCKED, PROJECT_LONG_TERM,
DUPLICATE_OR_OBSOLETE (WAITING_APPROVAL/PROJECT_LONG_TERM/
DUPLICATE_OR_OBSOLETE previously only had a score penalty, not exclusion
-- WAITING_APPROVAL has positive base score points, so it could
genuinely win a Top10 slot before this fix). The critical-override escape
hatch (SLA vencido + prioridade alta, etc.) is untouched -- still the
only way a blocked classification can surface.
logic/cooldown.test.js: rewrote the one test that was actually asserting
the buggy behavior ("mudanca de contexto real desbloqueia mesmo em
estado de espera" -> now proves the opposite, with a comment explaining
why); added tests for the chronology-driven unblock case (ticket moves
out of a blocking classification entirely) and the two newly-blocking
classes.
logic/score.test.js: added end-to-end tests through computeScore()
proving WAITING_VENDOR and WAITING_APPROVAL are excludedFromTop10 even
with contextChanged=true.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Production-readiness cycle. Classifier was excessively conservative (real
Run A: ACTIONABLE_NOW/UNASSIGNED_ACTIONABLE both 0) and made concrete
semantic mistakes on real tickets:
- #34419: technician "Rafael Brito" (internal) classified as WAITING_VENDOR
waiting_for -- an internal technician is not a vendor.
- #37389: ticket had concrete open tasks ("Realizar um inventário das
máquinas", "Alinhar o dia...") but was classified INSUFFICIENT_CONTEXT.
- Generic next_action ("Revisar chamado") repeated across unrelated
tickets, proving nothing about real actionability.
Rewrote the system prompt (n8n/nodes/prepare-ai-prompt.node.js) with:
practical actionability definition ("suporte consegue fazer algo util
agora", explicit inclusion list); contatar-vs-aguardar distinction with
examples; WAITING_* requires positive evidence (action already taken AND
external dependency still open); technician-vs-vendor distinction citing
the TECNICO context field; STATUS Planejado alone != blocked/scheduled;
SCHEDULED requires an explicit date/window; mandatory chronological
interpretation (most recent apontamento wins over a superseded old
blocker); next_action must be concrete, with explicit accepted/rejected
examples; INSUFFICIENT_CONTEXT bar raised (ask "is there a diagnostic
step available even without full info" first).
n8n/nodes/prepare-ai-prompt.node.test.js: 10 new tests asserting each
rule's text is still present in the prompt (deterministic proxy --
behavioral proof comes from the real DEV run, inspected manually).
logic/classification.js: added AI_CLASSIFIER_VERSION=3 (stamped on every
valid classification); added a deterministic safety net (not just a
prompt hope) -- isWaitingForInternalTechnician() rejects WAITING_VENDOR
when waiting_for matches the ticket's own TECNICO field, routing to a new
fallback_reason 'ai_semantic_conflict' instead of accepting a known-wrong
semantic classification; added next_action genericness rejection
(isGenericNextAction) for ACTIONABLE_NOW/UNASSIGNED_ACTIONABLE only.
n8n/nodes/validate-ai-response.node.js: now passes the ticket into
validateAiClassification() so the technician guard has the facts it needs.
logic/cache.js: added buildCacheKey(contextHash, classifierVersion) ->
"<hash>:v<version>", stored in the same last_context_hash column (no
schema migration) -- bumping AI_CLASSIFIER_VERSION now forces a cache
miss on every ticket without manually clearing the Data Table.
n8n/nodes/decide-needs-ai.node.js / prepare-memoria-analise-record.node.js
updated to compute/persist the composite key.
logic/config.js: AI_MAX_CANDIDATES default raised 30->60 (now equals
CANDIDATE_POOL_SIZE) -- no candidate should go unanalyzed by an
artificial cap now that Run B already proved the cache makes repeat runs
cheap; AI_FALLBACK stays reserved for real provider failures. Also added
SEND_ENABLED (default false) and ENVIRONMENT config fields for the real
Slack send gate (used by logic/slack.js, next commit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
buildDataQualityReview extracts an auditable record per INSUFFICIENT_CONTEXT
ticket (ticket_id, title, ai_reason, context_size, has_description,
followup_count, task_count, has_solution, last_interaction_at) -- not sent
anywhere, just for calibration. Also tightened validateAiClassification to
reject an empty/whitespace-only next_action (was previously accepted).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
applyFallbackPolicy (new, in logic/cooldown.js alongside the isCriticalOverride
it reuses) is now the single place that decides whether an AI_FALLBACK or
INSUFFICIENT_CONTEXT ticket can enter the Top10: blocked by default, let
through only via an explicit critical override (SLA vencido + prioridade/
urgencia alta, OR sem_tecnico + prioridade/urgencia alta -- isCriticalOverride
extended to cover both, previously only covered SLA+priority). score.js
calls this policy and sets excludedFromTop10/exclusionReason/decisionReason
accordingly, with an auditable *_critical_override or *_no_override reason.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fallback (ai_disabled, ai_error, invalid_json, ai_max_candidates_cap --
any candidate the AI never actually analyzed) now produces classification
'AI_FALLBACK', an explicit technical state that is never part of the
11-class enum sent to OpenAI (the AI itself can never legitimately return
it). Previously it silently claimed ACTIONABLE_NOW with confidence 0,
which made unanalyzed tickets indistinguishable from genuinely confirmed
ones at the classification-label level.
Also normalizes blocker_type/waiting_for when the model returns the
literal string "null"/"none"/"n/a"/"" instead of JSON null -- done
once, in the validation layer (logic/classification.js), not scattered
around the workflow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
context.normalizeTicket now runs normalizeGlpiText on titulo, descricao
(new field, sourced from glpi_tickets.content -- never fetched before),
and every followup/task texto. buildAiContextText restructured into
delimited sections (TITULO / DESCRICAO ORIGINAL / STATUS-TECNICO-GRUPO /
FOLLOW-UPS / TAREFAS) instead of one flat string.
hash.buildContextFingerprint now includes titulo/descricao and the
normalized followup/task texto (previously only id/date/status), so:
(a) the same content encoded differently (HTML vs escaped entities)
produces the same hash, and (b) editing a followup's actual content
(same id/date) is now detected as a real context change -- previously
silently invisible to the hash.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran the deployed workflow for real (temporary webhook trigger, briefly
activated, immediately deactivated and removed after each attempt) against
the real GLPI MySQL database and a real OpenAI credential. Found and fixed:
1. $env access is blocked on this n8n instance (N8N_BLOCK_ENV_ACCESS_IN_NODE).
parseConfig($env) threw "access to env vars denied" on the very first
Code node. Config now comes from the "Config - Coordenador GLPI v2" Set
node instead (same pattern the V1 workflow already used) -- expanded
that node with all 13 tunable parameters, and pointed the 3 affected
node drivers (build-sql-candidatos, decide-needs-ai, select-top10) at
it instead of $env.
2. Data Table "upsert" operation requires both `columns` (values) and a
separate `filters` (match condition) -- I had only set columns with
matchingColumns, which isn't enough; n8n refused to activate the
workflow ("Missing or invalid required parameters: filters"). Added
filters.conditions matching on ticket_id to both memory-writing Data
Table nodes.
3. Real architecture bug: "Data Table - Buscar Memoria" sat inline in the
41-candidate chain (Mesclar Contexto -> Buscar Memoria -> Decidir
Necessidade de IA). A bulk "get all rows" Data Table node run with 41
input items produced 41 identical fetches and its OWN output (the empty
memory table on a first run) replaced the 41 candidates entirely --
"Decidir Necessidade de IA" received 0 items and the whole main branch
silently skipped. Moved the memory fetch to a parallel branch off
Config (fires once) and added a real Merge node ("Merge - Aguardar
Memoria") to guarantee both the candidates and the memory rows have
executed before "Decidir Necessidade de IA" runs -- same synchronization
pattern already used for context and for the shadow comparison.
Also fixed a cosmetic bug found in the real message output: buildTopMessage
produced a quadruple newline between the header and the first ticket block
([header, '', ...blocks].join('\n\n') double-counts the blank line already
inserted by the '\n\n' join). Added a regression test.
Verified with a real execution (id 13599, 24/08/2026 19:25 UTC) against the
live GLPI database: 41 real candidates, 30 sent to OpenAI (capped), 11
fallback-capped, memory table populated with 41 real rows including
genuine WAITING_VENDOR/WAITING_REQUESTER classifications with real
blocker_type/waiting_for fields extracted from real ticket task content,
and a real shadow_runs row comparing Legacy x V2 (4 tickets removed by V2:
2 for INSUFFICIENT_CONTEXT, 1 WAITING_REQUESTER, 1 WAITING_VENDOR; 4 added).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fixed 3 remaining "return [...].join(...)" false-positive triggers in
logic/message.js (n8n-mcp's static validator flags any inner return-array
pattern as if it were the node's own output shape) by extracting each to
a named variable before joining. All 3 occurrences of the pattern lived
inside buildTopMessage/buildCoordinationDigest, which get fully inlined
into both "Code - Montar Mensagem Curta" and "Code - Montar Digest
Coordenacao" (whichever function is actually called, the whole module is
inlined), so both nodes needed the same fix.
- Deployed [DEV] Coordenador GLPI - Chamados Abertos v2 (Shadow)
(id y6JFEnp7Z2IJEUe2) to devn8n.gtigrupo.com.br: 36 nodes, inactive,
n8n_validate_workflow reports valid:true (0 errors, 1 minor deprecation
warning on continueOnFail).
- Ran n8n_autofix_workflow: 6 safe typeVersion upgrades applied and mirrored
back into n8n/workflow.template.json so future rebuilds stay in sync.
- Found and fixed a real safety issue: creating the workflow before the
"MySQL GLPI - Coordenador" credential existed caused n8n to silently
auto-bind the 3 MySQL nodes to the only other mySql-type credential in
the instance (an unrelated Asaas billing credential). Cleared those
credential bindings (both live and in the template) rather than leave
them pointing at the wrong database.
- Created the two Data Tables the pipeline needs: coordenador_glpi_memoria
(17 columns) and coordenador_glpi_shadow_runs (6 columns).
- Documented the pending schema introspection (glpi_tickettasks.state
column name unverified, blocked on the MySQL credential) and an unrelated
discovery (two other Data Tables, dev_glpi_v2_insights_memory and
dev_glpi_v2_run_history, appeared on the DEV instance mid-session from a
source other than this one) in docs/superpowers/notes/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>