Switched channelId from name-based lookup to the exact ID the user
pulled directly from Slack's channel details (C0BJNBTPFT5), eliminating
any ambiguity between similarly-named channels. Retested the real send:
still not_in_channel with the exact ID confirmed from Slack's own UI.
Diagnosed further with a temporary conversations.join attempt (same
credential, same channel) -- Slack returned a precise, actionable error:
"Your Slack credential is missing required Oauth Scopes: channels:join".
This confirms the token/credential itself authenticates correctly (not
a wrong-bot or wrong-workspace problem) -- the Slack App's OAuth scopes
are the real blocker: it has enough to attempt chat.postMessage but not
enough to self-join a channel, and per the earlier not_in_channel result,
also isn't a member and chat:write.public isn't granted either. This is
a Slack App admin-console change (api.slack.com/apps -> OAuth &
Permissions -> add channels:join and/or chat:write.public -> reinstall
app), not something fixable from n8n. Reported to the user with the
exact scope names needed.
Reverted SEND_ENABLED to false, workflow deactivated, both temp nodes
removed, failed test rows cleared from coordenador_glpi_envios_digest
per the user's request.
158/158 tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two more real test-send attempts (both with the user's standing
authorization), each confirming a bit more of the pipeline and each
correctly caught/recorded/surfaced by the failure branch -- no message
ever actually reached Slack, exactly as the guard is designed to behave
on failure:
1. channel "CHAMADOS SUPORTE" (as literally given) -> channel_not_found
(Slack channel identifiers are always lowercase-hyphenated).
2. channel "chamados-suporte" -> not_in_channel: the channel resolved
correctly this time, but the Bot Coordenador app is not yet a member
of that channel (chat.postMessage requires channel membership without
the chat:write.public scope). This is a Slack-side action for the
user (invite the bot to the channel), not fixable from n8n config.
Updated the channel value to the confirmed-correct slug. Reverted
SEND_ENABLED to false, workflow deactivated, temp webhook removed.
158/158 tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The user provided the real, dedicated Slack destination for this
automation (a separate Bot app credential + its channel), correcting my
earlier inference (the shared "Slack account" credential + $vars.
SLACK_CHANNEL_TECNICO, borrowed from an unrelated archived workflow,
which Slack's own API rejected as invalid_arguments in the prior test).
Created a new slackApi credential ("Slack - Bot Coordenador (Chamados
Suporte)", id lNqwBBrtLfkRqw2X) via n8n_manage_credentials -- the token
was created directly through the n8n credentials API and never written
to any file, workflow parameter, or commit; only the credential's id/name
reference (not a secret) is stored in the workflow, same as every other
credential in this project. Slack - Enviar Mensagem now targets channel
"CHAMADOS SUPORTE" by name instead of the unresolved variable.
158/158 tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Classifier before/after (real execution 14404), critical-case table for
the 7 reincident tickets (technician-vs-vendor and contact-vs-waiting
fixes confirmed on real data), Top10 critical inspection, Slack
mechanism confirmation (real test send attempted with explicit user
authorization -- pipeline confirmed working end-to-end up to Slack's own
API, which rejected $vars.SLACK_CHANNEL_TECNICO as invalid), Schedule
Trigger confirmation, test/validation summary, production state
confirmation. Verdict: PRONTO PARA ATIVAÇÃO — BLOQUEADO APENAS POR
ACESSO (Slack channel configuration only, not code or n8n access).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found running the real Slack test send (with the user's explicit
confirmation to try SEND_ENABLED=true once): the Slack node itself fired
correctly and got a real response back from Slack's API ("invalid_arguments"
-- confirms the pipeline mechanics, credential and node config work end
to end up to that point). Separately, once the failure branch tried to
record it, "Data Table - Registrar Envio Digest (Falha)" crashed with
"Cannot convert undefined or null to object" inside n8n's own insert
operation -- both new envio-digest insert nodes had operation:"insert"
and dataTableId but no "columns" mapping, which is a required property
for insert/update/upsert (confirmed via get_node nodes-base.dataTable).
Fixed both nodes with the same columns.mappingMode:"defineBelow" shape
already used by the existing "Data Table - Registrar Shadow Run" node.
Added a regression test scanning every dataTable node with an insert/
update/upsert operation for a columns.mappingMode.
158/158 tests passing.
Separately (not fixed here, needs the user): the actual Slack
"invalid_arguments" error suggests $vars.SLACK_CHANNEL_TECNICO isn't
resolving to a valid channel on this n8n instance -- no tool available
to this agent can list/inspect n8n instance Variables to confirm. Left
SEND_ENABLED=false and the workflow inactive pending the user's input on
the correct channel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found running the real validation execution against DEV: the envios
digest table starts empty (the normal state for every day until the
first successful send), the "get" operation returned 0 rows, and n8n
does not execute downstream nodes when a node outputs 0 items -- so
Code - Guard de Envio Slack, the IF, the real Slack node, and everything
else in the send branch silently never ran. This would have broken the
happy path on every single day in production, not just an edge case.
Fixed with alwaysOutputData: true on that node (forces at least one
output item even with 0 matches; the resulting empty {} item doesn't
false-positive in alreadySentSuccessfully since none of its fields match
a real record's shape). Re-ran the validation execution after the fix:
Code - Guard de Envio Slack now runs correctly (shouldSend: false,
reason: 'send_disabled' with SEND_ENABLED=false as expected), and the
IF node's true branch (Slack - Enviar Mensagem) is confirmed to never
fire while disabled.
Added a regression test asserting alwaysOutputData on this node.
157/157 tests passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>