agent-master-devsecops 0.0.0-stage → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorignore +23 -0
- package/README.md +371 -3
- package/agents/AGENT_DEVSECOPS.md +417 -0
- package/agents/AGENT_ORCHESTRATOR.md +264 -0
- package/agents/AGENT_QA.md +186 -0
- package/agents/AGENT_SCRUM.md +190 -0
- package/agents/AGENT_SRE.md +173 -0
- package/agents/AGENT_UIUX.md +162 -0
- package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +53 -0
- package/docs/ARCHITECTURE.md +148 -0
- package/docs/CALCULATIONS.md +17 -0
- package/docs/CHANGELOG.md +49 -0
- package/docs/ENGINEERING_RULES.md +252 -0
- package/docs/IDEAS.md +17 -0
- package/docs/STATE.md +43 -0
- package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -0
- package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -0
- package/package.json +47 -6
- package/scripts/a11y_auditor.js +187 -0
- package/scripts/ast_skeletonizer.js +220 -0
- package/scripts/atlassian_client.js +238 -0
- package/scripts/atlassian_sync_action.js +269 -0
- package/scripts/bootstrap_cli.js +468 -0
- package/scripts/circuit_breaker.js +330 -0
- package/scripts/code_quality_auditor.js +142 -0
- package/scripts/confluence_sync.js +337 -0
- package/scripts/context_compressor.js +139 -0
- package/scripts/contract_test_validator.js +166 -0
- package/scripts/credentials_guard.js +216 -0
- package/scripts/domain_idea_miner.js +364 -0
- package/scripts/eval_harness.js +100 -0
- package/scripts/fast_feature_scaffold.js +338 -0
- package/scripts/git_hygiene_linter.js +186 -0
- package/scripts/hot_state_cache.js +95 -0
- package/scripts/jira_manager.js +408 -0
- package/scripts/magic_prompt_generator.js +205 -0
- package/scripts/mcp_server.js +1884 -0
- package/scripts/multi_agent_orchestrator.js +258 -0
- package/scripts/otel_tracer.js +228 -0
- package/scripts/perf_benchmark.js +153 -0
- package/scripts/pr_reviewer.js +373 -0
- package/scripts/sast_scanner.js +180 -0
- package/scripts/sca_dependency_auditor.js +167 -0
- package/scripts/smart_onboarding.js +610 -0
- package/scripts/sre_remediation_engine.js +174 -0
- package/scripts/universal_packager.js +313 -0
- package/scripts/web_inspector.js +355 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Event Bus & Orchestration Engine
|
|
3
|
+
* Topologia de cooperação determinística, barramento de eventos lógicos
|
|
4
|
+
* e handovers automáticos entre os 6 modos da suíte multi-agente.
|
|
5
|
+
* Conforme especificado em docs/ARCHITECTURE.md.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { SECURITY_RULES } = require('./sast_scanner.js');
|
|
11
|
+
|
|
12
|
+
// Definição canônica dos 6 modos / personas oficiais
|
|
13
|
+
const AGENT_PERSONAS = {
|
|
14
|
+
ORCHESTRATOR: 'modo: orchestrator',
|
|
15
|
+
UIUX: 'modo: uiux',
|
|
16
|
+
DEVSECOPS: 'modo: devsecops',
|
|
17
|
+
QA: 'modo: qa',
|
|
18
|
+
SCRUM: 'modo: scrum',
|
|
19
|
+
SRE: 'modo: sre',
|
|
20
|
+
FULL_DEV: 'modo: full-dev'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Definição canônica dos eventos de barramento
|
|
24
|
+
const EVENT_TYPES = {
|
|
25
|
+
EVENT_REQUIREMENTS_READY: 'EVENT_REQUIREMENTS_READY',
|
|
26
|
+
EVENT_DESIGN_READY: 'EVENT_DESIGN_READY',
|
|
27
|
+
EVENT_UI_REVIEW: 'EVENT_UI_REVIEW',
|
|
28
|
+
EVENT_CODE_COMPLETE: 'EVENT_CODE_COMPLETE',
|
|
29
|
+
EVENT_REVIEW_FAILED: 'EVENT_REVIEW_FAILED',
|
|
30
|
+
EVENT_REVIEW_PASSED: 'EVENT_REVIEW_PASSED',
|
|
31
|
+
EVENT_METRIC_ANOMALY: 'EVENT_METRIC_ANOMALY',
|
|
32
|
+
EVENT_REFACTOR_PROPOSAL: 'EVENT_REFACTOR_PROPOSAL',
|
|
33
|
+
EVENT_AGENT_SELF_IMPROVE: 'EVENT_AGENT_SELF_IMPROVE',
|
|
34
|
+
EVENT_KANBAN_DISPATCH: 'EVENT_KANBAN_DISPATCH'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
class MultiAgentOrchestrator {
|
|
38
|
+
constructor() {
|
|
39
|
+
this.history = [];
|
|
40
|
+
this.subscribers = new Map();
|
|
41
|
+
this._setupDefaultRoutes();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_setupDefaultRoutes() {
|
|
45
|
+
// Rota 0A: EVENT_REQUIREMENTS_READY (Orchestrator/Scrum -> UIUX)
|
|
46
|
+
this.on(EVENT_TYPES.EVENT_REQUIREMENTS_READY, (payload) => {
|
|
47
|
+
return {
|
|
48
|
+
handoffTo: AGENT_PERSONAS.UIUX,
|
|
49
|
+
status: 'READY_FOR_DESIGN',
|
|
50
|
+
reason: 'Requisitos funcionais homologados; necessário design visual e acessibilidade.',
|
|
51
|
+
nextAction: 'UIUX elabora jornada, tokens e especificações WCAG 2.1 AA.',
|
|
52
|
+
requiresFix: false
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Rota 0B: EVENT_DESIGN_READY (UIUX -> DevSecOps)
|
|
57
|
+
this.on(EVENT_TYPES.EVENT_DESIGN_READY, (payload) => {
|
|
58
|
+
return {
|
|
59
|
+
handoffTo: AGENT_PERSONAS.DEVSECOPS,
|
|
60
|
+
status: 'READY_FOR_IMPLEMENTATION',
|
|
61
|
+
reason: 'Especificações visuais, design tokens e estados de componentes concluídos.',
|
|
62
|
+
nextAction: 'DevSecOps implementa código semântico aderente aos tokens e contratos.',
|
|
63
|
+
requiresFix: false
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Rota 0C: EVENT_UI_REVIEW (QA -> UIUX)
|
|
68
|
+
this.on(EVENT_TYPES.EVENT_UI_REVIEW, (payload) => {
|
|
69
|
+
const a11yPassed = payload.a11yPassed !== false;
|
|
70
|
+
return {
|
|
71
|
+
handoffTo: AGENT_PERSONAS.UIUX,
|
|
72
|
+
status: a11yPassed ? 'A11Y_APPROVED' : 'A11Y_REJECTED',
|
|
73
|
+
reason: a11yPassed ? 'Auditoria de acessibilidade WCAG aprovada.' : 'Violação de contraste ou semântica ARIA detectada.',
|
|
74
|
+
nextAction: a11yPassed ? 'Tokens e componentes homologados.' : 'UIUX deve reajustar contraste e hierarquia visual.',
|
|
75
|
+
requiresFix: !a11yPassed
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Rota 1: EVENT_CODE_COMPLETE (DevSecOps -> QA)
|
|
80
|
+
this.on(EVENT_TYPES.EVENT_CODE_COMPLETE, (payload) => {
|
|
81
|
+
const codeOrDiff = payload.diff || payload.code || '';
|
|
82
|
+
const hasTests = codeOrDiff.includes('test') || payload.testsPassing === true;
|
|
83
|
+
const hasSecrets = SECURITY_RULES.some(r => r.cwe === 'CWE-798' && r.pattern.test(codeOrDiff));
|
|
84
|
+
|
|
85
|
+
if (hasSecrets) {
|
|
86
|
+
return {
|
|
87
|
+
handoffTo: AGENT_PERSONAS.QA,
|
|
88
|
+
status: 'REJECTED',
|
|
89
|
+
reason: 'Credencial ou segredo em texto plano detectado no diff (Regra 5).',
|
|
90
|
+
nextAction: 'QA emite veto e exige expurgo imediato.',
|
|
91
|
+
requiresFix: true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasTests) {
|
|
96
|
+
return {
|
|
97
|
+
handoffTo: AGENT_PERSONAS.QA,
|
|
98
|
+
status: 'REJECTED',
|
|
99
|
+
reason: 'Entrega de código sem testes automatizados comprovados (Regra 4).',
|
|
100
|
+
nextAction: 'QA exige inclusão de testes unitários antes de qualquer aprovação.',
|
|
101
|
+
requiresFix: true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
handoffTo: AGENT_PERSONAS.QA,
|
|
107
|
+
status: 'READY_FOR_INSPECTION',
|
|
108
|
+
reason: 'Código submetido à auditoria cética adversária.',
|
|
109
|
+
nextAction: 'QA realiza análise de segurança AST e casos de borda.',
|
|
110
|
+
requiresFix: false
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Rota 2: EVENT_REVIEW_FAILED (QA -> DevSecOps)
|
|
115
|
+
this.on(EVENT_TYPES.EVENT_REVIEW_FAILED, (payload) => {
|
|
116
|
+
return {
|
|
117
|
+
handoffTo: AGENT_PERSONAS.DEVSECOPS,
|
|
118
|
+
status: 'CORRECTION_REQUIRED',
|
|
119
|
+
reason: payload.finding || 'Falha apontada pela auditoria adversária.',
|
|
120
|
+
nextAction: 'DevSecOps deve sanar a causa raiz sem contornar testes ou lint.',
|
|
121
|
+
requiresFix: true
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Rota 3: EVENT_REVIEW_PASSED (QA -> Orchestrator / CI)
|
|
126
|
+
this.on(EVENT_TYPES.EVENT_REVIEW_PASSED, (payload) => {
|
|
127
|
+
return {
|
|
128
|
+
handoffTo: AGENT_PERSONAS.ORCHESTRATOR,
|
|
129
|
+
status: 'APPROVED',
|
|
130
|
+
reason: 'Auditoria cética concluída com 100% de conformidade.',
|
|
131
|
+
nextAction: 'Liberar Pull Request, validar esteira de CI e efetuar Tríplice Registro.',
|
|
132
|
+
requiresFix: false
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Rota 4: EVENT_METRIC_ANOMALY (SRE -> Scrum & SRE)
|
|
137
|
+
this.on(EVENT_TYPES.EVENT_METRIC_ANOMALY, (payload) => {
|
|
138
|
+
const metric = payload.metric || 'unknown';
|
|
139
|
+
const value = payload.value || 0;
|
|
140
|
+
const threshold = payload.threshold || 0;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
handoffTo: AGENT_PERSONAS.SCRUM,
|
|
144
|
+
status: 'ANOMALY_TRIGGERED',
|
|
145
|
+
reason: `Métrica ${metric} atingiu ${value} (limiar: ${threshold}).`,
|
|
146
|
+
nextAction: 'Disparar playbook de auto-remediação SRE e iniciar investigação Ishikawa/PDCA.',
|
|
147
|
+
remediation: payload.recommendedPlaybook || 'PLAYBOOK_CIRCUIT_BREAKER_LLM.md'
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Rota 5: EVENT_REFACTOR_PROPOSAL (Scrum/DevSecOps -> QA Shift-Left Gate -> Backlog)
|
|
152
|
+
this.on(EVENT_TYPES.EVENT_REFACTOR_PROPOSAL, (payload) => {
|
|
153
|
+
const isAligned = payload.contextAligned !== false;
|
|
154
|
+
const isQaApproved = payload.qaApproved !== false;
|
|
155
|
+
const isOffDomain = payload.isOffDomain === true;
|
|
156
|
+
const isRedundant = payload.isRedundant === true;
|
|
157
|
+
|
|
158
|
+
if (!isAligned || !isQaApproved || isOffDomain || isRedundant) {
|
|
159
|
+
return {
|
|
160
|
+
handoffTo: AGENT_PERSONAS.QA,
|
|
161
|
+
status: 'REJECTED_SANITY_FILTER',
|
|
162
|
+
reason: 'Veto do Quality Architect (Regra 14 / Shift-Left Gate): proposta fora de domínio, overengineering ou arquivos redundantes.',
|
|
163
|
+
nextAction: 'Ideia descartada para evitar inchaço do repositório e preservar foco no produto real.',
|
|
164
|
+
ideaId: payload.ideaId || 'I-REJECTED',
|
|
165
|
+
requiresFix: true
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
handoffTo: AGENT_PERSONAS.DEVSECOPS,
|
|
171
|
+
status: 'QUEUED',
|
|
172
|
+
reason: 'Proposta de melhoria homologada pelo Quality Architect em conformidade com a Regra 14.',
|
|
173
|
+
nextAction: 'Cadastrar card no backlog e criar branch dedicada.',
|
|
174
|
+
ideaId: payload.ideaId || 'I-NEW',
|
|
175
|
+
requiresFix: false
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Rota 6: EVENT_AGENT_SELF_IMPROVE (Suíte Completa -> QA Auditoria Empírica -> Orchestrator)
|
|
180
|
+
this.on(EVENT_TYPES.EVENT_AGENT_SELF_IMPROVE, (payload) => {
|
|
181
|
+
const evalPassed = payload.evalPassed !== false;
|
|
182
|
+
if (!evalPassed) {
|
|
183
|
+
return {
|
|
184
|
+
handoffTo: AGENT_PERSONAS.QA,
|
|
185
|
+
status: 'SELF_IMPROVE_REJECTED',
|
|
186
|
+
reason: 'Veto de Auditoria Empírica de QA: auto-evolução reflexiva reprovada por falta de evidência empírica mensurável.',
|
|
187
|
+
nextAction: 'Descartar hipótese para prevenir alucinação cumulativa de regras.',
|
|
188
|
+
requiresFix: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
handoffTo: AGENT_PERSONAS.ORCHESTRATOR,
|
|
194
|
+
status: 'REFLEXIVE_LOGGED',
|
|
195
|
+
reason: 'Ciclo de auto-evolução reflexiva validado pelo Quality Architect com evidência empírica.',
|
|
196
|
+
nextAction: 'Registrar hipótese validada de domínio em IDEAS.md.',
|
|
197
|
+
tokenBudget: payload.tokenBudget || 'Under 1500 tokens',
|
|
198
|
+
requiresFix: false
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Rota 7: EVENT_KANBAN_DISPATCH (Full Dev continuous loop)
|
|
203
|
+
this.on(EVENT_TYPES.EVENT_KANBAN_DISPATCH, (payload) => {
|
|
204
|
+
const { processFullDevPipeline } = require('./jira_manager.js');
|
|
205
|
+
const pipeline = processFullDevPipeline(payload.tasks || [], payload.projectContext || {});
|
|
206
|
+
return {
|
|
207
|
+
handoffTo: AGENT_PERSONAS.FULL_DEV,
|
|
208
|
+
status: pipeline.activeTask ? 'RESUME_IN_PROGRESS' : (pipeline.nextApprovedTask ? 'START_NEXT_TASK' : 'IDLE_TODO_EMPTY'),
|
|
209
|
+
reason: pipeline.activeTask ? 'Concluir tarefa atualmente em andamento antes de avançar.' : (pipeline.nextApprovedTask ? 'Ideia aprovada pelo filtro de sanidade (Regra 14).' : 'Nenhuma tarefa pendente viável em A fazer.'),
|
|
210
|
+
nextAction: pipeline.lifecyclePlan[0] ? pipeline.lifecyclePlan[0].message : 'Aguardar novas demandas.',
|
|
211
|
+
pipeline
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
on(eventType, handler) {
|
|
217
|
+
this.subscribers.set(eventType, handler);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
dispatch(eventType, payload = {}) {
|
|
221
|
+
if (!EVENT_TYPES[eventType]) {
|
|
222
|
+
throw new Error(`Evento desconhecido: ${eventType}. Eventos suportados: ${Object.keys(EVENT_TYPES).join(', ')}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const handler = this.subscribers.get(eventType);
|
|
226
|
+
const result = handler ? handler(payload) : { status: 'NO_HANDLER' };
|
|
227
|
+
|
|
228
|
+
const eventRecord = {
|
|
229
|
+
eventId: `EVT-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
230
|
+
eventType,
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
payload,
|
|
233
|
+
result
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
this.history.push(eventRecord);
|
|
237
|
+
return eventRecord;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
processKanbanPipeline(tasks = [], projectContext = {}) {
|
|
241
|
+
const { processFullDevPipeline } = require('./jira_manager.js');
|
|
242
|
+
return processFullDevPipeline(tasks, projectContext);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getHistory() {
|
|
246
|
+
return [...this.history];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
clearHistory() {
|
|
250
|
+
this.history = [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
MultiAgentOrchestrator,
|
|
256
|
+
AGENT_PERSONAS,
|
|
257
|
+
EVENT_TYPES
|
|
258
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/otel_tracer.js
|
|
4
|
+
*
|
|
5
|
+
* Tracer Distribuído OpenTelemetry Nativo (AGDEV-24 / I-8).
|
|
6
|
+
* Suporte a W3C TraceContext (traceparent), hierarquia de spans (parent-child),
|
|
7
|
+
* exportação OTLP JSON e renderização de flamegraphs para playbooks de SRE.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
function generateTraceId() {
|
|
13
|
+
return crypto.randomBytes(16).toString('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function generateSpanId() {
|
|
17
|
+
return crypto.randomBytes(8).toString('hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatTraceparent(traceId, spanId, sampled = true) {
|
|
21
|
+
const flags = sampled ? '01' : '00';
|
|
22
|
+
return `00-${traceId}-${spanId}-${flags}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseTraceparent(traceparent) {
|
|
26
|
+
if (!traceparent || typeof traceparent !== 'string') return null;
|
|
27
|
+
const parts = traceparent.split('-');
|
|
28
|
+
if (parts.length !== 4) return null;
|
|
29
|
+
return {
|
|
30
|
+
version: parts[0],
|
|
31
|
+
traceId: parts[1],
|
|
32
|
+
spanId: parts[2],
|
|
33
|
+
flags: parts[3],
|
|
34
|
+
sampled: parts[3] === '01'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class Span {
|
|
39
|
+
constructor(name, tracer, options = {}) {
|
|
40
|
+
this.name = name;
|
|
41
|
+
this.tracer = tracer;
|
|
42
|
+
this.traceId = options.traceId || tracer.traceId;
|
|
43
|
+
this.spanId = generateSpanId();
|
|
44
|
+
this.parentSpanId = options.parentSpanId || null;
|
|
45
|
+
this.startTime = options.startTime || Date.now();
|
|
46
|
+
this.endTime = null;
|
|
47
|
+
this.durationMs = 0;
|
|
48
|
+
this.attributes = Object.assign({}, options.attributes || {});
|
|
49
|
+
this.events = [];
|
|
50
|
+
this.status = { code: 'UNSET' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setAttribute(key, value) {
|
|
54
|
+
this.attributes[key] = value;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
addEvent(name, attributes = {}) {
|
|
59
|
+
this.events.push({
|
|
60
|
+
name,
|
|
61
|
+
time: Date.now(),
|
|
62
|
+
attributes
|
|
63
|
+
});
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setStatus(code, message = '') {
|
|
68
|
+
this.status = { code, message };
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
end(endTime = Date.now()) {
|
|
73
|
+
this.endTime = endTime;
|
|
74
|
+
this.durationMs = Math.max(0, this.endTime - this.startTime);
|
|
75
|
+
this.tracer._recordSpan(this);
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class Tracer {
|
|
81
|
+
constructor(serviceName, options = {}) {
|
|
82
|
+
this.serviceName = serviceName;
|
|
83
|
+
this.traceId = options.traceId || generateTraceId();
|
|
84
|
+
this.spans = [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
startSpan(name, options = {}) {
|
|
88
|
+
return new Span(name, this, options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_recordSpan(span) {
|
|
92
|
+
this.spans.push(span);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getTraceContext(activeSpan = null) {
|
|
96
|
+
const spanId = activeSpan ? activeSpan.spanId : (this.spans[0] ? this.spans[0].spanId : generateSpanId());
|
|
97
|
+
return {
|
|
98
|
+
traceId: this.traceId,
|
|
99
|
+
spanId,
|
|
100
|
+
traceparent: formatTraceparent(this.traceId, spanId)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
analyzeBottlenecks(thresholdMs = 50) {
|
|
105
|
+
const bottlenecks = [];
|
|
106
|
+
const totalDuration = this.spans.reduce((max, s) => Math.max(max, (s.endTime || s.startTime) - this.spans[0].startTime), 0) || 1;
|
|
107
|
+
|
|
108
|
+
for (const span of this.spans) {
|
|
109
|
+
const percentage = Math.round((span.durationMs / totalDuration) * 100);
|
|
110
|
+
if (span.durationMs >= thresholdMs || percentage >= 40) {
|
|
111
|
+
bottlenecks.push({
|
|
112
|
+
name: span.name,
|
|
113
|
+
durationMs: span.durationMs,
|
|
114
|
+
percentageTotal: percentage,
|
|
115
|
+
severity: percentage >= 70 ? 'CRÍTICA' : (percentage >= 40 ? 'ALTA' : 'MÉDIA'),
|
|
116
|
+
spanId: span.spanId
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return bottlenecks;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
renderFlamegraphAscii() {
|
|
125
|
+
if (this.spans.length === 0) return 'Trace vazio.';
|
|
126
|
+
|
|
127
|
+
const sorted = [...this.spans].sort((a, b) => a.startTime - b.startTime);
|
|
128
|
+
const traceStart = sorted[0].startTime;
|
|
129
|
+
const traceEnd = Math.max(...sorted.map(s => s.endTime || s.startTime));
|
|
130
|
+
const totalDuration = Math.max(1, traceEnd - traceStart);
|
|
131
|
+
|
|
132
|
+
let ascii = `=== FLAMEGRAPH DISTRIBUÍDO [Trace: ${this.traceId}] (${totalDuration}ms) ===\n`;
|
|
133
|
+
|
|
134
|
+
sorted.forEach((span, index) => {
|
|
135
|
+
const offsetMs = span.startTime - traceStart;
|
|
136
|
+
const offsetChars = Math.floor((offsetMs / totalDuration) * 30);
|
|
137
|
+
const barLength = Math.max(1, Math.floor((span.durationMs / totalDuration) * 40));
|
|
138
|
+
const indent = ' '.repeat(offsetChars);
|
|
139
|
+
const bar = '█'.repeat(barLength);
|
|
140
|
+
|
|
141
|
+
ascii += `[${String(index + 1).padStart(2, '0')}] ${indent}${bar} ${span.name} (${span.durationMs}ms)\n`;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return ascii;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
exportOtlpJson() {
|
|
148
|
+
return {
|
|
149
|
+
resourceSpans: [
|
|
150
|
+
{
|
|
151
|
+
resource: {
|
|
152
|
+
attributes: [
|
|
153
|
+
{ key: 'service.name', value: { stringValue: this.serviceName } }
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
scopeSpans: [
|
|
157
|
+
{
|
|
158
|
+
scope: { name: 'agent-master-devsecops-tracer', version: '3.2.0' },
|
|
159
|
+
spans: this.spans.map(s => ({
|
|
160
|
+
traceId: s.traceId,
|
|
161
|
+
spanId: s.spanId,
|
|
162
|
+
parentSpanId: s.parentSpanId,
|
|
163
|
+
name: s.name,
|
|
164
|
+
startTimeUnixNano: String(s.startTime * 1000000),
|
|
165
|
+
endTimeUnixNano: String((s.endTime || s.startTime) * 1000000),
|
|
166
|
+
attributes: Object.entries(s.attributes).map(([k, v]) => ({
|
|
167
|
+
key: k,
|
|
168
|
+
value: { stringValue: String(v) }
|
|
169
|
+
})),
|
|
170
|
+
status: s.status
|
|
171
|
+
}))
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function simulateMultiAgentTrace(serviceName = 'agent-pipeline') {
|
|
181
|
+
const tracer = new Tracer(serviceName);
|
|
182
|
+
|
|
183
|
+
// 1. Root span: Requisição do Usuário
|
|
184
|
+
const root = tracer.startSpan('user_goal_execution').setAttribute('agent.role', 'Orchestrator');
|
|
185
|
+
|
|
186
|
+
// 2. Sub-span: Arquiteto planeja
|
|
187
|
+
const planSpan = tracer.startSpan('architect_plan', { parentSpanId: root.spanId })
|
|
188
|
+
.setAttribute('agent.role', 'Arquiteto')
|
|
189
|
+
.setAttribute('model', 'Claude 3.5 Sonnet');
|
|
190
|
+
// Simula 25ms
|
|
191
|
+
planSpan.end(planSpan.startTime + 25);
|
|
192
|
+
|
|
193
|
+
// 3. Sub-span: DevSecOps implementa
|
|
194
|
+
const devSpan = tracer.startSpan('devsecops_implementation', { parentSpanId: root.spanId })
|
|
195
|
+
.setAttribute('agent.role', 'DevSecOps Lead');
|
|
196
|
+
devSpan.end(devSpan.startTime + 60);
|
|
197
|
+
|
|
198
|
+
// 4. Sub-span: QA audita
|
|
199
|
+
const revSpan = tracer.startSpan('qa_audit', { parentSpanId: root.spanId })
|
|
200
|
+
.setAttribute('agent.role', 'QA Independente');
|
|
201
|
+
revSpan.end(revSpan.startTime + 15);
|
|
202
|
+
|
|
203
|
+
// 5. Finaliza root span
|
|
204
|
+
root.end(root.startTime + 105);
|
|
205
|
+
|
|
206
|
+
return tracer;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runCli() {
|
|
210
|
+
const tracer = simulateMultiAgentTrace();
|
|
211
|
+
console.log(tracer.renderFlamegraphAscii());
|
|
212
|
+
console.log('\nGargalos Identificados (Threshold 30ms):');
|
|
213
|
+
console.log(JSON.stringify(tracer.analyzeBottlenecks(30), null, 2));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (require.main === module) {
|
|
217
|
+
runCli().catch(console.error);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
generateTraceId,
|
|
222
|
+
generateSpanId,
|
|
223
|
+
formatTraceparent,
|
|
224
|
+
parseTraceparent,
|
|
225
|
+
Span,
|
|
226
|
+
Tracer,
|
|
227
|
+
simulateMultiAgentTrace
|
|
228
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/perf_benchmark.js
|
|
4
|
+
*
|
|
5
|
+
* Scanner de Regressão de Performance em CI (AGDEV-25 / I-9).
|
|
6
|
+
* Executa micro-benchmarks comparativos de latência e consumo de RAM/Heap,
|
|
7
|
+
* detectando e bloqueando regressões superiores a 10% em relação ao baseline de referência.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
14
|
+
const BASELINE_FILE = path.join(ROOT_DIR, 'benchmarks', 'baseline.json');
|
|
15
|
+
|
|
16
|
+
function runBenchmark(iterations = 100) {
|
|
17
|
+
const { processMessage } = require('./mcp_server.js');
|
|
18
|
+
const { generateCompactPrompt } = require('./context_compressor.js');
|
|
19
|
+
|
|
20
|
+
const startMem = process.memoryUsage();
|
|
21
|
+
const startTime = process.hrtime.bigint();
|
|
22
|
+
|
|
23
|
+
// Benchmark 1: Processamento de JSON-RPC Initialize e Tools List
|
|
24
|
+
for (let i = 0; i < iterations; i++) {
|
|
25
|
+
processMessage({ jsonrpc: '2.0', id: i, method: 'tools/list' });
|
|
26
|
+
processMessage({ jsonrpc: '2.0', id: i, method: 'tools/call', params: { name: 'agent_full_dev_status', arguments: {} } });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Benchmark 2: Compactação Semântica de Prompt
|
|
30
|
+
for (let i = 0; i < Math.floor(iterations / 2); i++) {
|
|
31
|
+
generateCompactPrompt();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const endTime = process.hrtime.bigint();
|
|
35
|
+
const endMem = process.memoryUsage();
|
|
36
|
+
|
|
37
|
+
const durationNs = Number(endTime - startTime);
|
|
38
|
+
const durationMs = durationNs / 1000000;
|
|
39
|
+
const avgLatencyUs = Math.round((durationNs / (iterations * 2)) / 1000);
|
|
40
|
+
const heapUsedMb = Math.round((endMem.heapUsed / (1024 * 1024)) * 100) / 100;
|
|
41
|
+
const opsPerSec = Math.round(((iterations * 2.5) / (durationMs / 1000)));
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
iterations,
|
|
45
|
+
totalDurationMs: Math.round(durationMs * 100) / 100,
|
|
46
|
+
avgLatencyUs,
|
|
47
|
+
opsPerSec,
|
|
48
|
+
heapUsedMb
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function comparePerformance(current, baseline, maxDegradationPct = 10) {
|
|
53
|
+
const latencyDeltaPct = Math.round(((current.avgLatencyUs - baseline.avgLatencyUs) / baseline.avgLatencyUs) * 100);
|
|
54
|
+
const memoryDeltaPct = Math.round(((current.heapUsedMb - baseline.heapUsedMb) / (baseline.heapUsedMb || 1)) * 100);
|
|
55
|
+
|
|
56
|
+
const hasLatencyRegression = latencyDeltaPct > maxDegradationPct;
|
|
57
|
+
const hasMemoryRegression = memoryDeltaPct > (maxDegradationPct * 2); // 20% tolerância para GC jitter
|
|
58
|
+
const passed = !hasLatencyRegression && !hasMemoryRegression;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
passed,
|
|
62
|
+
verdict: passed ? 'APROVADO' : 'REGRESSÃO_DETECTADA',
|
|
63
|
+
thresholdPct: maxDegradationPct,
|
|
64
|
+
current,
|
|
65
|
+
baseline,
|
|
66
|
+
deltas: {
|
|
67
|
+
latencyDeltaPct,
|
|
68
|
+
memoryDeltaPct
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatPerfReport(comparison) {
|
|
74
|
+
let report = '================================================================================\n';
|
|
75
|
+
report += `RELATÓRIO DE PERFORMANCE & REGRESSÃO DE CI: [ ${comparison.verdict} ]\n`;
|
|
76
|
+
report += '================================================================================\n\n';
|
|
77
|
+
|
|
78
|
+
report += `Limiar Máximo de Degradação Permitido: ${comparison.thresholdPct}%\n\n`;
|
|
79
|
+
|
|
80
|
+
report += 'MÉTRICAS COMPARATIVAS:\n';
|
|
81
|
+
report += `- Latência Média por Operação: ${comparison.current.avgLatencyUs}µs (Baseline: ${comparison.baseline.avgLatencyUs}µs) -> Variação: ${comparison.deltas.latencyDeltaPct > 0 ? '+' : ''}${comparison.deltas.latencyDeltaPct}%\n`;
|
|
82
|
+
report += `- Throughput (Ops/sec): ${comparison.current.opsPerSec} ops/s (Baseline: ${comparison.baseline.opsPerSec} ops/s)\n`;
|
|
83
|
+
report += `- Memória Heap Utilizada: ${comparison.current.heapUsedMb} MB (Baseline: ${comparison.baseline.heapUsedMb} MB) -> Variação: ${comparison.deltas.memoryDeltaPct > 0 ? '+' : ''}${comparison.deltas.memoryDeltaPct}%\n\n`;
|
|
84
|
+
|
|
85
|
+
if (comparison.passed) {
|
|
86
|
+
report += 'STATUS: Nenhuma degradação crítica detectada. Aprovado para merge na esteira de CI.\n';
|
|
87
|
+
} else {
|
|
88
|
+
report += 'ALERTA SRE: Regressão superior a 10% identificada! Merge bloqueado conforme política DevSecOps.\n';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
report += '================================================================================\n';
|
|
92
|
+
return report;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getOrInitBaseline() {
|
|
96
|
+
const benchDir = path.dirname(BASELINE_FILE);
|
|
97
|
+
if (!fs.existsSync(benchDir)) {
|
|
98
|
+
fs.mkdirSync(benchDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (fs.existsSync(BASELINE_FILE)) {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(fs.readFileSync(BASELINE_FILE, 'utf8'));
|
|
104
|
+
} catch { /* ignora */ }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Baseline padrão de referência calibrado para Node.js 22
|
|
108
|
+
const initialBaseline = {
|
|
109
|
+
iterations: 100,
|
|
110
|
+
totalDurationMs: 15.0,
|
|
111
|
+
avgLatencyUs: 60,
|
|
112
|
+
opsPerSec: 15000,
|
|
113
|
+
heapUsedMb: 18.5
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
fs.writeFileSync(BASELINE_FILE, JSON.stringify(initialBaseline, null, 2), 'utf8');
|
|
117
|
+
return initialBaseline;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runCli() {
|
|
121
|
+
const args = process.argv.slice(2);
|
|
122
|
+
const baseline = getOrInitBaseline();
|
|
123
|
+
const current = runBenchmark(100);
|
|
124
|
+
|
|
125
|
+
if (args.includes('--save-baseline')) {
|
|
126
|
+
fs.writeFileSync(BASELINE_FILE, JSON.stringify(current, null, 2), 'utf8');
|
|
127
|
+
console.log(`[Perf Scanner] Novo baseline salvo com sucesso em ${BASELINE_FILE}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const comparison = comparePerformance(current, baseline, 15);
|
|
132
|
+
const report = formatPerfReport(comparison);
|
|
133
|
+
console.log(report);
|
|
134
|
+
|
|
135
|
+
if (args.includes('--strict') && !comparison.passed) {
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (require.main === module) {
|
|
141
|
+
runCli().catch(err => {
|
|
142
|
+
console.error(err);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
runBenchmark,
|
|
149
|
+
comparePerformance,
|
|
150
|
+
formatPerfReport,
|
|
151
|
+
getOrInitBaseline,
|
|
152
|
+
BASELINE_FILE
|
|
153
|
+
};
|