agent-master-devsecops 3.9.0 → 5.1.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.
Files changed (82) hide show
  1. package/README.md +205 -371
  2. package/agents/AGENT_DEVSECOPS.md +32 -417
  3. package/agents/AGENT_INFRA.md +31 -0
  4. package/agents/AGENT_ORCHESTRATOR.md +39 -264
  5. package/agents/AGENT_QA.md +58 -186
  6. package/agents/AGENT_SCRUM.md +23 -190
  7. package/agents/AGENT_SRE.md +22 -173
  8. package/agents/AGENT_UIUX.md +26 -162
  9. package/docs/ARCHITECTURE.md +152 -148
  10. package/docs/CALCULATIONS.md +17 -17
  11. package/docs/CHANGELOG.md +112 -49
  12. package/docs/ENGINEERING_RULES.md +56 -252
  13. package/docs/IDEAS.md +17 -17
  14. package/docs/LOCAL_BACKLOG.md +20 -0
  15. package/docs/STATE.md +111 -43
  16. package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -58
  17. package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -58
  18. package/package.json +59 -47
  19. package/scripts/auditors/a11y_auditor.js +462 -0
  20. package/scripts/auditors/code_quality_auditor.js +444 -0
  21. package/scripts/auditors/contract_test_validator.js +396 -0
  22. package/scripts/auditors/eval_harness.js +213 -0
  23. package/scripts/auditors/git_hygiene_linter.js +391 -0
  24. package/scripts/auditors/pr_reviewer.js +627 -0
  25. package/scripts/auditors/sast_scanner.js +442 -0
  26. package/scripts/auditors/sca_dependency_auditor.js +673 -0
  27. package/scripts/{bootstrap_cli.js → bin/bootstrap_cli.js} +597 -468
  28. package/scripts/bin/domain_idea_miner.js +658 -0
  29. package/scripts/bin/smart_onboarding.js +905 -0
  30. package/scripts/{web_inspector.js → bin/web_inspector.js} +578 -355
  31. package/scripts/core/architecture_advisor.js +378 -0
  32. package/scripts/core/ast_skeletonizer.js +413 -0
  33. package/scripts/core/autonomous_remediation_engine.js +305 -0
  34. package/scripts/{circuit_breaker.js → core/circuit_breaker.js} +546 -330
  35. package/scripts/core/context_compressor.js +237 -0
  36. package/scripts/core/fast_feature_scaffold.js +840 -0
  37. package/scripts/core/hot_state_cache.js +238 -0
  38. package/scripts/core/knowledge_gateway.js +888 -0
  39. package/scripts/{magic_prompt_generator.js → core/magic_prompt_generator.js} +250 -205
  40. package/scripts/{multi_agent_orchestrator.js → core/multi_agent_orchestrator.js} +421 -258
  41. package/scripts/{otel_tracer.js → core/otel_tracer.js} +335 -228
  42. package/scripts/{perf_benchmark.js → core/perf_benchmark.js} +212 -153
  43. package/scripts/core/sarif_engine.js +299 -0
  44. package/scripts/{sre_remediation_engine.js → core/sre_remediation_engine.js} +236 -174
  45. package/scripts/{universal_packager.js → core/universal_packager.js} +357 -313
  46. package/scripts/integrations/atlassian_client.js +600 -0
  47. package/scripts/integrations/atlassian_sync_action.js +576 -0
  48. package/scripts/integrations/confluence_sync.js +684 -0
  49. package/scripts/integrations/credentials_guard.js +500 -0
  50. package/scripts/integrations/jira_manager.js +722 -0
  51. package/scripts/mcp/handlers/atlassian_handlers.js +300 -0
  52. package/scripts/mcp/handlers/core_handlers.js +419 -0
  53. package/scripts/mcp/handlers/qa_handlers.js +502 -0
  54. package/scripts/mcp/handlers/rules_handlers.js +379 -0
  55. package/scripts/mcp/handlers/scaffold_handlers.js +334 -0
  56. package/scripts/mcp/handlers/sre_handlers.js +443 -0
  57. package/scripts/mcp/resources_registry.js +453 -0
  58. package/scripts/mcp/server.js +408 -0
  59. package/scripts/mcp/tool_handlers.js +444 -0
  60. package/scripts/mcp/tools_registry.js +1123 -0
  61. package/scripts/mcp_server.js +236 -1884
  62. package/.cursorignore +0 -23
  63. package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +0 -53
  64. package/scripts/a11y_auditor.js +0 -187
  65. package/scripts/ast_skeletonizer.js +0 -220
  66. package/scripts/atlassian_client.js +0 -238
  67. package/scripts/atlassian_sync_action.js +0 -269
  68. package/scripts/code_quality_auditor.js +0 -142
  69. package/scripts/confluence_sync.js +0 -337
  70. package/scripts/context_compressor.js +0 -139
  71. package/scripts/contract_test_validator.js +0 -166
  72. package/scripts/credentials_guard.js +0 -216
  73. package/scripts/domain_idea_miner.js +0 -364
  74. package/scripts/eval_harness.js +0 -100
  75. package/scripts/fast_feature_scaffold.js +0 -338
  76. package/scripts/git_hygiene_linter.js +0 -186
  77. package/scripts/hot_state_cache.js +0 -95
  78. package/scripts/jira_manager.js +0 -408
  79. package/scripts/pr_reviewer.js +0 -373
  80. package/scripts/sast_scanner.js +0 -180
  81. package/scripts/sca_dependency_auditor.js +0 -167
  82. package/scripts/smart_onboarding.js +0 -610
@@ -0,0 +1,722 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/integrations/jira_manager.js
4
+ *
5
+ * Gerenciador Unificado do Jira: Padronização de Kanban, WIP Limits, Sprints, Backlog Offline-First e Time-Tracking.
6
+ * Consolida o controle de fluxo ágil dos 3 Kanbans canônicos ("A fazer", "Em andamento", "Concluído")
7
+ * com o registro automatizado de worklogs e timetracking durante o ciclo de desenvolvimento em CI/CD.
8
+ *
9
+ * Versão: 5.1.0 Enterprise
10
+ * Governança: Tríade Git + Jira + Confluence (Regra 11 e AGENT_SCRUM).
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+
18
+ const {
19
+ getAtlassianCredentials,
20
+ requestAtlassian,
21
+ getGitMetadata
22
+ } = require('./atlassian_client.js');
23
+
24
+ const { createSarifReport } = require('../core/sarif_engine.js');
25
+
26
+ // ==========================================
27
+ // 1. KANBAN CANÔNICO & PADRONIZAÇÃO DE QUADRO
28
+ // ==========================================
29
+
30
+ const CANONICAL_KANBANS = Object.freeze({
31
+ TODO: 'A fazer',
32
+ IN_PROGRESS: 'Em andamento',
33
+ DONE: 'Concluído'
34
+ });
35
+
36
+ const CANONICAL_COLUMNS_LIST = Object.freeze([
37
+ CANONICAL_KANBANS.TODO,
38
+ CANONICAL_KANBANS.IN_PROGRESS,
39
+ CANONICAL_KANBANS.DONE
40
+ ]);
41
+
42
+ /**
43
+ * Sanitiza o summary de um card do Jira, removendo chaves redundantes do título.
44
+ * Exemplo: "[PROJ-123] Deploy de Produção" -> "Deploy de Produção"
45
+ * @param {string} [summary='']
46
+ * @returns {string}
47
+ */
48
+ function sanitizeIssueSummary(summary = '') {
49
+ if (!summary || typeof summary !== 'string') return '';
50
+ return summary.replace(/^\s*\[[A-Z0-9_-]{2,15}\]\s*[:-]?\s*/i, '').trim();
51
+ }
52
+
53
+ /**
54
+ * Valida o limite de trabalho em progresso (WIP Limit).
55
+ * @param {number} [inProgressCount=0]
56
+ * @param {number} [maxWip=2]
57
+ * @returns {object}
58
+ */
59
+ function checkWipLimit(inProgressCount = 0, maxWip = 2) {
60
+ const safeInProgress = Math.max(0, Number(inProgressCount) || 0);
61
+ const safeMax = Math.max(1, Number(maxWip) || 2);
62
+ const isSaturated = safeInProgress >= safeMax;
63
+
64
+ return {
65
+ inProgressCount: safeInProgress,
66
+ maxWip: safeMax,
67
+ isSaturated,
68
+ canPullNewTask: !isSaturated,
69
+ alertMessage: isSaturated
70
+ ? `WIP SATURADO: ${safeInProgress} tarefas ativas em andamento (limite máximo: ${safeMax}). Conclua tarefas antes de puxar novas.`
71
+ : null
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Mapeia qualquer status ou nome de coluna legado do Jira para uma das 3 colunas canônicas.
77
+ * @param {string} [statusName='']
78
+ * @param {string} [statusCategory='']
79
+ * @returns {string}
80
+ */
81
+ function resolveCanonicalColumn(statusName = '', statusCategory = '') {
82
+ const normName = String(statusName || '').trim().toLowerCase();
83
+ const normCat = String(statusCategory || '').trim().toLowerCase();
84
+
85
+ // 1. Concluído / Done
86
+ if (
87
+ normName.includes('concl') ||
88
+ normName.includes('done') ||
89
+ normName.includes('finaliz') ||
90
+ normName.includes('resolv') ||
91
+ normCat.includes('conclu') ||
92
+ normCat.includes('done') ||
93
+ normCat.includes('complete')
94
+ ) {
95
+ return CANONICAL_KANBANS.DONE;
96
+ }
97
+
98
+ // 2. Em andamento / In Progress
99
+ if (
100
+ normName.includes('andamento') ||
101
+ normName.includes('progress') ||
102
+ normName.includes('desenvolv') ||
103
+ normName.includes('análise') ||
104
+ normName.includes('analise') ||
105
+ normCat.includes('andamento') ||
106
+ normCat.includes('progress') ||
107
+ normCat.includes('indeterminate')
108
+ ) {
109
+ return CANONICAL_KANBANS.IN_PROGRESS;
110
+ }
111
+
112
+ // 3. A fazer / To Do / Backlog
113
+ return CANONICAL_KANBANS.TODO;
114
+ }
115
+
116
+ /**
117
+ * Normaliza e consolida as colunas de uma configuração de Board do Jira para os 3 Kanbans padrão.
118
+ * @param {object} [boardConfig={}]
119
+ * @returns {object}
120
+ */
121
+ function standardizeBoardColumns(boardConfig = {}) {
122
+ const rawColumns = (boardConfig && boardConfig.columnConfig && Array.isArray(boardConfig.columnConfig.columns))
123
+ ? boardConfig.columnConfig.columns
124
+ : [];
125
+
126
+ const standardized = {
127
+ canonicalColumns: [...CANONICAL_COLUMNS_LIST],
128
+ columns: [
129
+ { name: CANONICAL_KANBANS.TODO, statuses: [] },
130
+ { name: CANONICAL_KANBANS.IN_PROGRESS, statuses: [] },
131
+ { name: CANONICAL_KANBANS.DONE, statuses: [] }
132
+ ],
133
+ remappedCount: 0
134
+ };
135
+
136
+ const todoCol = standardized.columns[0];
137
+ const inProgCol = standardized.columns[1];
138
+ const doneCol = standardized.columns[2];
139
+
140
+ for (const col of rawColumns) {
141
+ if (!col || typeof col !== 'object') continue;
142
+ const canonicalTarget = resolveCanonicalColumn(col.name);
143
+ const statuses = Array.isArray(col.statuses) ? col.statuses : [];
144
+
145
+ if (canonicalTarget === CANONICAL_KANBANS.DONE) {
146
+ doneCol.statuses.push(...statuses);
147
+ } else if (canonicalTarget === CANONICAL_KANBANS.IN_PROGRESS) {
148
+ inProgCol.statuses.push(...statuses);
149
+ } else {
150
+ todoCol.statuses.push(...statuses);
151
+ }
152
+
153
+ if (col.name !== canonicalTarget) {
154
+ standardized.remappedCount++;
155
+ }
156
+ }
157
+
158
+ return standardized;
159
+ }
160
+
161
+ /**
162
+ * Constrói a consulta JQL padronizada para uma das 3 colunas canônicas no Jira.
163
+ * @param {string} projectKey
164
+ * @param {string} canonicalColumn
165
+ * @returns {string}
166
+ */
167
+ function buildCanonicalJql(projectKey, canonicalColumn) {
168
+ if (!projectKey || typeof projectKey !== 'string') {
169
+ throw new Error('projectKey é obrigatório para construir JQL.');
170
+ }
171
+
172
+ switch (canonicalColumn) {
173
+ case CANONICAL_KANBANS.TODO:
174
+ return `project = "${projectKey}" AND statusCategory in ("To Do", "Undefined") ORDER BY rank ASC, created ASC`;
175
+ case CANONICAL_KANBANS.IN_PROGRESS:
176
+ return `project = "${projectKey}" AND statusCategory in ("In Progress") ORDER BY rank ASC, updated DESC`;
177
+ case CANONICAL_KANBANS.DONE:
178
+ return `project = "${projectKey}" AND statusCategory in ("Done") ORDER BY updated DESC`;
179
+ default:
180
+ throw new Error(`Coluna desconhecida: "${canonicalColumn}". Use: ${CANONICAL_COLUMNS_LIST.join(', ')}`);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Valida se há pelo menos um sprint ativo na esteira para evitar "quadro de sprints ativo vazio".
186
+ * @param {Array<object>} [sprints=[]]
187
+ * @returns {object}
188
+ */
189
+ function validateSprintHealth(sprints = []) {
190
+ const safeSprints = Array.isArray(sprints) ? sprints : [];
191
+ const activeSprints = safeSprints.filter(s => s && s.state === 'active');
192
+ const futureSprints = safeSprints.filter(s => s && s.state === 'future');
193
+ const closedSprints = safeSprints.filter(s => s && s.state === 'closed');
194
+
195
+ return {
196
+ totalSprints: safeSprints.length,
197
+ activeCount: activeSprints.length,
198
+ futureCount: futureSprints.length,
199
+ closedCount: closedSprints.length,
200
+ hasActiveSprint: activeSprints.length > 0,
201
+ warning: activeSprints.length === 0
202
+ ? 'ALERTA DE FLUXO: Nenhuma sprint está ativa no Jira. A tela de Sprints Ativos ficará em branco até que uma sprint seja iniciada.'
203
+ : null
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Avalia candidato do backlog com base no contexto do projeto e filtro de sanidade (Regra 14).
209
+ * @param {object} candidate
210
+ * @param {object} [projectContext={}]
211
+ * @returns {object}
212
+ */
213
+ function evaluateFullDevCandidate(candidate = {}, projectContext = {}) {
214
+ const safeCandidate = candidate && typeof candidate === 'object' ? candidate : {};
215
+ const { title = '', description = '' } = safeCandidate;
216
+ const { stack = 'generic', domain = 'general' } = projectContext || {};
217
+
218
+ const text = `${title} ${description}`.toLowerCase();
219
+ const evaluation = {
220
+ valid: true,
221
+ candidateTitle: sanitizeIssueSummary(title),
222
+ reasons: [],
223
+ recommendedAction: 'PROCEED_TO_IN_PROGRESS'
224
+ };
225
+
226
+ const overengineeringKeywords = ['blockchain', 'smart contract', 'quantum computing', 'microservices mesh'];
227
+ const matchedOver = overengineeringKeywords.filter(k => text.includes(k));
228
+ if (matchedOver.length > 0 && !String(domain).toLowerCase().includes('crypto')) {
229
+ evaluation.valid = false;
230
+ evaluation.reasons.push(`Overengineering ou desconexão com domínio (${domain}): detectado [${matchedOver.join(', ')}]`);
231
+ evaluation.recommendedAction = 'DROP_OR_REJECT';
232
+ }
233
+
234
+ if (text.includes('kubernetes') && (String(stack).includes('cli') || String(stack).includes('desktop'))) {
235
+ evaluation.valid = false;
236
+ evaluation.reasons.push(`Incompatibilidade com stack (${stack}): infraestrutura desproporcional`);
237
+ evaluation.recommendedAction = 'DROP_OR_REJECT';
238
+ }
239
+
240
+ return evaluation;
241
+ }
242
+
243
+ /**
244
+ * Orquestra o ciclo do Modo Full Developer baseado nos 3 Kanbans canônicos.
245
+ * @param {Array<object>} [tasks=[]]
246
+ * @param {object} [projectContext={}]
247
+ * @param {number} [maxWip=2]
248
+ * @returns {object}
249
+ */
250
+ function processFullDevPipeline(tasks = [], projectContext = {}, maxWip = 2) {
251
+ const safeTasks = Array.isArray(tasks) ? tasks : [];
252
+ const todoTasks = safeTasks.filter(t => t && resolveCanonicalColumn(t.status || t.statusName, t.statusCategory) === CANONICAL_KANBANS.TODO);
253
+ const inProgressTasks = safeTasks.filter(t => t && resolveCanonicalColumn(t.status || t.statusName, t.statusCategory) === CANONICAL_KANBANS.IN_PROGRESS);
254
+ const doneTasks = safeTasks.filter(t => t && resolveCanonicalColumn(t.status || t.statusName, t.statusCategory) === CANONICAL_KANBANS.DONE);
255
+
256
+ const wipStatus = checkWipLimit(inProgressTasks.length, maxWip);
257
+
258
+ const results = {
259
+ totalTasks: safeTasks.length,
260
+ counts: {
261
+ todo: todoTasks.length,
262
+ inProgress: inProgressTasks.length,
263
+ done: doneTasks.length
264
+ },
265
+ wipStatus,
266
+ activeTask: inProgressTasks[0] || null,
267
+ activeTasks: inProgressTasks,
268
+ nextApprovedTask: null,
269
+ rejectedTasks: [],
270
+ lifecyclePlan: []
271
+ };
272
+
273
+ if (wipStatus.isSaturated) {
274
+ results.lifecyclePlan.push({
275
+ step: 'CONCLUDE_ACTIVE_TASKS',
276
+ message: wipStatus.alertMessage
277
+ });
278
+ return results;
279
+ }
280
+
281
+ for (const candidate of todoTasks) {
282
+ const evalRes = evaluateFullDevCandidate(candidate, projectContext);
283
+ if (evalRes.valid && !results.nextApprovedTask) {
284
+ results.nextApprovedTask = candidate;
285
+ results.lifecyclePlan.push({
286
+ step: 'TRANSITION_AND_START',
287
+ taskKey: candidate.key || candidate.id,
288
+ cleanSummary: evalRes.candidateTitle,
289
+ from: CANONICAL_KANBANS.TODO,
290
+ to: CANONICAL_KANBANS.IN_PROGRESS,
291
+ message: 'Ideia/tarefa aprovada pelo filtro de sanidade (Regra 14). Transicionar para "Em andamento" e iniciar ciclo.'
292
+ });
293
+ } else if (!evalRes.valid) {
294
+ results.rejectedTasks.push({
295
+ taskKey: candidate.key || candidate.id,
296
+ reasons: evalRes.reasons
297
+ });
298
+ }
299
+ }
300
+
301
+ return results;
302
+ }
303
+
304
+ // ==========================================
305
+ // 2. TIME TRACKING & WORKLOGS
306
+ // ==========================================
307
+
308
+ function extractIssueKeys(text) {
309
+ if (!text || typeof text !== 'string') return [];
310
+ const matches = text.match(/\b[A-Z0-9_-]{2,15}-\d+\b/g) || [];
311
+ return [...new Set(matches.map(k => k.toUpperCase()))];
312
+ }
313
+
314
+ async function getExistingWorklogs(credentials, issueKey) {
315
+ try {
316
+ const res = await requestAtlassian(credentials, 'GET', `/rest/api/3/issue/${issueKey}/worklog`);
317
+ if (res.status === 200 && res.body?.worklogs) {
318
+ return res.body.worklogs;
319
+ }
320
+ } catch (err) {
321
+ void err;
322
+ }
323
+ return [];
324
+ }
325
+
326
+ async function logWorkTime(credentials, issueKey, timeSpentSeconds, commentText) {
327
+ const startedTime = new Date().toISOString().replace('Z', '+0000');
328
+ const payload = {
329
+ timeSpentSeconds,
330
+ started: startedTime,
331
+ comment: {
332
+ type: 'doc',
333
+ version: 1,
334
+ content: [
335
+ {
336
+ type: 'paragraph',
337
+ content: [{ type: 'text', text: commentText }]
338
+ }
339
+ ]
340
+ }
341
+ };
342
+
343
+ return requestAtlassian(credentials, 'POST', `/rest/api/3/issue/${issueKey}/worklog`, payload);
344
+ }
345
+
346
+ async function ensureInProgress(credentials, issueKey) {
347
+ try {
348
+ const issueRes = await requestAtlassian(credentials, 'GET', `/rest/api/3/issue/${issueKey}?fields=status`);
349
+ if (issueRes.status === 200 && issueRes.body?.fields?.status) {
350
+ const currentStatus = issueRes.body.fields.status.name || '';
351
+ const currentCat = issueRes.body.fields.status.statusCategory?.name || '';
352
+ if (resolveCanonicalColumn(currentStatus, currentCat) === CANONICAL_KANBANS.TODO) {
353
+ const transRes = await requestAtlassian(credentials, 'GET', `/rest/api/3/issue/${issueKey}/transitions`);
354
+ const transitions = transRes.body?.transitions || [];
355
+ const inProgTransition = transitions.find(t =>
356
+ resolveCanonicalColumn(t.name, t.to?.statusCategory?.name) === CANONICAL_KANBANS.IN_PROGRESS
357
+ );
358
+ if (inProgTransition) {
359
+ await requestAtlassian(credentials, 'POST', `/rest/api/3/issue/${issueKey}/transitions`, {
360
+ transition: { id: inProgTransition.id }
361
+ });
362
+ console.log(`[Jira Manager] 🔄 Card ${issueKey} transicionado para "Em andamento".`);
363
+ }
364
+ }
365
+ }
366
+ } catch (err) {
367
+ console.warn(`[Jira Manager] Aviso ao checar status de ${issueKey}:`, err.message);
368
+ }
369
+ }
370
+
371
+ async function trackIssueTime(credentials, key, gitMeta = {}) {
372
+ await ensureInProgress(credentials, key);
373
+
374
+ const shortSha = gitMeta.shortSha || (gitMeta.sha ? gitMeta.sha.slice(0, 7) : 'unknown');
375
+ const rawMessage = gitMeta.commitMessage || gitMeta.message || '';
376
+ const existing = await getExistingWorklogs(credentials, key);
377
+ const tag = `[commit:${shortSha}]`;
378
+ const alreadyLogged = existing.some(wl => {
379
+ const text = JSON.stringify(wl.comment || {});
380
+ return text.includes(tag);
381
+ });
382
+
383
+ if (alreadyLogged) {
384
+ console.log(`[Jira Manager] ℹ Tempo já registrado anteriormente para o commit ${shortSha} no card ${key}.`);
385
+ return { key, status: 'already_logged' };
386
+ }
387
+
388
+ const timeSpentSeconds = 900;
389
+ const comment = `Tempo de andamento registrado automaticamente via esteira de CI/CD.\nAutor: ${gitMeta.authorName || 'DevSecOps Bot'}\nCommit: ${shortSha} (${rawMessage.split('\n')[0]})\n${tag}`;
390
+
391
+ const res = await logWorkTime(credentials, key, timeSpentSeconds, comment);
392
+ if (res.status === 201) {
393
+ console.log(`[Jira Manager] ✅ 15m de andamento registrados no Jira para ${key} (Commit ${shortSha}).`);
394
+ return { key, status: 'logged', timeSpentSeconds };
395
+ } else {
396
+ console.warn(`[Jira Manager] ⚠️ Falha ao registrar tempo para ${key}: HTTP ${res.status}`);
397
+ return { key, status: 'failed', code: res.status };
398
+ }
399
+ }
400
+
401
+ async function runTimeTracking() {
402
+ console.log('--- Início da Execução do Jira Time Tracking Automático ---');
403
+ const credentials = getAtlassianCredentials();
404
+ if (!credentials.isConfigured) {
405
+ console.log('[Jira Manager] ℹ Credenciais Atlassian não configuradas. Pulando registro de tempo.');
406
+ return;
407
+ }
408
+
409
+ const gitMeta = getGitMetadata();
410
+ const keys = [
411
+ ...extractIssueKeys(gitMeta.branch),
412
+ ...extractIssueKeys(gitMeta.commitMessage)
413
+ ];
414
+
415
+ const uniqueKeys = [...new Set(keys)];
416
+ if (uniqueKeys.length === 0) {
417
+ console.log('[Jira Manager] ℹ Nenhuma chave Jira identificada na branch ou mensagem de commit.');
418
+ return;
419
+ }
420
+
421
+ console.log(`[Jira Manager] Chaves detectadas: ${uniqueKeys.join(', ')}`);
422
+ for (const key of uniqueKeys) {
423
+ try {
424
+ await trackIssueTime(credentials, key, gitMeta);
425
+ } catch (err) {
426
+ console.error(`[Jira Manager] ❌ Erro ao processar tempo para ${key}:`, err.message);
427
+ }
428
+ }
429
+ }
430
+
431
+ // ==========================================
432
+ // 3. BACKLOG LOCAL & OFFLINE-FIRST (GIT-NATIVE)
433
+ // ==========================================
434
+
435
+ function getLocalBacklogPath() {
436
+ return path.resolve(process.cwd(), 'docs/LOCAL_BACKLOG.md');
437
+ }
438
+
439
+ function readLocalBacklog(customPath) {
440
+ const filePath = customPath || getLocalBacklogPath();
441
+ if (!fs.existsSync(filePath)) {
442
+ return [];
443
+ }
444
+
445
+ const content = fs.readFileSync(filePath, 'utf8');
446
+ const lines = content.split(/\r?\n/);
447
+ const tasks = [];
448
+ let currentSection = CANONICAL_KANBANS.TODO;
449
+
450
+ for (const line of lines) {
451
+ const trimmed = line.trim();
452
+ if (trimmed.startsWith('## A fazer')) {
453
+ currentSection = CANONICAL_KANBANS.TODO;
454
+ } else if (trimmed.startsWith('## Em andamento')) {
455
+ currentSection = CANONICAL_KANBANS.IN_PROGRESS;
456
+ } else if (trimmed.startsWith('## Concluído')) {
457
+ currentSection = CANONICAL_KANBANS.DONE;
458
+ } else if (trimmed.startsWith('- [ ]') || trimmed.startsWith('- [x]') || trimmed.startsWith('- [X]')) {
459
+ const isChecked = trimmed.startsWith('- [x]') || trimmed.startsWith('- [X]');
460
+ const taskText = trimmed.replace(/^-\s*\[[ xX]\]\s*/, '');
461
+ const keyMatch = taskText.match(/\[([A-Z0-9_-]+)\]/);
462
+ const key = keyMatch ? keyMatch[1] : `TASK-${tasks.length + 1}`;
463
+ const summary = taskText.replace(/\[([A-Z0-9_-]+)\]\s*/, '').trim();
464
+
465
+ tasks.push({
466
+ key,
467
+ id: key,
468
+ summary,
469
+ status: isChecked ? CANONICAL_KANBANS.DONE : currentSection,
470
+ statusName: isChecked ? CANONICAL_KANBANS.DONE : currentSection,
471
+ statusCategory: isChecked ? 'DONE' : currentSection === CANONICAL_KANBANS.IN_PROGRESS ? 'IN_PROGRESS' : 'TO_DO'
472
+ });
473
+ }
474
+ }
475
+
476
+ return tasks;
477
+ }
478
+
479
+ function fetchKanbanTasks(credentials = {}, options = {}) {
480
+ const safeOptions = options && typeof options === 'object' ? options : {};
481
+ if (safeOptions.offline || !credentials || !credentials.isConfigured) {
482
+ return readLocalBacklog(safeOptions.localBacklogPath);
483
+ }
484
+ return [];
485
+ }
486
+
487
+ // ==========================================
488
+ // 4. RELATÓRIO EXECUTIVO & AUDITORIA SARIF v2.1.0
489
+ // ==========================================
490
+
491
+ /**
492
+ * Formata relatório executivo em Markdown sobre o status do Kanban e pipeline Full Dev.
493
+ * @param {object} [outcome={}]
494
+ * @returns {string}
495
+ */
496
+ function formatReport(outcome = {}) {
497
+ const safeOutcome = outcome && typeof outcome === 'object' ? outcome : {};
498
+ let md = '# 📊 Jira Kanban & Flow Management — Status Report\n\n';
499
+
500
+ const counts = safeOutcome.counts || {};
501
+ const wipStatus = safeOutcome.wipStatus || {};
502
+ const isSaturated = Boolean(wipStatus.isSaturated);
503
+
504
+ const statusBadge = isSaturated ? '🔴 **WIP SATURADO**' : '🟢 **FLUXO SAUDÁVEL**';
505
+ md += `**Status do Fluxo:** ${statusBadge}\n`;
506
+ md += `**Total de Tarefas:** \`${safeOutcome.totalTasks || 0}\`\n\n`;
507
+
508
+ md += '| Coluna Canônica | Contagem | Status |\n';
509
+ md += '| :--- | :---: | :--- |\n';
510
+ md += `| **A fazer** (\`TODO\`) | \`${counts.todo || 0}\` | 📥 Aguardando |\n`;
511
+ md += `| **Em andamento** (\`IN_PROGRESS\`) | \`${counts.inProgress || 0}\` | ${isSaturated ? '⚠️ Limite atingido' : '⚡ Ativo'} |\n`;
512
+ md += `| **Concluído** (\`DONE\`) | \`${counts.done || 0}\` | ✅ Finalizado |\n\n`;
513
+
514
+ if (safeOutcome.activeTask) {
515
+ const at = safeOutcome.activeTask;
516
+ md += `### 🚀 Tarefa Ativa em Desenvolvimento\n`;
517
+ md += `- **Chave:** \`${at.key || at.id || 'N/A'}\`\n`;
518
+ md += `- **Título:** ${at.title || at.summary || 'Sem título'}\n\n`;
519
+ }
520
+
521
+ if (safeOutcome.nextApprovedTask) {
522
+ const nt = safeOutcome.nextApprovedTask;
523
+ md += `### 🎯 Próxima Tarefa Aprovada (Regra 14)\n`;
524
+ md += `- **Chave:** \`${nt.key || nt.id || 'N/A'}\`\n`;
525
+ md += `- **Título:** ${nt.title || nt.summary || 'Sem título'}\n\n`;
526
+ }
527
+
528
+ if (Array.isArray(safeOutcome.rejectedTasks) && safeOutcome.rejectedTasks.length > 0) {
529
+ md += `### 🚫 Tarefas Descartadas / Sanity Filter (Regra 14)\n`;
530
+ for (const rej of safeOutcome.rejectedTasks) {
531
+ md += `- **${rej.taskKey}:** ${(rej.reasons || []).join('; ')}\n`;
532
+ }
533
+ md += '\n';
534
+ }
535
+
536
+ return md;
537
+ }
538
+
539
+ /**
540
+ * Audita a saúde do fluxo de trabalho e conformidade com limites de WIP emitindo relatório OASIS SARIF v2.1.0.
541
+ * @param {object} [outcome={}]
542
+ * @returns {object} SARIF v2.1.0 JSON object
543
+ */
544
+ function auditToSarif(outcome = {}) {
545
+ const safeOutcome = outcome && typeof outcome === 'object' ? outcome : {};
546
+ const rules = [
547
+ {
548
+ id: 'RULE-KANBAN-001',
549
+ name: 'WipLimitExceeded',
550
+ severity: 'WARNING',
551
+ message: 'Limite de WIP saturado na coluna "Em andamento". Conclua tarefas antes de puxar novas.'
552
+ },
553
+ {
554
+ id: 'RULE-KANBAN-002',
555
+ name: 'NoActiveSprint',
556
+ severity: 'NOTE',
557
+ message: 'Nenhuma sprint está ativa no Jira, resultando em quadro ativo vazio.'
558
+ },
559
+ {
560
+ id: 'RULE-KANBAN-003',
561
+ name: 'OverengineeringCandidateRejected',
562
+ severity: 'WARNING',
563
+ message: 'Candidato do backlog rejeitado pelo filtro de sanidade (Regra 14).'
564
+ }
565
+ ];
566
+
567
+ const findings = [];
568
+
569
+ // Checagem de saturação de WIP
570
+ if (safeOutcome.wipStatus && safeOutcome.wipStatus.isSaturated) {
571
+ findings.push({
572
+ ruleId: 'RULE-KANBAN-001',
573
+ message: safeOutcome.wipStatus.alertMessage || 'WIP limit excedido.',
574
+ filePath: 'docs/LOCAL_BACKLOG.md',
575
+ line: 1,
576
+ column: 1
577
+ });
578
+ }
579
+
580
+ // Checagem de candidatos rejeitados por overengineering
581
+ if (Array.isArray(safeOutcome.rejectedTasks)) {
582
+ for (const rej of safeOutcome.rejectedTasks) {
583
+ findings.push({
584
+ ruleId: 'RULE-KANBAN-003',
585
+ message: `Tarefa ${rej.taskKey} vetada por anti-overengineering: ${(rej.reasons || []).join('; ')}`,
586
+ filePath: 'docs/LOCAL_BACKLOG.md',
587
+ line: 1,
588
+ column: 1
589
+ });
590
+ }
591
+ }
592
+
593
+ return createSarifReport({
594
+ toolName: 'agent-jira-manager',
595
+ toolVersion: '5.1.0',
596
+ rules,
597
+ findings,
598
+ properties: {
599
+ category: 'Agile Flow & Kanban Governance',
600
+ rule: 'Regra 11 e Regra 14'
601
+ }
602
+ });
603
+ }
604
+
605
+ // ==========================================
606
+ // 5. CLASSE CANÔNICA JIRAMANAGER
607
+ // ==========================================
608
+
609
+ class JiraManager {
610
+ constructor(options = {}) {
611
+ this.credentials = options.credentials || getAtlassianCredentials();
612
+ this.targetDir = options.targetDir || process.cwd();
613
+ this.maxWip = Math.max(1, Number(options.maxWip) || 2);
614
+ this.logger = options.logger || console;
615
+ }
616
+
617
+ checkWipLimit(inProgressCount, maxWip = this.maxWip) {
618
+ return JiraManager.checkWipLimit(inProgressCount, maxWip);
619
+ }
620
+
621
+ resolveCanonicalColumn(statusName, statusCategory) {
622
+ return JiraManager.resolveCanonicalColumn(statusName, statusCategory);
623
+ }
624
+
625
+ standardizeBoardColumns(boardConfig) {
626
+ return JiraManager.standardizeBoardColumns(boardConfig);
627
+ }
628
+
629
+ buildCanonicalJql(projectKey, canonicalColumn) {
630
+ return JiraManager.buildCanonicalJql(projectKey, canonicalColumn);
631
+ }
632
+
633
+ evaluateFullDevCandidate(candidate, projectContext) {
634
+ return JiraManager.evaluateFullDevCandidate(candidate, projectContext);
635
+ }
636
+
637
+ processFullDevPipeline(tasks, projectContext, maxWip = this.maxWip) {
638
+ return JiraManager.processFullDevPipeline(tasks, projectContext, maxWip);
639
+ }
640
+
641
+ readLocalBacklog(customPath) {
642
+ return JiraManager.readLocalBacklog(customPath);
643
+ }
644
+
645
+ fetchKanbanTasks(options) {
646
+ return JiraManager.fetchKanbanTasks(this.credentials, options);
647
+ }
648
+
649
+ formatReport(outcome) {
650
+ return JiraManager.formatReport(outcome);
651
+ }
652
+
653
+ auditToSarif(outcome) {
654
+ return JiraManager.auditToSarif(outcome);
655
+ }
656
+
657
+ async trackIssueTime(key, gitMeta) {
658
+ return JiraManager.trackIssueTime(this.credentials, key, gitMeta);
659
+ }
660
+
661
+ // Métodos estáticos canônicos
662
+ static get CANONICAL_KANBANS() { return CANONICAL_KANBANS; }
663
+ static get CANONICAL_COLUMNS_LIST() { return CANONICAL_COLUMNS_LIST; }
664
+ static sanitizeIssueSummary(s) { return sanitizeIssueSummary(s); }
665
+ static checkWipLimit(c, m) { return checkWipLimit(c, m); }
666
+ static resolveCanonicalColumn(name, cat) { return resolveCanonicalColumn(name, cat); }
667
+ static standardizeBoardColumns(cfg) { return standardizeBoardColumns(cfg); }
668
+ static buildCanonicalJql(key, col) { return buildCanonicalJql(key, col); }
669
+ static validateSprintHealth(sprints) { return validateSprintHealth(sprints); }
670
+ static evaluateFullDevCandidate(c, ctx) { return evaluateFullDevCandidate(c, ctx); }
671
+ static processFullDevPipeline(tasks, ctx, maxWip) { return processFullDevPipeline(tasks, ctx, maxWip); }
672
+ static getLocalBacklogPath() { return getLocalBacklogPath(); }
673
+ static readLocalBacklog(p) { return readLocalBacklog(p); }
674
+ static fetchKanbanTasks(creds, opts) { return fetchKanbanTasks(creds, opts); }
675
+ static formatReport(o) { return formatReport(o); }
676
+ static auditToSarif(o) { return auditToSarif(o); }
677
+ static extractIssueKeys(t) { return extractIssueKeys(t); }
678
+ static getExistingWorklogs(creds, k) { return getExistingWorklogs(creds, k); }
679
+ static logWorkTime(creds, k, s, c) { return logWorkTime(creds, k, s, c); }
680
+ static ensureInProgress(creds, k) { return ensureInProgress(creds, k); }
681
+ static trackIssueTime(creds, k, meta) { return trackIssueTime(creds, k, meta); }
682
+ static runTimeTracking() { return runTimeTracking(); }
683
+ }
684
+
685
+ if (require.main === module) {
686
+ const args = process.argv.slice(2);
687
+ if (args.includes('--track')) {
688
+ runTimeTracking().catch(console.error);
689
+ } else {
690
+ console.log('Jira Manager operacional. Use --track para executar apontamento de horas.');
691
+ }
692
+ }
693
+
694
+ module.exports = {
695
+ JiraManager,
696
+ // Kanban
697
+ CANONICAL_KANBANS,
698
+ CANONICAL_COLUMNS_LIST,
699
+ sanitizeIssueSummary,
700
+ checkWipLimit,
701
+ resolveCanonicalColumn,
702
+ standardizeBoardColumns,
703
+ buildCanonicalJql,
704
+ validateSprintHealth,
705
+ evaluateFullDevCandidate,
706
+ processFullDevPipeline,
707
+ // Backlog Local & Offline-First
708
+ getLocalBacklogPath,
709
+ readLocalBacklog,
710
+ fetchKanbanTasks,
711
+ // Relatórios & SARIF v2.1.0
712
+ formatReport,
713
+ auditToSarif,
714
+ auditKanbanToSarif: auditToSarif,
715
+ // Time Tracking
716
+ extractIssueKeys,
717
+ getExistingWorklogs,
718
+ logWorkTime,
719
+ ensureInProgress,
720
+ trackIssueTime,
721
+ runTimeTracking
722
+ };