n8n/workflow.template.json:
- Added the 10 new nodes for the real Slack send branch (Data Table -
Buscar Envios Recentes, Code - Guard de Envio Slack, IF - Deve Enviar
Slack?, Slack - Enviar Mensagem, Code - Pular Envio Slack, Code -
Registrar Envio Sucesso/Falha, the two Data Table - Registrar Envio
Digest nodes, Code - Lancar Erro Envio Slack) and wired every
connection -- Code - Montar Digest Coordenacao now fans out to both
the existing shadow-diff branch (unchanged, always runs) and the new
send branch in parallel, so a Slack failure never loses the shadow
audit trail.
- Renamed "Schedule Trigger - Dias uteis 08h (desligado em DEV)" ->
"Schedule Trigger - Dias uteis 08h": it was already wired to Config
and already had the correct cron (0 8 * * 1-5) from earlier in the
project -- this cycle didn't need to build it, just stop calling it
"desligado" now that it's genuinely production-track (activation is
controlled by the workflow's own active flag, not the node's name).
- Added settings.timezone: "America/Sao_Paulo" explicitly (previously
implicit/unset) -- matches the timezone already hardcoded into
buildTopMessage's dateStr computation elsewhere in this codebase, not
a new assumption.
- Config node: AI_MAX_CANDIDATES 30->60, added ENVIRONMENT/SEND_ENABLED.
tools/build-workflow.test.js: 10 new tests (connections integrity after
a node rename -- would have caught a real bug this cycle where renaming
the Schedule Trigger orphaned its connection key; Schedule Trigger wired
+ correct cron/timezone; Manual Trigger still present; real Slack node
exists with the right credential/onError; guard runs before Slack;
per-ticket envio registration only fires after the success Data Table
write; Slack failure always registers before throwing; AI_MAX_CANDIDATES
== CANDIDATE_POOL_SIZE; SEND_ENABLED/ENVIRONMENT present; GLPI SQL/query
scan for INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/TRUNCATE; dataTable node
operation values are valid enum members -- caught a real bug, "getRows"
[the MCP tool's action name] isn't a valid node parameter value, it's
"get"). Replaced the narrow normalize.js-specific transitive-dependency
test with a general one that scans every logic/*.js top-level function
and asserts any node calling it also has it inlined -- this caught a
second real instance of the same bug class (slack.js's use of hash.js's
djb2Hash, missing from 3 new node headers) before it reached a live run.
n8n/deployed/coordenador-glpi-v2.n8n.json regenerated (43 nodes).
156/156 tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Live DEV sync completed (all 11 changed nodes pushed, including the
transitive-dependency fix), then two real executions against production
GLPI in shadow mode:
- Run A (execution 14259): 45 candidates, 30 real AI calls (0 errors),
15 correctly routed to AI_FALLBACK instead of fake ACTIONABLE_NOW,
3 critical overrides. ACTIONABLE_NOW/UNASSIGNED_ACTIONABLE still 0 --
reported honestly, not forced.
- Run B (execution 14263, ~1 min later, no data changes): 0 AI calls,
45/45 cache hits, cached AI_FALLBACK items correctly stayed fallback
(proves the cache is_fallback bug fix), Top10 correctly shrank
10->5 as WAITING_VENDOR/WAITING_REQUESTER items hit cooldown.
Full reincident-ticket table (7 tickets) with real evidence, real
before/after HTML-normalization examples (tickets #37389, #36530),
shadow diff vs legacy, webhook-debug-removed confirmation, production-
untouched confirmation. Verdict: CONTINUAR SHADOW MODE.
Workflow y6JFEnp7Z2IJEUe2 ends this cycle at active:false, 33 nodes,
temp webhook removed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while syncing this cycle's fixes to the live DEV workflow and
preparing Run A: tools/build-workflow.js's readLogicSource() only inlines
whatever a node's own `@requires` header lists -- it does not resolve
transitive dependencies. context.js started doing `require('./normalize')`
earlier this cycle, but the three node drivers that need normalizeTicket/
buildAiContextText (which call normalizeGlpiText internally) still only
declared `@requires context.js`, never `normalize.js` itself.
The result compiles fine (new Function(...) syntax-checks pass -- a
ReferenceError is a runtime error, not a syntax error) but would throw
"normalizeGlpiText is not defined" the moment any of these three nodes
actually ran:
- Code - Mesclar Contexto e Calcular Hash (merge-context-hash.node.js)
- Code - Normalizar Candidatos Base (normalize-candidates.node.js)
- Code - Preparar Prompt IA (prepare-ai-prompt.node.js)
This never surfaced in Run 1 (prior session) because normalize.js/the
descricao field didn't exist yet at that point, and it didn't surface in
this cycle's local test suite because node--test only exercises the pure
logic/*.js modules directly (with real requires), never the inlined,
concatenated jsCode a node actually runs in n8n.
Fixed by adding normalize.js explicitly to each of the three headers,
matching the codebase's existing convention for multi-file deps (e.g.
compute-score.node.js already lists `@requires cooldown.js score.js`
together instead of relying on any transitive resolution).
Added a regression test that scans every generated Code node for calls to
normalizeGlpiText/normalizeTicket/buildAiContextText and asserts the
normalizeGlpiText function definition is actually present in that same
inlined source -- would have caught this before it ever reached DEV.
102/102 tests passing (was 101/101 -- +1 new regression test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Real bug found by code review while implementing this cycle: "Code -
Reusar Classificacao em Cache" always hardcoded is_fallback: false when
reconstructing a classification from memory, even when the cached
classification was itself AI_FALLBACK. On a cache hit for a previously
fallback-classified ticket, this would have let it skip
applyFallbackPolicy's exclusion entirely (the policy checks
aiClassification.is_fallback first). Fixed to derive is_fallback from
the cached ai_classification value.
- select-top10.node.js now carries `resolucao` (ia | cache | fallback_cap)
through to the scored item, previously dropped -- needed to tell cache
hits apart from real AI calls in observability.
- Code - Comparar Legacy x V2 now computes the full observability summary
(candidate_count, cache_hits/misses, ai_requested/success/errors,
ai_fallback_count and by-reason breakdown, classification_counts,
insufficient_context_count, actionable_real_count,
critical_override_count, top10_count, blocked_count,
coordination_count) and the data-quality review (buildDataQualityReview
over INSUFFICIENT_CONTEXT tickets), both stored in the shadow_runs
`summary` JSON column -- no new Data Table needed.
- Removed the debug webhook entirely (Debug - Ver Painel, Data Table -
Buscar Shadow Runs Recentes debug branch, Code - Montar Painel Debug,
Debug - Responder) and the now-unused debug-panel.node.js driver.
Workflow went from 37 to 33 nodes. Debugging goes through n8n execution
data, the Data Tables, and the shadow diff instead, as requested.
- Fixed glpi_base_url: was still V1's own placeholder
(https://glpi.suaempresa.com.br, never a real value, not report
anonymization) -- confirmed the real domain (https://glpi.gtigrupo.com.br)
from the live, active GLPI x WhatsApp workflow's own GLPI API config in
the same n8n instance. SQL still only ever receives it through
{GLPI_BASE_URL} substitution from the Config node, never hardcoded.
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>
- 14 new Code node drivers implementing the pipeline stages: normalize
candidates, build context SQL, merge context + hash, decide AI need,
prepare AI prompt, validate AI response, AI-disabled fallback, reuse
cached classification, compute score, select top 10, prepare memory/
envio records, build message/digest, compare legacy x V2, debug panel.
- Consolidated the two context queries (followups/tasks) into a single
UNION ALL query tagged by tipo, and added a "Merge - Unir Shadow"
synchronization node before comparing legacy x V2: n8n does not
guarantee relative execution order between sibling branches that only
share an early common ancestor, so cross-branch data was only safe to
read by name once a real Merge node forced both to complete first.
- Reordered score calculation before the memory upsert (score is needed
in the persisted row) and turned both Data Table writes into
side-effect branches that fan out from the main path instead of
gating it, so the Data Table node's own output shape never needs to
carry the full pipeline state forward.
- Fixed a duplicate-identifier bug in the build script: score.js's
require('./cooldown') survived inlining and collided with
cooldown.js's own computeCooldown function in the same Code node
scope. build-workflow.js now strips inter-module requires when
inlining, and a new test syntax-checks every generated jsCode string
plus asserts no leftover require() calls.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/superpowers/specs/2026-08-24-coordenador-glpi-v2-design.md
Covers Etapa 0 recovery findings, architecture, data model, AI schema,
scoring, message format, shadow mode. Approved by user in chat.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Snapshot of the pre-existing, working V1 automation (n8n workflow +
SQL query + docker-compose + README) exactly as found on disk, before
any V2 changes. .env is intentionally not tracked (gitignored,
contains local secrets).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>