oxe-cc 0.7.1 → 0.9.1

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 (156) hide show
  1. package/.cursor/commands/oxe-ask.md +34 -0
  2. package/.cursor/commands/oxe-capabilities.md +34 -0
  3. package/.cursor/commands/oxe-checkpoint.md +34 -0
  4. package/.cursor/commands/oxe-compact.md +33 -0
  5. package/.cursor/commands/oxe-dashboard.md +34 -0
  6. package/.cursor/commands/oxe-debug.md +34 -0
  7. package/.cursor/commands/oxe-discuss.md +34 -0
  8. package/.cursor/commands/oxe-execute.md +34 -0
  9. package/.cursor/commands/oxe-forensics.md +34 -0
  10. package/.cursor/commands/oxe-help.md +33 -0
  11. package/.cursor/commands/oxe-loop.md +34 -0
  12. package/.cursor/commands/oxe-milestone.md +34 -0
  13. package/.cursor/commands/oxe-next.md +33 -0
  14. package/.cursor/commands/oxe-obs.md +34 -0
  15. package/.cursor/commands/oxe-plan-agent.md +33 -0
  16. package/.cursor/commands/oxe-plan.md +34 -0
  17. package/.cursor/commands/oxe-project.md +34 -0
  18. package/.cursor/commands/oxe-quick.md +34 -0
  19. package/.cursor/commands/oxe-research.md +34 -0
  20. package/.cursor/commands/oxe-retro.md +34 -0
  21. package/.cursor/commands/oxe-review-pr.md +34 -0
  22. package/.cursor/commands/oxe-route.md +34 -0
  23. package/.cursor/commands/oxe-scan.md +34 -0
  24. package/.cursor/commands/oxe-security.md +34 -0
  25. package/.cursor/commands/oxe-session.md +34 -0
  26. package/.cursor/commands/oxe-skill.md +45 -0
  27. package/.cursor/commands/oxe-spec.md +34 -0
  28. package/.cursor/commands/oxe-ui-review.md +34 -0
  29. package/.cursor/commands/oxe-ui-spec.md +34 -0
  30. package/.cursor/commands/oxe-update.md +33 -0
  31. package/.cursor/commands/oxe-validate-gaps.md +34 -0
  32. package/.cursor/commands/oxe-verify.md +34 -0
  33. package/.cursor/commands/oxe-workstream.md +34 -0
  34. package/.cursor/commands/oxe.md +38 -2
  35. package/.github/copilot-instructions.md +8 -5
  36. package/.github/prompts/oxe-ask.prompt.md +33 -0
  37. package/.github/prompts/oxe-capabilities.prompt.md +33 -0
  38. package/.github/prompts/oxe-checkpoint.prompt.md +45 -12
  39. package/.github/prompts/oxe-compact.prompt.md +44 -11
  40. package/.github/prompts/oxe-dashboard.prompt.md +33 -0
  41. package/.github/prompts/oxe-debug.prompt.md +45 -12
  42. package/.github/prompts/oxe-discuss.prompt.md +33 -0
  43. package/.github/prompts/oxe-execute.prompt.md +45 -12
  44. package/.github/prompts/oxe-forensics.prompt.md +45 -12
  45. package/.github/prompts/oxe-help.prompt.md +42 -9
  46. package/.github/prompts/oxe-loop.prompt.md +45 -12
  47. package/.github/prompts/oxe-milestone.prompt.md +45 -12
  48. package/.github/prompts/oxe-next.prompt.md +42 -9
  49. package/.github/prompts/oxe-obs.prompt.md +45 -12
  50. package/.github/prompts/oxe-plan-agent.prompt.md +43 -10
  51. package/.github/prompts/oxe-plan.prompt.md +45 -12
  52. package/.github/prompts/oxe-project.prompt.md +45 -12
  53. package/.github/prompts/oxe-quick.prompt.md +45 -12
  54. package/.github/prompts/oxe-research.prompt.md +45 -12
  55. package/.github/prompts/oxe-retro.prompt.md +45 -12
  56. package/.github/prompts/oxe-review-pr.prompt.md +45 -12
  57. package/.github/prompts/oxe-route.prompt.md +45 -12
  58. package/.github/prompts/oxe-scan.prompt.md +45 -12
  59. package/.github/prompts/oxe-security.prompt.md +45 -12
  60. package/.github/prompts/oxe-session.prompt.md +33 -0
  61. package/.github/prompts/oxe-skill.prompt.md +45 -0
  62. package/.github/prompts/oxe-spec.prompt.md +45 -12
  63. package/.github/prompts/oxe-ui-review.prompt.md +45 -12
  64. package/.github/prompts/oxe-ui-spec.prompt.md +45 -12
  65. package/.github/prompts/oxe-update.prompt.md +44 -11
  66. package/.github/prompts/oxe-validate-gaps.prompt.md +45 -12
  67. package/.github/prompts/oxe-verify.prompt.md +45 -12
  68. package/.github/prompts/oxe-workstream.prompt.md +45 -12
  69. package/.github/prompts/oxe.prompt.md +45 -12
  70. package/AGENTS.md +6 -4
  71. package/CHANGELOG.md +45 -0
  72. package/README.md +38 -8
  73. package/bin/lib/oxe-agent-install.cjs +69 -55
  74. package/bin/lib/oxe-context-engine.cjs +866 -0
  75. package/bin/lib/oxe-dashboard.cjs +605 -588
  76. package/bin/lib/oxe-operational.cjs +105 -0
  77. package/bin/lib/oxe-plugins.cjs +115 -0
  78. package/bin/lib/oxe-project-health.cjs +1139 -666
  79. package/bin/lib/oxe-runtime-semantics.cjs +459 -0
  80. package/bin/lib/oxe-security.cjs +64 -0
  81. package/bin/oxe-cc.js +615 -46
  82. package/commands/oxe/ask.md +33 -0
  83. package/commands/oxe/capabilities.md +33 -0
  84. package/commands/oxe/checkpoint.md +49 -16
  85. package/commands/oxe/compact.md +43 -10
  86. package/commands/oxe/dashboard.md +33 -0
  87. package/commands/oxe/debug.md +49 -16
  88. package/commands/oxe/discuss.md +33 -0
  89. package/commands/oxe/execute.md +49 -16
  90. package/commands/oxe/forensics.md +49 -16
  91. package/commands/oxe/help.md +44 -11
  92. package/commands/oxe/loop.md +50 -17
  93. package/commands/oxe/milestone.md +49 -16
  94. package/commands/oxe/next.md +45 -12
  95. package/commands/oxe/obs.md +49 -16
  96. package/commands/oxe/oxe.md +49 -16
  97. package/commands/oxe/plan-agent.md +48 -15
  98. package/commands/oxe/plan.md +48 -15
  99. package/commands/oxe/project.md +49 -16
  100. package/commands/oxe/quick.md +49 -16
  101. package/commands/oxe/research.md +49 -16
  102. package/commands/oxe/retro.md +49 -16
  103. package/commands/oxe/review-pr.md +49 -16
  104. package/commands/oxe/route.md +44 -11
  105. package/commands/oxe/scan.md +49 -16
  106. package/commands/oxe/security.md +49 -16
  107. package/commands/oxe/session.md +33 -0
  108. package/commands/oxe/skill.md +49 -0
  109. package/commands/oxe/spec.md +47 -14
  110. package/commands/oxe/ui-review.md +49 -16
  111. package/commands/oxe/ui-spec.md +49 -16
  112. package/commands/oxe/update.md +49 -16
  113. package/commands/oxe/validate-gaps.md +49 -16
  114. package/commands/oxe/verify.md +48 -15
  115. package/commands/oxe/workstream.md +49 -16
  116. package/lib/sdk/index.cjs +140 -7
  117. package/lib/sdk/index.d.ts +266 -1
  118. package/oxe/templates/HYPOTHESES.template.md +33 -0
  119. package/oxe/templates/PLAN.template.md +53 -22
  120. package/oxe/templates/SESSION.template.md +2 -0
  121. package/oxe/templates/SKILL.template.md +26 -0
  122. package/oxe/templates/WORKFLOW_AUTHORING.md +18 -2
  123. package/oxe/templates/config.template.json +16 -14
  124. package/oxe/workflows/ask.md +28 -7
  125. package/oxe/workflows/capabilities.md +2 -0
  126. package/oxe/workflows/dashboard.md +12 -2
  127. package/oxe/workflows/debug.md +9 -4
  128. package/oxe/workflows/discuss.md +12 -6
  129. package/oxe/workflows/execute.md +34 -12
  130. package/oxe/workflows/forensics.md +14 -9
  131. package/oxe/workflows/help.md +20 -9
  132. package/oxe/workflows/loop.md +13 -7
  133. package/oxe/workflows/next.md +6 -4
  134. package/oxe/workflows/plan-agent.md +3 -2
  135. package/oxe/workflows/plan.md +26 -3
  136. package/oxe/workflows/quick.md +10 -3
  137. package/oxe/workflows/references/reasoning-discovery.md +28 -0
  138. package/oxe/workflows/references/reasoning-execution.md +29 -0
  139. package/oxe/workflows/references/reasoning-planning.md +32 -0
  140. package/oxe/workflows/references/reasoning-review.md +29 -0
  141. package/oxe/workflows/references/reasoning-status.md +24 -0
  142. package/oxe/workflows/references/workflow-runtime-contracts.json +879 -0
  143. package/oxe/workflows/research.md +8 -2
  144. package/oxe/workflows/retro.md +7 -2
  145. package/oxe/workflows/review-pr.md +12 -8
  146. package/oxe/workflows/route.md +16 -13
  147. package/oxe/workflows/security.md +3 -2
  148. package/oxe/workflows/session.md +44 -0
  149. package/oxe/workflows/skill.md +44 -0
  150. package/oxe/workflows/spec.md +21 -18
  151. package/oxe/workflows/ui-review.md +13 -7
  152. package/oxe/workflows/update.md +3 -1
  153. package/oxe/workflows/validate-gaps.md +12 -6
  154. package/oxe/workflows/verify-audit.md +73 -0
  155. package/oxe/workflows/verify.md +40 -16
  156. package/package.json +84 -83
@@ -1,588 +1,605 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const http = require('http');
6
- const { URL } = require('url');
7
- const health = require('./oxe-project-health.cjs');
8
- const operational = require('./oxe-operational.cjs');
9
- const azure = require('./oxe-azure.cjs');
10
-
11
- function readTextIfExists(p) {
12
- try { return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null; } catch { return null; }
13
- }
14
-
15
- function readJsonArrayIfExists(p) {
16
- try {
17
- if (!fs.existsSync(p)) return [];
18
- const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
19
- return Array.isArray(raw) ? raw : [];
20
- } catch { return []; }
21
- }
22
-
23
- function ensureDirForFile(p) { fs.mkdirSync(path.dirname(p), { recursive: true }); }
24
- function firstMatch(text, regex) { const m = String(text || '').match(regex); return m ? m[1].trim() : null; }
25
- function escapeRegex(v) { return v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
26
- function summarizeText(text, max = 500) { const clean = String(text || '').replace(/\r\n/g, '\n').trim(); return !clean ? '' : clean.length > max ? `${clean.slice(0, max - 1)}…` : clean; }
27
- function normalizeCell(v) { return String(v || '').replace(/<br\s*\/?>/gi, ' ').replace(/`/g, '').replace(/\s+/g, ' ').trim(); }
28
- function splitTableRow(line) { return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => normalizeCell(c)); }
29
-
30
- function parseMarkdownTables(text) {
31
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
32
- const tables = [];
33
- for (let i = 0; i < lines.length - 1; i += 1) {
34
- if (!lines[i].trim().startsWith('|')) continue;
35
- if (!/^\s*\|?[\-:\s|]+\|?\s*$/.test(lines[i + 1] || '')) continue;
36
- const headers = splitTableRow(lines[i]);
37
- const rows = [];
38
- let j = i + 2;
39
- while (j < lines.length && lines[j].trim().startsWith('|')) {
40
- const cells = splitTableRow(lines[j]);
41
- const row = {};
42
- for (let k = 0; k < headers.length; k += 1) row[headers[k]] = normalizeCell(cells[k] || '');
43
- rows.push(row);
44
- j += 1;
45
- }
46
- tables.push({ headers, rows });
47
- i = j - 1;
48
- }
49
- return tables;
50
- }
51
-
52
- function findTableByHeaders(text, expected) {
53
- const wants = expected.map((x) => x.toLowerCase());
54
- return parseMarkdownTables(text).find((table) => wants.every((w) => table.headers.some((h) => h.toLowerCase() === w)));
55
- }
56
-
57
- function listSectionItems(text, heading) {
58
- const match = String(text || '').match(new RegExp(`##\\s*${escapeRegex(heading)}\\s*\\n+([\\s\\S]*?)(?=\\n##\\s|$)`, 'i'));
59
- if (!match) return [];
60
- return match[1].split('\n').map((l) => l.trim()).filter((l) => /^-\s+/.test(l)).map((l) => l.replace(/^-\s+/, '').trim()).filter(Boolean);
61
- }
62
-
63
- function parsePlan(planMd) {
64
- const parts = planMd.split(/^###\s+(T\d+)\s*[—-]\s*/m);
65
- const tasks = [];
66
- const waves = {};
67
- for (let i = 1; i < parts.length; i += 2) {
68
- const id = parts[i].trim();
69
- const rest = (parts[i + 1] || '').split(/^###\s+T\d+/m)[0];
70
- const title = ((rest.match(/^([^\n]+)/) || [null, ''])[1] || '').trim();
71
- const wave = Number((rest.match(/\*\*Onda:\*\*\s*(\d+)/i) || [null, ''])[1]) || null;
72
- const dependsOn = (((rest.match(/\*\*Depende\s+de:\*\*\s*([^\n]+)/i) || [null, ''])[1]) || '').split(/[,\s]+/).filter((s) => /^T\d+$/.test(s.trim()));
73
- const verifyCommand = (rest.match(/Comando:\s*`([^`]+)`/i) || [null, null])[1];
74
- const aceite = ((((rest.match(/\*\*Aceite\s+vinculado:\*\*\s*([^\n]+)/i) || [null, ''])[1]) || '').match(/A\d+/g) || []);
75
- const decisions = ((((rest.match(/\*\*Decisão\s+vinculada:\*\*\s*([^\n]+)/i) || [null, ''])[1]) || '').match(/D-\d+/g) || []);
76
- const task = { id, title, wave, dependsOn, verifyCommand, aceite, decisions };
77
- tasks.push(task);
78
- if (wave != null) (waves[wave] ||= []).push(id);
79
- }
80
- return {
81
- tasks,
82
- totalTasks: tasks.length,
83
- waves: Object.keys(waves).map((k) => ({ wave: Number(k), taskIds: waves[k], tasks: waves[k].map((id) => tasks.find((t) => t.id === id)).filter(Boolean) })).sort((a, b) => a.wave - b.wave),
84
- };
85
- }
86
-
87
- function parseSpec(specMd) {
88
- const objective = (((specMd.match(/##\s*Objetivo\s*\n+([\s\S]*?)(?=\n##\s|\n#[^\#]|$)/im) || [null, ''])[1]) || '').trim().split('\n')[0].trim() || null;
89
- const criteria = [];
90
- const m = specMd.match(/##\s*Critérios.*?aceite[\s\S]*?(\|[\s\S]*?)(?=\n##\s|\n#[^\#]|$)/im);
91
- if (m) {
92
- for (const row of m[1].split('\n').filter((l) => l.startsWith('|'))) {
93
- const cells = row.split('|').map((c) => c.trim()).filter(Boolean);
94
- if (cells.length >= 2 && /^A\d+$/i.test(cells[0])) criteria.push({ id: cells[0].toUpperCase(), criterion: cells[1] || '', howToVerify: cells[2] || '' });
95
- }
96
- }
97
- return { objective, criteria };
98
- }
99
-
100
- function parseRuntime(runtimeMd) {
101
- const agentTable = findTableByHeaders(runtimeMd, ['ID', 'Papel', 'Tarefas', 'Estado']);
102
- const checkpointTable = findTableByHeaders(runtimeMd, ['ID', 'Tipo', 'Escopo', 'Estado']);
103
- return {
104
- currentWave: Number(firstMatch(runtimeMd, /\*\*Onda:\*\*\s*([^\n]+)/i)) || firstMatch(runtimeMd, /\*\*Onda:\*\*\s*([^\n]+)/i),
105
- status: firstMatch(runtimeMd, /\*\*Estado:\*\*\s*([^\n]+)/i),
106
- activeTasks: ((firstMatch(runtimeMd, /\*\*Tarefas ativas:\*\*\s*([^\n]+)/i) || '').split(',').map((x) => x.trim()).filter(Boolean)),
107
- nextAction: firstMatch(runtimeMd, /\*\*Ação:\*\*\s*([^\n]+)/i),
108
- nextReason: firstMatch(runtimeMd, /\*\*Motivo:\*\*\s*([^\n]+)/i),
109
- agents: (agentTable?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', role: row.Papel || row.Role || '—', tasks: (row.Tarefas || row.Tasks || '').split(',').map((x) => x.trim()).filter(Boolean), status: row.Estado || row.Status || '—' })),
110
- checkpoints: (checkpointTable?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', type: row.Tipo || row.Type || '—', scope: row.Escopo || row.Scope || '', status: row.Estado || row.Status || '—', decision: row.Decisão || row.Decisao || row.Decision || '—' })),
111
- evidence: listSectionItems(runtimeMd, 'Evidências produzidas'),
112
- blockages: listSectionItems(runtimeMd, 'Bloqueios').filter((x) => !/^(\(nenhum\)|nenhum)$/i.test(x)),
113
- };
114
- }
115
-
116
- function parseCheckpointsIndex(md) {
117
- const table = findTableByHeaders(md, ['ID', 'Tipo', 'Fase', 'Escopo', 'Estado']);
118
- return (table?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', type: row.Tipo || row.Type || '', phase: row.Fase || row.Phase || '', scope: row.Escopo || row.Scope || '', status: row.Estado || row.Status || '', notes: row.Notas || row.Notes || '' }));
119
- }
120
-
121
- function parseSessionsIndex(md) {
122
- const table = parseMarkdownTables(md).find((t) => {
123
- const h = t.headers.map((x) => x.toLowerCase());
124
- return h.includes('id') && h.includes('nome') && h.includes('status') && h.includes('path');
125
- });
126
- return (table?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', name: row.Nome || row.Name || '—', status: row.Status || '—', createdAt: row.Criada || row.Created || '—', lastActivity: row['Última atividade'] || row['Ultima atividade'] || row['Last activity'] || '—', summary: row.Resumo || row.Summary || '—', path: (row.Path || row.Caminho || '—').replace(/`/g, '') }));
127
- }
128
-
129
- function parseSessionDetail(md) {
130
- return { id: firstMatch(md, /\*\*ID:\*\*\s*([^\n]+)/i), name: firstMatch(md, /\*\*Nome:\*\*\s*([^\n]+)/i), status: firstMatch(md, /\*\*Status:\*\*\s*([^\n]+)/i), createdAt: firstMatch(md, /\*\*Criada:\*\*\s*([^\n]+)/i), lastActivity: firstMatch(md, /\*\*(?:Última|Ultima) atividade:\*\*\s*([^\n]+)/i), summary: firstMatch(md, /\*\*Resumo:\*\*\s*([^\n]+)/i), tags: listSectionItems(md, 'Tags'), history: (findTableByHeaders(md, ['Data', 'Evento'])?.rows || []).map((row) => ({ date: row.Data || row.Date || '—', event: row.Evento || row.Event || '—' })) };
131
- }
132
-
133
- function normalizeEvidenceStatus(raw) {
134
- const v = String(raw || '').toLowerCase();
135
- if (!v) return 'mentioned';
136
- if (/(ok|pass|aprov|sim|true|done|conclu)/.test(v)) return 'passed';
137
- if (/(fail|falh|reprov|não|nao|false|blocked)/.test(v)) return 'failed';
138
- return 'mentioned';
139
- }
140
-
141
- function parseVerify(md) {
142
- const criteria = [];
143
- for (const table of parseMarkdownTables(md)) {
144
- for (const row of table.rows) {
145
- const idCandidate = row.ID || row.Id || row['Critério'] || row['Criterio'] || '';
146
- const match = String(idCandidate).match(/A\d+/i);
147
- if (!match) continue;
148
- criteria.push({ id: match[0].toUpperCase(), status: normalizeEvidenceStatus(row.Status || row.Resultado || row.Result || row.Veredito || row['Passou?'] || ''), summary: row.Evidência || row.Evidencia || row.Resumo || row.Notas || '' });
149
- }
150
- }
151
- return { criteria, mentionedCriteria: Array.from(new Set((md.match(/\bA\d+\b/g) || []).map((x) => x.toUpperCase()))), failed: /\b(verify_failed|falhou|falha|reprovad)\b/i.test(md), passed: /\b(verify_complete|aprovad|passou|sucesso)\b/i.test(md) };
152
- }
153
-
154
- function confidenceBand(confidence, threshold) {
155
- if (confidence == null) return 'unknown';
156
- if (confidence >= 85) return 'ready';
157
- if (confidence >= threshold) return 'controlled';
158
- if (confidence >= 50) return 'needs_refinement';
159
- return 'do_not_execute';
160
- }
161
-
162
- function computeReadiness(ctx, threshold) {
163
- const blockers = [];
164
- const warnings = [...ctx.diagnostics.reviewWarnings, ...ctx.diagnostics.runtimeWarnings, ...ctx.diagnostics.planWarnings];
165
- if (ctx.planReviewStatus !== 'approved') blockers.push(`review_status:${ctx.planReviewStatus || 'draft'}`);
166
- if (ctx.plan.selfEvaluation.bestPlan === 'não') blockers.push('best_plan:no');
167
- if (ctx.plan.selfEvaluation.confidence == null) blockers.push('confidence:missing');
168
- else if (ctx.plan.selfEvaluation.confidence < threshold) blockers.push(`confidence:${ctx.plan.selfEvaluation.confidence}%<${threshold}%`);
169
- if (ctx.checkpoints.parsed.some((x) => /pending_approval/i.test(x.status))) blockers.push('checkpoint:pending_approval');
170
- if (ctx.runtime.parsed.status === 'blocked') blockers.push('runtime:blocked');
171
- if (ctx.spec.uncoveredCriteria.length) warnings.push(`${ctx.spec.uncoveredCriteria.length} critérios sem cobertura no plano`);
172
- return {
173
- go: blockers.length === 0,
174
- decision: blockers.length === 0 ? 'go' : 'no-go',
175
- threshold,
176
- confidence: ctx.plan.selfEvaluation.confidence,
177
- confidenceBand: confidenceBand(ctx.plan.selfEvaluation.confidence, threshold),
178
- checkpointPending: blockers.includes('checkpoint:pending_approval'),
179
- blockers,
180
- warnings,
181
- };
182
- }
183
-
184
- function buildCoverageMatrix(spec, plan, verify) {
185
- const taskMap = new Map();
186
- for (const task of plan.tasks) {
187
- for (const criterion of task.aceite || []) {
188
- if (!taskMap.has(criterion)) taskMap.set(criterion, []);
189
- taskMap.get(criterion).push(task.id);
190
- }
191
- }
192
- const verifyMap = new Map((verify.criteria || []).map((x) => [x.id, x]));
193
- return spec.criteria.map((c) => ({
194
- id: c.id,
195
- criterion: c.criterion,
196
- verifyHow: c.howToVerify,
197
- tasks: taskMap.get(c.id) || [],
198
- planCovered: taskMap.has(c.id),
199
- verifyStatus: verifyMap.has(c.id) ? verifyMap.get(c.id).status : (verify.mentionedCriteria || []).includes(c.id) ? 'mentioned' : 'missing',
200
- verifySummary: verifyMap.has(c.id) ? verifyMap.get(c.id).summary : '',
201
- }));
202
- }
203
-
204
- function computeCalibration(phase, confidence, verify) {
205
- if (confidence == null) return { status: 'pending', summary: 'Calibração indisponível antes do verify.' };
206
- const low = String(phase || '').toLowerCase();
207
- const completed = low === 'verify_complete' || verify.passed;
208
- const failed = low === 'verify_failed' || verify.failed;
209
- if (!completed && !failed) return { status: 'pending', summary: 'Calibração só fecha após verify.' };
210
- if (confidence >= 85 && failed) return { status: 'overconfident', summary: `Confiança ${confidence}% alta, mas o verify falhou.` };
211
- if (confidence < 70 && failed) return { status: 'calibrated-risk', summary: `O plano já sinalizava risco (${confidence}%) e o verify confirmou a fragilidade.` };
212
- if (confidence < 70 && completed) return { status: 'underconfident', summary: `O resultado final foi melhor que a confiança inicial (${confidence}%).` };
213
- if (confidence >= 85 && completed) return { status: 'well-calibrated', summary: `Alta confiança (${confidence}%) e verify coerente com a expectativa.` };
214
- return { status: 'acceptable', summary: `Confiança ${confidence}% e verify dentro da faixa esperada.` };
215
- }
216
-
217
- function readRepositoryContext(codebaseDir) {
218
- const names = ['OVERVIEW.md', 'STACK.md', 'STRUCTURE.md', 'TESTING.md', 'CONCERNS.md', 'INTEGRATIONS.md'];
219
- const out = {};
220
- for (const name of names) out[name.replace('.md', '').toLowerCase()] = { path: path.join(codebaseDir, name), summary: summarizeText(readTextIfExists(path.join(codebaseDir, name)) || '', 420) };
221
- return out;
222
- }
223
-
224
- function ensureStateSection(stateText, sectionTitle) {
225
- return new RegExp(`##\\s*${escapeRegex(sectionTitle)}`, 'i').test(stateText) ? stateText : `${stateText.trimEnd()}\n\n## ${sectionTitle}\n\n`;
226
- }
227
-
228
- function upsertBulletInSection(body, label, value) {
229
- const line = `- **${label}:** ${value}`;
230
- const re = new RegExp(`^- \\*\\*${escapeRegex(label)}:\\*\\*.*$`, 'im');
231
- if (re.test(body)) return body.replace(re, line);
232
- const trimmed = body.trimEnd();
233
- return !trimmed ? `${line}\n` : `${trimmed}\n${line}\n`;
234
- }
235
-
236
- function upsertStateBullet(stateText, sectionTitle, label, value) {
237
- const ensured = ensureStateSection(stateText, sectionTitle);
238
- const re = new RegExp(`(##\\s*${escapeRegex(sectionTitle)}\\s*\\n+)([\\s\\S]*?)(?=\\n##\\s|$)`, 'i');
239
- return ensured.replace(re, (m, head, body) => `${head}${upsertBulletInSection(body, label, value)}`);
240
- }
241
-
242
- function reviewPaths(projectRoot, activeSession) {
243
- const p = health.scopedOxePaths(projectRoot, activeSession);
244
- return { ...p, state: health.oxePaths(projectRoot).state, reviewJson: p.planReviewComments };
245
- }
246
-
247
- function readScopedText(primaryPath, fallbackPath) {
248
- const primary = readTextIfExists(primaryPath);
249
- if (primary) return primary;
250
- return fallbackPath && fallbackPath !== primaryPath ? readTextIfExists(fallbackPath) || '' : '';
251
- }
252
-
253
- function savePlanReviewStatus(projectRoot, input = {}) {
254
- const globalStatePath = health.oxePaths(projectRoot).state;
255
- const stateText = readTextIfExists(globalStatePath) || '# OXE — Estado\n';
256
- const activeSession = input.activeSession === undefined ? health.parseActiveSession(stateText) : input.activeSession;
257
- const p = reviewPaths(projectRoot, activeSession || null);
258
- const nowIso = new Date().toISOString();
259
- const status = String(input.status || 'draft');
260
- const note = String(input.note || '');
261
- const author = String(input.author || 'dashboard');
262
- const reviewRef = path.relative(path.join(projectRoot, '.oxe'), p.planReview).replace(/\\/g, '/');
263
- let nextState = stateText;
264
- nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'plan_review_status', `\`${status}\``);
265
- nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'plan_review_updated', `\`${nowIso}\``);
266
- nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'plan_review_ref', `\`${reviewRef}\``);
267
- nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'Notas', note);
268
- ensureDirForFile(globalStatePath);
269
- fs.writeFileSync(globalStatePath, nextState, 'utf8');
270
- const comments = readJsonArrayIfExists(p.reviewJson);
271
- const format = (c) => `- **${c.target || 'plan'}** [${c.type || 'note'} | ${c.status || 'open'}] ${c.text || ''}`;
272
- const reviewMd =
273
- `---\noxe_doc: plan_review\nstatus: ${status}\nupdated: ${nowIso.slice(0, 10)}\nplan_ref: ${path.basename(p.plan)}\n---\n\n# OXE — Revisão do Plano\n\n## Estado\n\n- **Status:** ${status}\n- **Atualizado em:** ${nowIso}\n- **Origem:** dashboard local\n- **Autor:** ${author}\n\n## Decisão\n\n- **Resultado:** ${status}\n- **Justificativa:** ${note}\n\n## Comentários abertos\n\n${comments.filter((x) => x.status !== 'resolved').map(format).join('\n') || '- Nenhum'}\n\n## Comentários resolvidos\n\n${comments.filter((x) => x.status === 'resolved').map(format).join('\n') || '- Nenhum'}\n\n## Próxima ação recomendada\n\n${status === 'approved' ? '- `/oxe-execute` ou `oxe-cc status` para seguir a trilha.' : '- `/oxe-plan --replan` ou ajuste do plano antes de executar.'}\n`;
274
- ensureDirForFile(p.planReview);
275
- fs.writeFileSync(p.planReview, reviewMd, 'utf8');
276
- operational.appendEvent(projectRoot, activeSession || null, {
277
- type: 'plan_review_status_changed',
278
- payload: { status, note, author, review_ref: reviewRef },
279
- });
280
- return { status, updatedAt: nowIso, ref: p.planReview, note, author, activeSession: activeSession || null };
281
- }
282
-
283
- function addPlanReviewComment(projectRoot, input = {}) {
284
- const stateText = readTextIfExists(health.oxePaths(projectRoot).state) || '# OXE — Estado\n';
285
- const activeSession = input.activeSession === undefined ? health.parseActiveSession(stateText) : input.activeSession;
286
- const p = reviewPaths(projectRoot, activeSession || null);
287
- const comments = readJsonArrayIfExists(p.reviewJson);
288
- const next = { id: `c-${Date.now().toString(36)}`, target: String(input.target || 'plan'), type: String(input.type || 'note'), status: 'open', author: String(input.author || 'dashboard'), created_at: new Date().toISOString(), text: String(input.text || '').trim() };
289
- comments.push(next);
290
- ensureDirForFile(p.reviewJson);
291
- fs.writeFileSync(p.reviewJson, JSON.stringify(comments, null, 2), 'utf8');
292
- operational.appendEvent(projectRoot, activeSession || null, {
293
- type: 'plan_review_comment_added',
294
- payload: { comment_id: next.id, target: next.target, comment_type: next.type },
295
- });
296
- savePlanReviewStatus(projectRoot, { activeSession, status: health.parsePlanReviewStatus(stateText) || 'in_review', note: 'Há comentários de revisão em aberto', author: String(input.author || 'dashboard') });
297
- return next;
298
- }
299
-
300
- function updatePlanReviewCommentStatus(projectRoot, input = {}) {
301
- const stateText = readTextIfExists(health.oxePaths(projectRoot).state) || '# OXE — Estado\n';
302
- const activeSession = input.activeSession === undefined ? health.parseActiveSession(stateText) : input.activeSession;
303
- const p = reviewPaths(projectRoot, activeSession || null);
304
- const comments = readJsonArrayIfExists(p.reviewJson);
305
- const idx = comments.findIndex((x) => x.id === input.commentId);
306
- if (idx === -1) return null;
307
- comments[idx] = { ...comments[idx], status: String(input.status || 'resolved'), updated_at: new Date().toISOString() };
308
- fs.writeFileSync(p.reviewJson, JSON.stringify(comments, null, 2), 'utf8');
309
- operational.appendEvent(projectRoot, activeSession || null, {
310
- type: 'plan_review_comment_updated',
311
- payload: { comment_id: comments[idx].id, status: comments[idx].status },
312
- });
313
- return comments[idx];
314
- }
315
-
316
- function loadDashboardContext(projectRoot, opts = {}) {
317
- const globalPaths = health.oxePaths(projectRoot);
318
- const stateText = readTextIfExists(globalPaths.state) || '';
319
- const activeSession = opts.activeSession === undefined ? health.parseActiveSession(stateText) : opts.activeSession;
320
- const p = reviewPaths(projectRoot, activeSession || null);
321
- const rootScoped = reviewPaths(projectRoot, null);
322
- const report = health.buildHealthReport(projectRoot);
323
- const specText = readScopedText(p.spec, rootScoped.spec);
324
- const planText = readScopedText(p.plan, rootScoped.plan);
325
- const verifyText = readScopedText(p.verify, rootScoped.verify);
326
- const runtimeText = readScopedText(p.runtime, rootScoped.runtime);
327
- const checkpointsText = readScopedText(p.checkpoints, rootScoped.checkpoints);
328
- const spec = parseSpec(specText);
329
- const plan = parsePlan(planText);
330
- const runtime = parseRuntime(runtimeText);
331
- const checkpoints = parseCheckpointsIndex(checkpointsText);
332
- const verify = parseVerify(verifyText);
333
- const activeRunState = operational.readRunState(projectRoot, activeSession || null);
334
- const traceEvents = operational.readEvents(projectRoot, activeSession || null);
335
- const traceSummary = operational.summarizeEvents(traceEvents);
336
- const memoryLayers = operational.buildMemoryLayers(projectRoot, activeSession || null);
337
- const capabilityCatalog = operational.readCapabilityCatalog(projectRoot);
338
- const azurePaths = azure.azurePaths(projectRoot);
339
- const sessionsRaw = readTextIfExists(globalPaths.sessionsIndex) || '';
340
- const sessions = parseSessionsIndex(sessionsRaw);
341
- const sessionPath = activeSession ? path.join(projectRoot, '.oxe', activeSession, 'SESSION.md') : null;
342
- const sessionRaw = sessionPath ? readTextIfExists(sessionPath) || '' : '';
343
- const ctx = {
344
- projectRoot: path.resolve(projectRoot),
345
- activeSession: activeSession || null,
346
- phase: report.phase || health.parseStatePhase(stateText),
347
- healthStatus: report.healthStatus,
348
- nextStep: report.next,
349
- planReviewStatus: report.planReviewStatus || 'draft',
350
- state: { path: globalPaths.state, raw: stateText, parsed: { phase: health.parseStatePhase(stateText), activeSession, runtimeStatus: firstMatch(stateText, /\*\*runtime_status:\*\*\s*([^\n]+)/i) } },
351
- spec: { path: p.spec, raw: specText, objective: spec.objective, criteria: spec.criteria, uncoveredCriteria: spec.criteria.filter((c) => !plan.tasks.some((t) => (t.aceite || []).includes(c.id))) },
352
- plan: { path: p.plan, raw: planText, tasks: plan.tasks, waves: plan.waves, totalTasks: plan.totalTasks, selfEvaluation: report.planSelfEvaluation },
353
- runtime: { path: p.runtime, raw: runtimeText, summary: summarizeText(runtimeText, 800), parsed: runtime },
354
- activeRun: activeRunState,
355
- tracing: { path: operational.operationalPaths(projectRoot, activeSession || null).events, events: traceEvents, summary: traceSummary },
356
- checkpoints: { path: p.checkpoints, raw: checkpointsText, parsed: checkpoints },
357
- verify: { path: p.verify, raw: verifyText, summary: summarizeText(verifyText, 800), parsed: verify },
358
- review: { markdownPath: p.planReview, commentsPath: p.reviewJson, comments: readJsonArrayIfExists(p.reviewJson) },
359
- sessions: { indexPath: globalPaths.sessionsIndex, raw: sessionsRaw, items: sessions, currentPath: sessionPath, current: sessionRaw ? parseSessionDetail(sessionRaw) : null },
360
- support: { capabilitiesSummary: summarizeText(readTextIfExists(p.capabilitiesIndex) || '', 420), investigationsSummary: summarizeText(readTextIfExists(p.investigationsIndex) || '', 420) },
361
- capabilities: capabilityCatalog,
362
- memoryLayers,
363
- azure: report.azureActive && report.azure
364
- ? {
365
- ...report.azure,
366
- profilePath: azurePaths.profile,
367
- authStatusPath: azurePaths.authStatus,
368
- inventoryMarkdownPath: azurePaths.inventoryMd,
369
- serviceBusPath: azurePaths.serviceBusMd,
370
- eventGridPath: azurePaths.eventGridMd,
371
- sqlPath: azurePaths.sqlMd,
372
- }
373
- : null,
374
- diagnostics: { reviewWarnings: report.reviewWarn, runtimeWarnings: report.runtimeWarn, planWarnings: report.planWarn, sessionWarnings: report.sessionWarn, capabilityWarnings: report.capabilityWarn, investigationWarnings: report.investigationWarn },
375
- repositoryContext: readRepositoryContext(rootScoped.codebase),
376
- };
377
- if (ctx.azure) {
378
- const inventorySummary = ctx.azure.inventorySummary || { total: 0, servicebus: 0, eventgrid: 0, sql: 0 };
379
- ctx.repositoryContext.azure = {
380
- path: ctx.azure.inventoryMarkdownPath,
381
- summary: `login=${ctx.azure.authStatus && ctx.azure.authStatus.login_active ? 'ativo' : 'ausente'} · subscription=${ctx.azure.profile && (ctx.azure.profile.subscription_name || ctx.azure.profile.subscription_id) || '—'} · total=${inventorySummary.total} · sb=${inventorySummary.servicebus || 0} · eg=${inventorySummary.eventgrid || 0} · sql=${inventorySummary.sql || 0}`,
382
- };
383
- }
384
- ctx.readiness = computeReadiness(ctx, 70);
385
- ctx.coverage = buildCoverageMatrix(ctx.spec, ctx.plan, verify);
386
- ctx.calibration = computeCalibration(ctx.phase, ctx.plan.selfEvaluation.confidence, verify);
387
- ctx.visual = {
388
- flow: { nodes: [{ label: 'STATE', status: 'done' }, { label: 'SPEC', status: ctx.spec.raw ? 'done' : 'pending' }, { label: 'PLAN', status: ctx.plan.raw ? 'done' : 'pending' }, { label: 'REVIEW', status: ctx.planReviewStatus === 'approved' ? 'done' : /(rejected|needs_revision)/i.test(ctx.planReviewStatus) ? 'blocked' : 'active' }, { label: 'EXECUTE', status: ctx.runtime.parsed.status === 'running' ? 'active' : ctx.runtime.raw ? 'done' : 'pending' }, { label: 'CHECKPOINTS', status: ctx.readiness.checkpointPending ? 'active' : ctx.checkpoints.parsed.length ? 'done' : 'pending' }, { label: 'VERIFY', status: ctx.verify.raw ? 'done' : 'pending' }, { label: 'LESSONS', status: 'pending' }] },
389
- artifactGraph: [{ id: 'state', label: 'STATE', path: ctx.state.path, detail: ctx.phase || 'índice global', status: 'done' }, { id: 'spec', label: 'SPEC', path: ctx.spec.path, detail: ctx.spec.objective || 'contrato', status: ctx.spec.raw ? 'done' : 'pending' }, { id: 'plan', label: 'PLAN', path: ctx.plan.path, detail: `${ctx.plan.totalTasks} tarefas`, status: ctx.plan.raw ? 'done' : 'pending' }, { id: 'review', label: 'PLAN REVIEW', path: ctx.review.markdownPath, detail: ctx.planReviewStatus, status: ctx.planReviewStatus === 'approved' ? 'done' : 'active' }, { id: 'runtime', label: 'RUNTIME', path: ctx.runtime.path, detail: ctx.runtime.parsed.status || 'sem status', status: ctx.runtime.raw ? 'active' : 'pending' }, { id: 'active-run', label: 'ACTIVE RUN', path: operational.operationalPaths(projectRoot, activeSession || null).activeRun, detail: ctx.activeRun && ctx.activeRun.run_id ? `${ctx.activeRun.run_id} · ${ctx.activeRun.status}` : 'sem run ativo', status: ctx.activeRun ? 'active' : 'pending' }, { id: 'events', label: 'TRACE', path: operational.operationalPaths(projectRoot, activeSession || null).events, detail: `${ctx.tracing.summary.total} evento(s)`, status: ctx.tracing.summary.total ? 'done' : 'pending' }, { id: 'checkpoints', label: 'CHECKPOINTS', path: ctx.checkpoints.path, detail: `${ctx.checkpoints.parsed.length} gates`, status: ctx.readiness.checkpointPending ? 'active' : 'pending' }, { id: 'verify', label: 'VERIFY', path: ctx.verify.path, detail: ctx.calibration.status, status: ctx.verify.raw ? 'done' : 'pending' }],
390
- };
391
- if (ctx.azure) {
392
- ctx.visual.artifactGraph.push(
393
- {
394
- id: 'azure-profile',
395
- label: 'AZURE PROFILE',
396
- path: ctx.azure.profilePath,
397
- detail: ctx.azure.profile && (ctx.azure.profile.subscription_name || ctx.azure.profile.subscription_id) || 'sem subscription',
398
- status: ctx.azure.authStatus && ctx.azure.authStatus.login_active ? 'done' : 'blocked',
399
- },
400
- {
401
- id: 'azure-inventory',
402
- label: 'AZURE INVENTORY',
403
- path: ctx.azure.inventoryMarkdownPath,
404
- detail: ctx.azure.inventorySummary ? `${ctx.azure.inventorySummary.total} recurso(s)` : 'inventário ausente',
405
- status: ctx.azure.inventorySyncedAt ? (ctx.azure.inventoryStale && ctx.azure.inventoryStale.stale ? 'warning' : 'done') : 'pending',
406
- }
407
- );
408
- }
409
- ctx.operationalGraph = ctx.activeRun ? operational.buildOperationalGraph(ctx.activeRun) : { nodes: [], edges: [] };
410
- ctx.dashboard = { readOnly: false };
411
- return ctx;
412
- }
413
-
414
- function dashboardHtml() {
415
- return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>OXE Dashboard</title><style>
416
- :root{--bg:#f4efe6;--bg2:#edf4ed;--panel:#fff;--ink:#1d2f3e;--muted:#617787;--line:#dbe3e8;--brand:#173f58;--ok:#1f7a52;--warn:#9d6b12;--bad:#a53e3e;--info:#235f84;--okbg:#e4f3eb;--warnbg:#fbf0da;--badbg:#f8e8e8;--infobg:#e5eff7;--shadow:0 16px 40px rgba(17,32,44,.08);--radius:18px}*{box-sizing:border-box}body{margin:0;background:linear-gradient(180deg,var(--bg),var(--bg2));font-family:"Segoe UI",Arial,sans-serif;color:var(--ink)}.page{max-width:1560px;margin:0 auto;padding:24px}.hero,.stats,.tabs,.grid2,.grid3,.layout{display:grid;gap:16px}.hero{grid-template-columns:1.2fr .8fr}.stats{grid-template-columns:repeat(5,minmax(0,1fr));margin:0 0 16px}.layout{grid-template-columns:1.45fr .95fr}.grid2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid3{grid-template-columns:repeat(3,minmax(0,1fr))}.panel,.metric,.mini,.item,.entry{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}.panel{padding:18px}.metric,.mini,.item,.entry{padding:14px}.stack,.list,.rail{display:grid;gap:14px}.rail{grid-template-columns:repeat(8,minmax(100px,1fr))}.tabs{grid-template-columns:repeat(6,max-content)}.tab{padding:10px 14px;border:1px solid var(--line);border-radius:999px;background:#fff;cursor:pointer;font-weight:700}.tab.active{background:var(--brand);color:#fff;border-color:var(--brand)}.view{display:none}.view.active{display:block}.label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.value{font-size:24px;font-weight:800;margin-top:6px}.muted{color:var(--muted)}.small{font-size:13px}.badge{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:6px 10px;font-size:11px;text-transform:uppercase;font-weight:800}.done{background:var(--okbg);color:var(--ok)}.active{background:var(--infobg);color:var(--info)}.warning{background:var(--warnbg);color:var(--warn)}.blocked{background:var(--badbg);color:var(--bad)}.pending{background:#edf1f4;color:#567181}.head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;margin-bottom:12px}.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}button{border:0;border-radius:12px;padding:10px 12px;font-weight:700;cursor:pointer}.primary{background:var(--brand);color:#fff}.secondary{background:#edf2f5;color:var(--ink)}.warnbtn{background:#f2e3c5;color:#81540a}.danger{background:#f5dada;color:#8d3030}input,select,textarea{width:100%;border:1px solid var(--line);border-radius:12px;padding:10px;font:inherit}.form{display:grid;gap:10px}.raw{white-space:pre-wrap;background:#f8faf9;border:1px solid var(--line);border-radius:12px;padding:12px;max-height:220px;overflow:auto}.chip{display:inline-flex;border-radius:999px;padding:4px 8px;background:#eef3f6;color:#355267;font-size:12px;margin:4px 6px 0 0}.node{padding:12px;border:1px solid var(--line);border-radius:16px;background:#fff}.go{background:linear-gradient(180deg,#f2fbf5,#ebf7ef)}.nogo{background:linear-gradient(180deg,#fff5f5,#faeded)}@media (max-width:1200px){.hero,.layout{grid-template-columns:1fr}.rail{grid-template-columns:repeat(4,minmax(100px,1fr))}}@media (max-width:860px){.stats,.grid2,.grid3,.tabs,.rail{grid-template-columns:1fr}.page{padding:16px}}</style></head><body><div class="page"><div class="hero"><div class="panel"><h1 style="margin:0;font-size:42px">OXE Decision Control Room</h1><div class="muted" style="margin-top:10px">Control room para decisões reais de Go / No-Go, sem duplicar a fonte de verdade textual do OXE.</div></div><div class="grid2"><div class="mini"><div class="label">Projeto</div><div class="small" id="hero-project">—</div></div><div class="mini"><div class="label">Sessão</div><div class="value" id="hero-session">—</div></div><div class="mini"><div class="label">Próximo passo</div><div class="small" id="hero-next">—</div></div><div class="mini"><div class="label">Confiança</div><div class="value" id="hero-confidence">—</div></div></div></div><div id="app">Carregando…</div></div><script>
417
- const app=document.getElementById('app');let ctx=null;let tab='decision';let session=null;
418
- const esc=(s)=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); const cls=(s)=>{const v=String(s||'').toLowerCase();if(/(healthy|approved|done|go|well-calibrated|passed|ready|completed)/.test(v))return'done';if(/(running|in_review|active|mentioned|acceptable|controlled|replaying)/.test(v))return'active';if(/(rejected|needs_revision|blocked|failed|no-go|overconfident|do_not_execute|aborted|waiting_approval)/.test(v))return'blocked';if(/(risk|warning|underconfident|needs_refinement|paused)/.test(v))return'warning';return'pending';}; const badge=(s,l)=>'<span class="badge '+cls(s)+'">'+esc(l||s||'pending')+'</span>'; const list=(items,empty)=>items&&items.length?'<div class="list">'+items.join('')+'</div>':'<div class="entry small">'+esc(empty)+'</div>'; const runtimeDisabled=()=>ctx&&ctx.dashboard&&ctx.dashboard.readOnly;
419
- async function api(url,opts={}){const res=await fetch(url,{headers:{'Content-Type':'application/json'},...opts});if(!res.ok)throw new Error(await res.text());return res.json();} async function refresh(){ctx=await api('/api/context'+(session?'?session='+encodeURIComponent(session):''));render();}
420
- function render(){document.getElementById('hero-project').textContent=ctx.projectRoot||'—';document.getElementById('hero-session').textContent=ctx.activeSession||'modo legado';document.getElementById('hero-next').textContent=(ctx.nextStep.step||'—')+' · '+(ctx.nextStep.reason||'—');document.getElementById('hero-confidence').textContent=ctx.readiness.confidence!=null?ctx.readiness.confidence+'%':'—';
421
- const flow=(ctx.visual.flow.nodes||[]).map((n)=>'<div class="node"><div class="label">Etapa</div><div style="font-weight:800;font-size:18px;margin-top:6px">'+esc(n.label)+'</div><div style="margin-top:10px">'+badge(n.status)+'</div></div>');
422
- const graph=(ctx.visual.artifactGraph||[]).map((n)=>'<div class="item"><div class="head"><strong>'+esc(n.label)+'</strong>'+badge(n.status)+'</div><div class="small">'+esc(n.detail||'—')+'</div><div class="small muted" style="margin-top:8px">'+esc(n.path||'—')+'</div></div>');
423
- const waves=(ctx.plan.waves||[]).map((w)=>'<div class="panel"><div class="head"><div><strong>Onda '+esc(w.wave)+'</strong><div class="small muted">'+esc(w.taskIds.length)+' tarefas</div></div>'+badge(String(ctx.runtime.parsed.currentWave)===String(w.wave)?'running':'planned')+'</div>'+w.tasks.map((t)=>'<div class="entry"><div style="font-weight:800">'+esc(t.id)+' — '+esc(t.title)+'</div><div class="small muted" style="margin-top:6px">Depende de: '+esc(t.dependsOn.length?t.dependsOn.join(', '):'—')+'</div><div class="small muted">Verificação: '+esc(t.verifyCommand||'—')+'</div><div class="small" style="margin-top:6px">'+(t.aceite||[]).map((x)=>'<span class="chip">'+esc(x)+'</span>').join('')+(t.decisions||[]).map((x)=>'<span class="chip">'+esc(x)+'</span>').join('')+'</div></div>').join('')+'</div>');
424
- const coverage=(ctx.coverage||[]).map((c)=>'<div class="item"><div class="head"><strong>'+esc(c.id)+'</strong>'+badge(c.verifyStatus,c.verifyStatus)+'</div><div class="small">'+esc(c.criterion)+'</div><div class="small muted" style="margin-top:8px">Plano: '+esc(c.tasks.length?c.tasks.join(', '):'sem cobertura')+'</div><div class="small muted">Verify: '+esc(c.verifySummary||c.verifyHow||'sem evidência explícita')+'</div></div>');
425
- const checkpoints=(ctx.checkpoints.parsed||[]).map((c)=>'<div class="item"><div class="head"><strong>'+esc(c.id)+'</strong>'+badge(c.status)+'</div><div class="small muted">Tipo: '+esc(c.type)+' · Fase: '+esc(c.phase||'—')+' · Escopo: '+esc(c.scope||'—')+'</div><div class="small" style="margin-top:8px">'+esc(c.notes||'—')+'</div></div>');
426
- const agents=((ctx.runtime.parsed.agents&&ctx.runtime.parsed.agents.length)?ctx.runtime.parsed.agents:(ctx.blueprint&&ctx.blueprint.agents)||[]).map((a)=>'<div class="item"><div class="head"><strong>'+esc(a.id||a.name||'agent')+'</strong>'+badge(a.status||a.model_hint||'planned')+'</div><div class="small muted">'+esc(a.role||a.persona||'—')+'</div><div class="small">Tarefas: '+esc((a.tasks||[]).join(', ')||a.scope||'—')+'</div></div>');
427
- const comments=(ctx.review.comments||[]).map((c)=>'<div class="item"><div class="small muted">'+esc(c.target)+' · '+esc(c.type)+' · '+esc(c.status)+' · '+esc(c.author)+'</div><div style="margin-top:6px">'+esc(c.text)+'</div>'+(c.status!=='resolved'?'<div class="actions"><button class="secondary" onclick="resolveComment(\\''+esc(c.id)+'\\')">Resolver</button></div>':'')+'</div>');
428
- const sessions=(ctx.sessions.items||[]).map((s)=>'<div class="item"><div class="head"><div><strong>'+esc(s.id)+' · '+esc(s.name)+'</strong><div class="small muted">Criada: '+esc(s.createdAt)+' · Última atividade: '+esc(s.lastActivity)+'</div></div>'+badge(s.status)+'</div><div class="small">'+esc(s.summary||'—')+'</div><div class="small muted" style="margin-top:8px">'+esc(s.path||'—')+'</div><div class="actions"><button class="secondary" onclick="openSession(\\''+esc(s.path)+'\\')">Abrir sessão</button></div></div>');
429
- const repo=Object.entries(ctx.repositoryContext||{}).map(([k,v])=>'<div class="item"><strong>'+esc(k.toUpperCase())+'</strong><div class="small" style="margin-top:8px">'+esc(v.summary||'Sem resumo disponível.')+'</div><div class="small muted" style="margin-top:8px">'+esc(v.path||'—')+'</div></div>');
430
- const blockers=(ctx.readiness.blockers||[]).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const warnings=(ctx.readiness.warnings||[]).slice(0,8).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const evidence=(ctx.runtime.parsed.evidence||[]).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const blockages=(ctx.runtime.parsed.blockages||[]).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const history=((ctx.sessions.current&&ctx.sessions.current.history)||[]).map((x)=>'<div class="entry small">'+esc(x.date)+' · '+esc(x.event)+'</div>');
431
- app.innerHTML='<div class="stats"><div class="metric"><div class="label">Saúde lógica</div><div class="value">'+badge(ctx.healthStatus)+'</div></div><div class="metric"><div class="label">Go / No-Go</div><div class="value">'+badge(ctx.readiness.decision,ctx.readiness.decision)+'</div></div><div class="metric"><div class="label">Review</div><div class="value">'+badge(ctx.planReviewStatus)+'</div></div><div class="metric"><div class="label">Checkpoints</div><div class="value">'+badge(ctx.readiness.checkpointPending?'pending_approval':'clear',ctx.readiness.checkpointPending?'pendente':'ok')+'</div></div><div class="metric"><div class="label">Runtime</div><div class="value">'+badge(ctx.runtime.parsed.status||ctx.state.parsed.runtimeStatus||'pending')+'</div></div></div>'
432
- +'<div class="tabs">'+['decision','plan','execution','evidence','sessions','repository'].map((t)=>'<button class="tab'+(tab===t?' active':'')+'" onclick="setTab(\\''+t+'\\')">'+(t==='repository'?'repository context':t)+'</button>').join('')+'</div>'
433
- +'<div class="view'+(tab==='decision'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Decision Control</h2><div class="muted">A pergunta principal: podemos executar agora?</div></div></div><div class="'+(ctx.readiness.go?'panel go':'panel nogo')+'" style="padding:18px"><div class="label">Decisão operacional</div><div style="font-size:38px;font-weight:900;margin-top:8px">'+(ctx.readiness.go?'GO':'NO-GO')+'</div><div class="small muted" style="margin-top:8px">'+esc(ctx.nextStep.reason||'—')+'</div><div class="actions">'+badge(ctx.planReviewStatus,'review:'+ctx.planReviewStatus)+badge(ctx.readiness.confidenceBand,'confiança:'+ctx.readiness.confidenceBand)+(ctx.readiness.checkpointPending?badge('pending_approval','checkpoint pendente'):'')+'</div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Decision Rail</h2><div class="muted">Spec → Plan → Review → Execute → Verify → Lessons.</div></div></div><div class="rail">'+flow.join('')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Readiness Map</h2><div class="muted">Bloqueios e sinais ativos antes do execute.</div></div></div><div class="grid2"><div><h3>Bloqueios</h3>'+list(blockers,'Sem bloqueios formais para executar.')+'</div><div><h3>Warnings</h3>'+list(warnings,'Sem warnings relevantes.')+'</div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Review e aprovação</h2><div class="muted">Persistida em STATE.md, PLAN-REVIEW.md e comentários.</div></div></div><div class="grid2"><div><div class="actions"><button class="primary" onclick="changeReview(\\'approved\\')">Aprovar</button><button class="warnbtn" onclick="changeReview(\\'needs_revision\\')">Pedir revisão</button><button class="secondary" onclick="changeReview(\\'in_review\\')">Marcar em revisão</button><button class="danger" onclick="changeReview(\\'rejected\\')">Rejeitar</button></div><div class="form"><input id="comment-target" placeholder="target ex.: wave:1, T2, checkpoint:CP-01"/><select id="comment-type"><option value="note">note</option><option value="risk">risk</option><option value="question">question</option><option value="approval">approval</option></select><textarea id="comment-text" rows="6" placeholder="Comentário de revisão"></textarea><button class="primary" onclick="addComment()">Adicionar comentário</button></div></div><div><h3>Comentários</h3>'+list(comments,'Nenhum comentário de revisão.')+'</div></div></div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Mapa de artefatos</h2><div class="muted">Fonte de verdade visual do ciclo atual.</div></div></div><div class="grid3">'+graph.join('')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Calibração do plano</h2><div class="muted">Confiança prevista versus resultado real.</div></div>'+badge(ctx.calibration.status)+'</div><div class="small">'+esc(ctx.calibration.summary)+'</div></div></div></div></div>'
434
- +'<div class="view'+(tab==='plan'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Trilha do plano</h2><div class="muted">Ondas, dependências e vínculos com aceite e decisões.</div></div>'+badge(ctx.planReviewStatus)+'</div>'+list(waves,'PLAN.md ausente ou sem tarefas estruturadas.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Matriz de cobertura</h2><div class="muted">SPEC → PLAN → VERIFY em um único quadro.</div></div></div><div class="grid2">'+coverage.join('')+'</div></div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Autoavaliação do plano</h2><div class="muted">Melhor plano atual e faixa de confiança.</div></div></div><div class="grid3"><div class="item"><strong>Melhor plano atual</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.plan.selfEvaluation&&ctx.plan.selfEvaluation.bestPlan)||'—')+'</div></div><div class="item"><strong>Confiança</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.readiness.confidence!=null?ctx.readiness.confidence+'%':'—')+'</div></div><div class="item"><strong>Faixa</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.readiness.confidenceBand)+'</div></div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Lacunas da SPEC</h2><div class="muted">Critérios sem cobertura explícita no plano.</div></div></div>'+list((ctx.spec.uncoveredCriteria||[]).map((c)=>'<div class="entry small">'+esc(c.id)+' · '+esc(c.criterion)+'</div>'),'Sem lacunas detectadas.')+'</div></div></div></div>'
435
- +'<div class="view'+(tab==='execution'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Runtime operacional</h2><div class="muted">Estado tático da execução.</div></div>'+badge(ctx.runtime.parsed.status||'pending')+'</div><div class="grid2"><div class="item"><strong>Onda atual</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.runtime.parsed.currentWave||'—')+'</div></div><div class="item"><strong>Tarefas ativas</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.runtime.parsed.activeTasks||[]).join(', ')||'—')+'</div></div><div class="item"><strong>Próxima ação</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.runtime.parsed.nextAction||'—')+'</div></div><div class="item"><strong>Motivo</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.runtime.parsed.nextReason||'—')+'</div></div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Checkpoints</h2><div class="muted">Gates humanos formais.</div></div></div>'+list(checkpoints,'CHECKPOINTS.md ausente ou sem gates formais.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Agentes</h2><div class="muted">Blueprint e runtime multiagente.</div></div></div>'+list(agents,'Sem blueprint de agentes ou runtime multiagente ativo.')+'</div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Evidências</h2><div class="muted">Artefatos e sinais da execução atual.</div></div></div>'+list(evidence,'Sem evidência operacional registrada.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Bloqueios</h2><div class="muted">O que impede avanço agora.</div></div></div>'+list(blockages,'Sem bloqueios explícitos.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Runtime bruto</h2><div class="muted">Leitura direta do artefato operacional.</div></div></div><div class="raw">'+esc(ctx.runtime.summary||'EXECUTION-RUNTIME.md ausente.')+'</div></div></div></div></div>'
436
- +'<div class="view'+(tab==='evidence'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Verify</h2><div class="muted">Evidência final e validação.</div></div>'+badge(ctx.calibration.status)+'</div><div class="raw">'+esc(ctx.verify.summary||'VERIFY.md ausente.')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Critérios no verify</h2><div class="muted">Evidências detectadas por critério A*.</div></div></div><div class="grid2">'+coverage.join('')+'</div></div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Investigações</h2><div class="muted">Base de evidência antes do plano.</div></div></div><div class="raw">'+esc(ctx.support.investigationsSummary||'INVESTIGATIONS.md ausente.')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Capabilities</h2><div class="muted">Recursos nativos do projeto.</div></div></div><div class="raw">'+esc(ctx.support.capabilitiesSummary||'CAPABILITIES.md ausente.')+'</div></div></div></div></div>'
437
- +'<div class="view'+(tab==='sessions'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Sessão ativa</h2><div class="muted">Contexto atual resolvido pelo STATE global.</div></div></div><div class="grid2"><div class="item"><strong>ID</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.id)||'—')+'</div></div><div class="item"><strong>Nome</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.name)||ctx.activeSession||'—')+'</div></div><div class="item"><strong>Status</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.status)||'—')+'</div></div><div class="item"><strong>Última atividade</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.lastActivity)||'—')+'</div></div></div><div class="item" style="margin-top:14px"><strong>Resumo</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.summary)||'Sem SESSION.md ativo ou modo legado.')+'</div><div class="small muted" style="margin-top:8px">'+esc(ctx.sessions.currentPath||'—')+'</div></div><div class="item" style="margin-top:14px"><strong>Tags</strong><div class="small" style="margin-top:8px">'+((ctx.sessions.current&&ctx.sessions.current.tags)||[]).map((x)=>'<span class="chip">'+esc(x)+'</span>').join('')+'</div></div><h3>Histórico</h3>'+list(history,'Sem histórico de sessão disponível.')+'</div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Lista de sessões</h2><div class="muted">Índice real lido de .oxe/SESSIONS.md.</div></div></div>'+list(sessions,'SESSIONS.md ausente ou sem sessões registradas.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Índice bruto</h2><div class="muted">Leitura direta do artefato global.</div></div></div><div class="raw">'+esc(ctx.sessions.raw||'SESSIONS.md ausente.')+'</div></div></div></div></div>'
438
- +'<div class="view'+(tab==='repository'?' active':'')+'"><div class="panel"><div class="head"><div><h2 style="margin:0">Repository Context</h2><div class="muted">Contexto de repositório como suporte de decisão.</div></div></div><div class="grid3">'+repo.join('')+'</div></div></div>'; enhanceExecutionView();}
439
- function executionViewElement(){return Array.from(app.querySelectorAll('.view')).find((view)=>view.textContent.includes('Runtime operacional'));}
440
- function panel(title,body){return '<div class="panel"><div class="head"><div><h2 style="margin:0">'+esc(title)+'</h2></div></div>'+body+'</div>';}
441
- function kvItem(label,value){return '<div class="item"><strong>'+esc(label)+'</strong><div class="small muted" style="margin-top:8px">'+esc(value)+'</div></div>';}
442
- function actionButton(label,action,klass){return '<button class="'+klass+'" '+(runtimeDisabled()?'disabled':'')+' onclick="runtimeAction(\\''+action+'\\')">'+label+'</button>';}
443
- function enhanceExecutionView(){
444
- const view=executionViewElement();
445
- if(!view||!ctx)return;
446
- const stacks=view.querySelectorAll('.stack');
447
- if(stacks.length<2)return;
448
- const activeRun=ctx.activeRun||{};
449
- const graph=ctx.operationalGraph||{nodes:[],edges:[]};
450
- const trace=((ctx.tracing&&ctx.tracing.events)||[]).slice(-8).reverse().map((event)=>'<div class="entry"><div class="head"><strong>'+esc(event.type)+'</strong>'+badge(event.timestamp?event.timestamp.slice(11,19):'—',event.timestamp?event.timestamp.slice(11,19):'—')+'</div><div class="small muted">run='+esc(event.run_id||'')+' · wave='+esc(event.wave_id||'—')+' · task='+esc(event.task_id||'—')+'</div><div class="small" style="margin-top:8px">'+esc(JSON.stringify(event.payload||{}))+'</div></div>').join('')||'<div class="entry small">Sem eventos registrados.</div>';
451
- const nodes=(graph.nodes||[]).map((node)=>'<div class="item"><div class="head"><strong>'+esc(node.label||node.id)+'</strong>'+badge(node.status||'pending')+'</div><div class="small muted">'+esc(node.kind||'node')+'</div><div class="small" style="margin-top:8px">'+esc(node.detail||'—')+'</div></div>').join('')||'<div class="entry small">Sem nós operacionais.</div>';
452
- const edges=(graph.edges||[]).map((edge)=>'<div class="entry"><div class="small"><strong>'+esc(edge.from)+'</strong> → <strong>'+esc(edge.to)+'</strong></div><div class="small muted" style="margin-top:6px">'+esc(edge.type||'link')+' · '+esc(edge.status||'')+'</div><div class="small" style="margin-top:6px">'+esc(edge.reason||edge.label||'')+'</div></div>').join('')||'<div class="entry small">Sem handoffs ou dependências registradas.</div>';
453
- const memory=Object.entries(ctx.memoryLayers||{}).filter(([key])=>key!=='readOrder').map(([key,val])=>kvItem(key,Array.isArray(val.source)?val.source.join(' | '):val.source||'—')).join('')||'<div class="entry small">Sem contrato de memória disponível.</div>';
454
- const azure=ctx.azure||null;
455
- const azureInventory=azure&&azure.inventorySummary?azure.inventorySummary:{total:0,servicebus:0,eventgrid:0,sql:0,other:0};
456
- const azurePanel=azure?panel('Azure context operacional','<div class="grid3">'+kvItem('Login',azure.authStatus&&azure.authStatus.login_active?'ativo':'ausente')+kvItem('Subscription',azure.profile&&(azure.profile.subscription_name||azure.profile.subscription_id)||'—')+kvItem('Último sync',azure.inventorySyncedAt||'—')+kvItem('Service Bus',String(azureInventory.servicebus||0))+kvItem('Event Grid',String(azureInventory.eventgrid||0))+kvItem('Azure SQL',String(azureInventory.sql||0))+'</div><div class="small muted" style="margin-top:12px">Inventário: total '+esc(azureInventory.total||0)+' · pending operations: '+esc(azure.pendingOperations||0)+' · '+esc(azure.inventoryStale&&azure.inventoryStale.stale?'stale':'fresh')+'</div>'+(azure.lastOperation?'<div class="entry" style="margin-top:12px"><div class="head"><strong>Última operação</strong>'+badge(azure.lastOperation.phase||'pending')+'</div><div class="small">'+esc(azure.lastOperation.summary||azure.lastOperation.operation_id||'—')+'</div><div class="small muted" style="margin-top:8px">'+esc(azure.lastOperation.operation_id||'—')+'</div></div>':'')):'';
457
- stacks[0].insertAdjacentHTML('afterbegin',panel('Run control','<div class="grid3">'+kvItem('Run ID',activeRun.run_id||'—')+kvItem('Status',activeRun.status||'pending')+kvItem('Cursor',(activeRun.cursor&&((activeRun.cursor.wave!=null?'wave '+activeRun.cursor.wave:'')+(activeRun.cursor.task?' · '+activeRun.cursor.task:'')+(activeRun.cursor.mode?' · '+activeRun.cursor.mode:'')))||'—')+'</div><div class="actions">'+actionButton('Start','start','primary')+actionButton('Pause','pause','warnbtn')+actionButton('Resume','resume','secondary')+actionButton('Replay','replay','danger')+'</div><div class="small muted" style="margin-top:12px">Transitions: '+esc((activeRun.metrics&&activeRun.metrics.transitions)||0)+' · pause_count: '+esc((activeRun.metrics&&activeRun.metrics.pause_count)||0)+' · replay_count: '+esc((activeRun.metrics&&activeRun.metrics.replay_count)||0)+'</div>'));
458
- stacks[0].insertAdjacentHTML('beforeend',panel('Mapa operacional','<div class="grid3">'+nodes+'</div><h3>Handoffs e dependências</h3>'+edges)+(azurePanel||''));
459
- stacks[1].insertAdjacentHTML('afterbegin',panel('Trace operacional','<div class="small muted" style="margin-bottom:12px">Arquivo: '+esc((ctx.tracing&&ctx.tracing.path)||'')+' · total: '+esc((ctx.tracing&&ctx.tracing.summary&&ctx.tracing.summary.total)||0)+' evento(s)</div>'+trace));
460
- stacks[1].insertAdjacentHTML('beforeend',panel('Camadas de memória','<div class="small muted" style="margin-bottom:12px">'+esc(((ctx.memoryLayers&&ctx.memoryLayers.readOrder)||[]).join(' → ')||'—')+'</div><div class="grid2">'+memory+'</div>'));
461
- }
462
- function setTab(next){tab=next;render();}
463
- async function openSession(path){session=path||null;await refresh();tab='sessions';render();}
464
- async function changeReview(status){const note=prompt('Justificativa curta para o novo estado do plano:',status==='approved'?'Plano aprovado para execução.':'');if(note==null)return;await api('/api/review/status',{method:'POST',body:JSON.stringify({status,note,author:'user',activeSession:session})});await refresh();}
465
- async function addComment(){const target=document.getElementById('comment-target').value||'plan';const type=document.getElementById('comment-type').value||'note';const text=document.getElementById('comment-text').value.trim();if(!text)return alert('Escreva um comentário.');await api('/api/review/comment',{method:'POST',body:JSON.stringify({target,type,text,author:'user',activeSession:session})});document.getElementById('comment-text').value='';await refresh();}
466
- async function resolveComment(id){await api('/api/review/comment-status',{method:'POST',body:JSON.stringify({commentId:id,status:'resolved',activeSession:session})});await refresh();}
467
- async function runtimeAction(action){if(runtimeDisabled())return;const reason=prompt('Motivo da transição de runtime:',action==='start'?'iniciar execução':'');if(reason==null)return;const wave=prompt('Onda atual (opcional):',ctx.activeRun&&ctx.activeRun.current_wave!=null?String(ctx.activeRun.current_wave):'');if(wave===null)return;const task=prompt('Tarefa atual (opcional):',ctx.activeRun&&ctx.activeRun.cursor&&ctx.activeRun.cursor.task?String(ctx.activeRun.cursor.task):'');if(task===null)return;await api('/api/runtime/action',{method:'POST',body:JSON.stringify({action,reason,activeSession:session,wave:wave?Number(wave):null,task:task||null})});await refresh();}
468
- window.setTab=setTab;window.openSession=openSession;window.changeReview=changeReview;window.addComment=addComment;window.resolveComment=resolveComment;window.runtimeAction=runtimeAction;refresh().catch((err)=>{app.innerHTML='<div class="panel"><h2>Erro</h2><div class="muted">'+esc(err.message)+'</div></div>';});
469
- </script></body></html>`;
470
- }
471
-
472
- function readJsonBody(req) {
473
- return new Promise((resolve, reject) => {
474
- let raw = '';
475
- req.on('data', (chunk) => {
476
- raw += chunk;
477
- if (raw.length > 1024 * 1024) {
478
- reject(new Error('Payload demasiado grande.'));
479
- req.destroy();
480
- }
481
- });
482
- req.on('end', () => {
483
- if (!raw.trim()) return resolve({});
484
- try {
485
- resolve(JSON.parse(raw));
486
- } catch (err) {
487
- reject(new Error('JSON inválido.'));
488
- }
489
- });
490
- req.on('error', reject);
491
- });
492
- }
493
-
494
- function sendJson(res, status, body) {
495
- const payload = JSON.stringify(body, null, 2);
496
- res.writeHead(status, {
497
- 'Content-Type': 'application/json; charset=utf-8',
498
- 'Content-Length': Buffer.byteLength(payload),
499
- 'Cache-Control': 'no-store',
500
- });
501
- res.end(payload);
502
- }
503
-
504
- function createDashboardServer(projectRoot, opts = {}) {
505
- const root = path.resolve(projectRoot);
506
- const server = http.createServer(async (req, res) => {
507
- try {
508
- const reqUrl = new URL(req.url || '/', 'http://127.0.0.1');
509
- if (req.method === 'GET' && reqUrl.pathname === '/') {
510
- const html = dashboardHtml();
511
- res.writeHead(200, {
512
- 'Content-Type': 'text/html; charset=utf-8',
513
- 'Content-Length': Buffer.byteLength(html),
514
- 'Cache-Control': 'no-store',
515
- });
516
- res.end(html);
517
- return;
518
- }
519
-
520
- if (req.method === 'GET' && reqUrl.pathname === '/api/context') {
521
- const activeSession = reqUrl.searchParams.get('session') || opts.activeSession || null;
522
- sendJson(res, 200, { ...loadDashboardContext(root, { activeSession }), dashboard: { readOnly: Boolean(opts.readOnly) } });
523
- return;
524
- }
525
-
526
- if (opts.readOnly && req.method === 'POST' && (reqUrl.pathname.startsWith('/api/review/') || reqUrl.pathname.startsWith('/api/runtime/'))) {
527
- sendJson(res, 403, { error: 'Dashboard em modo read-only.' });
528
- return;
529
- }
530
-
531
- if (req.method === 'POST' && reqUrl.pathname === '/api/review/status') {
532
- const body = await readJsonBody(req);
533
- const result = savePlanReviewStatus(root, body || {});
534
- sendJson(res, 200, result);
535
- return;
536
- }
537
-
538
- if (req.method === 'POST' && reqUrl.pathname === '/api/review/comment') {
539
- const body = await readJsonBody(req);
540
- const result = addPlanReviewComment(root, body || {});
541
- sendJson(res, 200, result);
542
- return;
543
- }
544
-
545
- if (req.method === 'POST' && reqUrl.pathname === '/api/review/comment-status') {
546
- const body = await readJsonBody(req);
547
- const result = updatePlanReviewCommentStatus(root, body || {});
548
- if (!result) {
549
- sendJson(res, 404, { error: 'Comentário não encontrado.' });
550
- return;
551
- }
552
- const stateText = readTextIfExists(health.oxePaths(root).state) || '';
553
- savePlanReviewStatus(root, {
554
- activeSession: body.activeSession,
555
- status: health.parsePlanReviewStatus(stateText) || 'in_review',
556
- note: 'Estado de comentários de revisão atualizado',
557
- author: body.author || 'dashboard',
558
- });
559
- sendJson(res, 200, result);
560
- return;
561
- }
562
-
563
- if (req.method === 'POST' && reqUrl.pathname === '/api/runtime/action') {
564
- const body = await readJsonBody(req);
565
- const result = operational.applyRuntimeAction(root, body.activeSession || opts.activeSession || null, body || {});
566
- sendJson(res, 200, result);
567
- return;
568
- }
569
-
570
- sendJson(res, 404, { error: 'Rota não encontrada.' });
571
- } catch (err) {
572
- sendJson(res, 500, { error: err && err.message ? err.message : 'Erro interno.' });
573
- }
574
- });
575
- return server;
576
- }
577
-
578
- module.exports = {
579
- loadDashboardContext,
580
- savePlanReviewStatus,
581
- addPlanReviewComment,
582
- updatePlanReviewCommentStatus,
583
- createDashboardServer,
584
- parsePlan,
585
- parseSpec,
586
- parseVerify,
587
- buildCoverageMatrix,
588
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const http = require('http');
6
+ const { URL } = require('url');
7
+ const health = require('./oxe-project-health.cjs');
8
+ const operational = require('./oxe-operational.cjs');
9
+ const azure = require('./oxe-azure.cjs');
10
+ const contextEngine = require('./oxe-context-engine.cjs');
11
+
12
+ function readTextIfExists(p) {
13
+ try { return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null; } catch { return null; }
14
+ }
15
+
16
+ function readJsonArrayIfExists(p) {
17
+ try {
18
+ if (!fs.existsSync(p)) return [];
19
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
20
+ return Array.isArray(raw) ? raw : [];
21
+ } catch { return []; }
22
+ }
23
+
24
+ function ensureDirForFile(p) { fs.mkdirSync(path.dirname(p), { recursive: true }); }
25
+ function firstMatch(text, regex) { const m = String(text || '').match(regex); return m ? m[1].trim() : null; }
26
+ function escapeRegex(v) { return v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
27
+ function summarizeText(text, max = 500) { const clean = String(text || '').replace(/\r\n/g, '\n').trim(); return !clean ? '' : clean.length > max ? `${clean.slice(0, max - 1)}…` : clean; }
28
+ function normalizeCell(v) { return String(v || '').replace(/<br\s*\/?>/gi, ' ').replace(/`/g, '').replace(/\s+/g, ' ').trim(); }
29
+ function splitTableRow(line) { return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => normalizeCell(c)); }
30
+
31
+ function parseMarkdownTables(text) {
32
+ const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
33
+ const tables = [];
34
+ for (let i = 0; i < lines.length - 1; i += 1) {
35
+ if (!lines[i].trim().startsWith('|')) continue;
36
+ if (!/^\s*\|?[\-:\s|]+\|?\s*$/.test(lines[i + 1] || '')) continue;
37
+ const headers = splitTableRow(lines[i]);
38
+ const rows = [];
39
+ let j = i + 2;
40
+ while (j < lines.length && lines[j].trim().startsWith('|')) {
41
+ const cells = splitTableRow(lines[j]);
42
+ const row = {};
43
+ for (let k = 0; k < headers.length; k += 1) row[headers[k]] = normalizeCell(cells[k] || '');
44
+ rows.push(row);
45
+ j += 1;
46
+ }
47
+ tables.push({ headers, rows });
48
+ i = j - 1;
49
+ }
50
+ return tables;
51
+ }
52
+
53
+ function findTableByHeaders(text, expected) {
54
+ const wants = expected.map((x) => x.toLowerCase());
55
+ return parseMarkdownTables(text).find((table) => wants.every((w) => table.headers.some((h) => h.toLowerCase() === w)));
56
+ }
57
+
58
+ function listSectionItems(text, heading) {
59
+ const match = String(text || '').match(new RegExp(`##\\s*${escapeRegex(heading)}\\s*\\n+([\\s\\S]*?)(?=\\n##\\s|$)`, 'i'));
60
+ if (!match) return [];
61
+ return match[1].split('\n').map((l) => l.trim()).filter((l) => /^-\s+/.test(l)).map((l) => l.replace(/^-\s+/, '').trim()).filter(Boolean);
62
+ }
63
+
64
+ function parsePlan(planMd) {
65
+ const parts = planMd.split(/^###\s+(T\d+)\s*[—-]\s*/m);
66
+ const tasks = [];
67
+ const waves = {};
68
+ for (let i = 1; i < parts.length; i += 2) {
69
+ const id = parts[i].trim();
70
+ const rest = (parts[i + 1] || '').split(/^###\s+T\d+/m)[0];
71
+ const title = ((rest.match(/^([^\n]+)/) || [null, ''])[1] || '').trim();
72
+ const wave = Number((rest.match(/\*\*Onda:\*\*\s*(\d+)/i) || [null, ''])[1]) || null;
73
+ const dependsOn = (((rest.match(/\*\*Depende\s+de:\*\*\s*([^\n]+)/i) || [null, ''])[1]) || '').split(/[,\s]+/).filter((s) => /^T\d+$/.test(s.trim()));
74
+ const verifyCommand = (rest.match(/Comando:\s*`([^`]+)`/i) || [null, null])[1];
75
+ const aceite = ((((rest.match(/\*\*Aceite\s+vinculado:\*\*\s*([^\n]+)/i) || [null, ''])[1]) || '').match(/A\d+/g) || []);
76
+ const decisions = ((((rest.match(/\*\*Decisão\s+vinculada:\*\*\s*([^\n]+)/i) || [null, ''])[1]) || '').match(/D-\d+/g) || []);
77
+ const task = { id, title, wave, dependsOn, verifyCommand, aceite, decisions };
78
+ tasks.push(task);
79
+ if (wave != null) (waves[wave] ||= []).push(id);
80
+ }
81
+ return {
82
+ tasks,
83
+ totalTasks: tasks.length,
84
+ waves: Object.keys(waves).map((k) => ({ wave: Number(k), taskIds: waves[k], tasks: waves[k].map((id) => tasks.find((t) => t.id === id)).filter(Boolean) })).sort((a, b) => a.wave - b.wave),
85
+ };
86
+ }
87
+
88
+ function parseSpec(specMd) {
89
+ const objective = (((specMd.match(/##\s*Objetivo\s*\n+([\s\S]*?)(?=\n##\s|\n#[^\#]|$)/im) || [null, ''])[1]) || '').trim().split('\n')[0].trim() || null;
90
+ const criteria = [];
91
+ const m = specMd.match(/##\s*Critérios.*?aceite[\s\S]*?(\|[\s\S]*?)(?=\n##\s|\n#[^\#]|$)/im);
92
+ if (m) {
93
+ for (const row of m[1].split('\n').filter((l) => l.startsWith('|'))) {
94
+ const cells = row.split('|').map((c) => c.trim()).filter(Boolean);
95
+ if (cells.length >= 2 && /^A\d+$/i.test(cells[0])) criteria.push({ id: cells[0].toUpperCase(), criterion: cells[1] || '', howToVerify: cells[2] || '' });
96
+ }
97
+ }
98
+ return { objective, criteria };
99
+ }
100
+
101
+ function parseRuntime(runtimeMd) {
102
+ const agentTable = findTableByHeaders(runtimeMd, ['ID', 'Papel', 'Tarefas', 'Estado']);
103
+ const checkpointTable = findTableByHeaders(runtimeMd, ['ID', 'Tipo', 'Escopo', 'Estado']);
104
+ return {
105
+ currentWave: Number(firstMatch(runtimeMd, /\*\*Onda:\*\*\s*([^\n]+)/i)) || firstMatch(runtimeMd, /\*\*Onda:\*\*\s*([^\n]+)/i),
106
+ status: firstMatch(runtimeMd, /\*\*Estado:\*\*\s*([^\n]+)/i),
107
+ activeTasks: ((firstMatch(runtimeMd, /\*\*Tarefas ativas:\*\*\s*([^\n]+)/i) || '').split(',').map((x) => x.trim()).filter(Boolean)),
108
+ nextAction: firstMatch(runtimeMd, /\*\*Ação:\*\*\s*([^\n]+)/i),
109
+ nextReason: firstMatch(runtimeMd, /\*\*Motivo:\*\*\s*([^\n]+)/i),
110
+ agents: (agentTable?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', role: row.Papel || row.Role || '—', tasks: (row.Tarefas || row.Tasks || '').split(',').map((x) => x.trim()).filter(Boolean), status: row.Estado || row.Status || '—' })),
111
+ checkpoints: (checkpointTable?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', type: row.Tipo || row.Type || '—', scope: row.Escopo || row.Scope || '—', status: row.Estado || row.Status || '—', decision: row.Decisão || row.Decisao || row.Decision || '—' })),
112
+ evidence: listSectionItems(runtimeMd, 'Evidências produzidas'),
113
+ blockages: listSectionItems(runtimeMd, 'Bloqueios').filter((x) => !/^(\(nenhum\)|nenhum)$/i.test(x)),
114
+ };
115
+ }
116
+
117
+ function parseCheckpointsIndex(md) {
118
+ const table = findTableByHeaders(md, ['ID', 'Tipo', 'Fase', 'Escopo', 'Estado']);
119
+ return (table?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', type: row.Tipo || row.Type || '—', phase: row.Fase || row.Phase || '—', scope: row.Escopo || row.Scope || '—', status: row.Estado || row.Status || '—', notes: row.Notas || row.Notes || '—' }));
120
+ }
121
+
122
+ function parseSessionsIndex(md) {
123
+ const table = parseMarkdownTables(md).find((t) => {
124
+ const h = t.headers.map((x) => x.toLowerCase());
125
+ return h.includes('id') && h.includes('nome') && h.includes('status') && h.includes('path');
126
+ });
127
+ return (table?.rows || []).map((row) => ({ id: row.ID || row.Id || '—', name: row.Nome || row.Name || '—', status: row.Status || '—', createdAt: row.Criada || row.Created || '—', lastActivity: row['Última atividade'] || row['Ultima atividade'] || row['Last activity'] || '—', summary: row.Resumo || row.Summary || '—', path: (row.Path || row.Caminho || '—').replace(/`/g, '') }));
128
+ }
129
+
130
+ function parseSessionDetail(md) {
131
+ return { id: firstMatch(md, /\*\*ID:\*\*\s*([^\n]+)/i), name: firstMatch(md, /\*\*Nome:\*\*\s*([^\n]+)/i), status: firstMatch(md, /\*\*Status:\*\*\s*([^\n]+)/i), createdAt: firstMatch(md, /\*\*Criada:\*\*\s*([^\n]+)/i), lastActivity: firstMatch(md, /\*\*(?:Última|Ultima) atividade:\*\*\s*([^\n]+)/i), summary: firstMatch(md, /\*\*Resumo:\*\*\s*([^\n]+)/i), tags: listSectionItems(md, 'Tags'), history: (findTableByHeaders(md, ['Data', 'Evento'])?.rows || []).map((row) => ({ date: row.Data || row.Date || '—', event: row.Evento || row.Event || '—' })) };
132
+ }
133
+
134
+ function normalizeEvidenceStatus(raw) {
135
+ const v = String(raw || '').toLowerCase();
136
+ if (!v) return 'mentioned';
137
+ if (/(ok|pass|aprov|sim|true|done|conclu)/.test(v)) return 'passed';
138
+ if (/(fail|falh|reprov|não|nao|false|blocked)/.test(v)) return 'failed';
139
+ return 'mentioned';
140
+ }
141
+
142
+ function parseVerify(md) {
143
+ const criteria = [];
144
+ for (const table of parseMarkdownTables(md)) {
145
+ for (const row of table.rows) {
146
+ const idCandidate = row.ID || row.Id || row['Critério'] || row['Criterio'] || '';
147
+ const match = String(idCandidate).match(/A\d+/i);
148
+ if (!match) continue;
149
+ criteria.push({ id: match[0].toUpperCase(), status: normalizeEvidenceStatus(row.Status || row.Resultado || row.Result || row.Veredito || row['Passou?'] || ''), summary: row.Evidência || row.Evidencia || row.Resumo || row.Notas || '' });
150
+ }
151
+ }
152
+ return { criteria, mentionedCriteria: Array.from(new Set((md.match(/\bA\d+\b/g) || []).map((x) => x.toUpperCase()))), failed: /\b(verify_failed|falhou|falha|reprovad)\b/i.test(md), passed: /\b(verify_complete|aprovad|passou|sucesso)\b/i.test(md) };
153
+ }
154
+
155
+ function confidenceBand(confidence, threshold) {
156
+ if (confidence == null) return 'unknown';
157
+ if (confidence >= 85) return 'ready';
158
+ if (confidence >= threshold) return 'controlled';
159
+ if (confidence >= 50) return 'needs_refinement';
160
+ return 'do_not_execute';
161
+ }
162
+
163
+ function computeReadiness(ctx, threshold) {
164
+ const blockers = [];
165
+ const warnings = [...ctx.diagnostics.reviewWarnings, ...ctx.diagnostics.runtimeWarnings, ...ctx.diagnostics.planWarnings];
166
+ if (ctx.planReviewStatus !== 'approved') blockers.push(`review_status:${ctx.planReviewStatus || 'draft'}`);
167
+ if (ctx.plan.selfEvaluation.bestPlan === 'não') blockers.push('best_plan:no');
168
+ if (ctx.plan.selfEvaluation.confidence == null) blockers.push('confidence:missing');
169
+ else if (ctx.plan.selfEvaluation.confidence < threshold) blockers.push(`confidence:${ctx.plan.selfEvaluation.confidence}%<${threshold}%`);
170
+ if (ctx.checkpoints.parsed.some((x) => /pending_approval/i.test(x.status))) blockers.push('checkpoint:pending_approval');
171
+ if (ctx.runtime.parsed.status === 'blocked') blockers.push('runtime:blocked');
172
+ if (ctx.spec.uncoveredCriteria.length) warnings.push(`${ctx.spec.uncoveredCriteria.length} critérios sem cobertura no plano`);
173
+ return {
174
+ go: blockers.length === 0,
175
+ decision: blockers.length === 0 ? 'go' : 'no-go',
176
+ threshold,
177
+ confidence: ctx.plan.selfEvaluation.confidence,
178
+ confidenceBand: confidenceBand(ctx.plan.selfEvaluation.confidence, threshold),
179
+ checkpointPending: blockers.includes('checkpoint:pending_approval'),
180
+ blockers,
181
+ warnings,
182
+ };
183
+ }
184
+
185
+ function buildCoverageMatrix(spec, plan, verify) {
186
+ const taskMap = new Map();
187
+ for (const task of plan.tasks) {
188
+ for (const criterion of task.aceite || []) {
189
+ if (!taskMap.has(criterion)) taskMap.set(criterion, []);
190
+ taskMap.get(criterion).push(task.id);
191
+ }
192
+ }
193
+ const verifyMap = new Map((verify.criteria || []).map((x) => [x.id, x]));
194
+ return spec.criteria.map((c) => ({
195
+ id: c.id,
196
+ criterion: c.criterion,
197
+ verifyHow: c.howToVerify,
198
+ tasks: taskMap.get(c.id) || [],
199
+ planCovered: taskMap.has(c.id),
200
+ verifyStatus: verifyMap.has(c.id) ? verifyMap.get(c.id).status : (verify.mentionedCriteria || []).includes(c.id) ? 'mentioned' : 'missing',
201
+ verifySummary: verifyMap.has(c.id) ? verifyMap.get(c.id).summary : '',
202
+ }));
203
+ }
204
+
205
+ function computeCalibration(phase, confidence, verify) {
206
+ if (confidence == null) return { status: 'pending', summary: 'Calibração indisponível antes do verify.' };
207
+ const low = String(phase || '').toLowerCase();
208
+ const completed = low === 'verify_complete' || verify.passed;
209
+ const failed = low === 'verify_failed' || verify.failed;
210
+ if (!completed && !failed) return { status: 'pending', summary: 'Calibração fecha após verify.' };
211
+ if (confidence >= 85 && failed) return { status: 'overconfident', summary: `Confiança ${confidence}% alta, mas o verify falhou.` };
212
+ if (confidence < 70 && failed) return { status: 'calibrated-risk', summary: `O plano sinalizava risco (${confidence}%) e o verify confirmou a fragilidade.` };
213
+ if (confidence < 70 && completed) return { status: 'underconfident', summary: `O resultado final foi melhor que a confiança inicial (${confidence}%).` };
214
+ if (confidence >= 85 && completed) return { status: 'well-calibrated', summary: `Alta confiança (${confidence}%) e verify coerente com a expectativa.` };
215
+ return { status: 'acceptable', summary: `Confiança ${confidence}% e verify dentro da faixa esperada.` };
216
+ }
217
+
218
+ function readRepositoryContext(codebaseDir) {
219
+ const names = ['OVERVIEW.md', 'STACK.md', 'STRUCTURE.md', 'TESTING.md', 'CONCERNS.md', 'INTEGRATIONS.md'];
220
+ const out = {};
221
+ for (const name of names) out[name.replace('.md', '').toLowerCase()] = { path: path.join(codebaseDir, name), summary: summarizeText(readTextIfExists(path.join(codebaseDir, name)) || '', 420) };
222
+ return out;
223
+ }
224
+
225
+ function ensureStateSection(stateText, sectionTitle) {
226
+ return new RegExp(`##\\s*${escapeRegex(sectionTitle)}`, 'i').test(stateText) ? stateText : `${stateText.trimEnd()}\n\n## ${sectionTitle}\n\n`;
227
+ }
228
+
229
+ function upsertBulletInSection(body, label, value) {
230
+ const line = `- **${label}:** ${value}`;
231
+ const re = new RegExp(`^- \\*\\*${escapeRegex(label)}:\\*\\*.*$`, 'im');
232
+ if (re.test(body)) return body.replace(re, line);
233
+ const trimmed = body.trimEnd();
234
+ return !trimmed ? `${line}\n` : `${trimmed}\n${line}\n`;
235
+ }
236
+
237
+ function upsertStateBullet(stateText, sectionTitle, label, value) {
238
+ const ensured = ensureStateSection(stateText, sectionTitle);
239
+ const re = new RegExp(`(##\\s*${escapeRegex(sectionTitle)}\\s*\\n+)([\\s\\S]*?)(?=\\n##\\s|$)`, 'i');
240
+ return ensured.replace(re, (m, head, body) => `${head}${upsertBulletInSection(body, label, value)}`);
241
+ }
242
+
243
+ function reviewPaths(projectRoot, activeSession) {
244
+ const p = health.scopedOxePaths(projectRoot, activeSession);
245
+ return { ...p, state: health.oxePaths(projectRoot).state, reviewJson: p.planReviewComments };
246
+ }
247
+
248
+ function readScopedText(primaryPath, fallbackPath) {
249
+ const primary = readTextIfExists(primaryPath);
250
+ if (primary) return primary;
251
+ return fallbackPath && fallbackPath !== primaryPath ? readTextIfExists(fallbackPath) || '' : '';
252
+ }
253
+
254
+ function savePlanReviewStatus(projectRoot, input = {}) {
255
+ const globalStatePath = health.oxePaths(projectRoot).state;
256
+ const stateText = readTextIfExists(globalStatePath) || '# OXE Estado\n';
257
+ const activeSession = input.activeSession === undefined ? health.parseActiveSession(stateText) : input.activeSession;
258
+ const p = reviewPaths(projectRoot, activeSession || null);
259
+ const nowIso = new Date().toISOString();
260
+ const status = String(input.status || 'draft');
261
+ const note = String(input.note || '');
262
+ const author = String(input.author || 'dashboard');
263
+ const reviewRef = path.relative(path.join(projectRoot, '.oxe'), p.planReview).replace(/\\/g, '/');
264
+ let nextState = stateText;
265
+ nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'plan_review_status', `\`${status}\``);
266
+ nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'plan_review_updated', `\`${nowIso}\``);
267
+ nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'plan_review_ref', `\`${reviewRef}\``);
268
+ nextState = upsertStateBullet(nextState, 'Revisão do plano (opcional — dashboard / aprovação)', 'Notas', note);
269
+ ensureDirForFile(globalStatePath);
270
+ fs.writeFileSync(globalStatePath, nextState, 'utf8');
271
+ const comments = readJsonArrayIfExists(p.reviewJson);
272
+ const format = (c) => `- **${c.target || 'plan'}** [${c.type || 'note'} | ${c.status || 'open'}] ${c.text || ''}`;
273
+ const reviewMd =
274
+ `---\noxe_doc: plan_review\nstatus: ${status}\nupdated: ${nowIso.slice(0, 10)}\nplan_ref: ${path.basename(p.plan)}\n---\n\n# OXE — Revisão do Plano\n\n## Estado\n\n- **Status:** ${status}\n- **Atualizado em:** ${nowIso}\n- **Origem:** dashboard local\n- **Autor:** ${author}\n\n## Decisão\n\n- **Resultado:** ${status}\n- **Justificativa:** ${note}\n\n## Comentários abertos\n\n${comments.filter((x) => x.status !== 'resolved').map(format).join('\n') || '- Nenhum'}\n\n## Comentários resolvidos\n\n${comments.filter((x) => x.status === 'resolved').map(format).join('\n') || '- Nenhum'}\n\n## Próxima ação recomendada\n\n${status === 'approved' ? '- `/oxe-execute` ou `oxe-cc status` para seguir a trilha.' : '- `/oxe-plan --replan` ou ajuste do plano antes de executar.'}\n`;
275
+ ensureDirForFile(p.planReview);
276
+ fs.writeFileSync(p.planReview, reviewMd, 'utf8');
277
+ operational.appendEvent(projectRoot, activeSession || null, {
278
+ type: 'plan_review_status_changed',
279
+ payload: { status, note, author, review_ref: reviewRef },
280
+ });
281
+ return { status, updatedAt: nowIso, ref: p.planReview, note, author, activeSession: activeSession || null };
282
+ }
283
+
284
+ function addPlanReviewComment(projectRoot, input = {}) {
285
+ const stateText = readTextIfExists(health.oxePaths(projectRoot).state) || '# OXE Estado\n';
286
+ const activeSession = input.activeSession === undefined ? health.parseActiveSession(stateText) : input.activeSession;
287
+ const p = reviewPaths(projectRoot, activeSession || null);
288
+ const comments = readJsonArrayIfExists(p.reviewJson);
289
+ const next = { id: `c-${Date.now().toString(36)}`, target: String(input.target || 'plan'), type: String(input.type || 'note'), status: 'open', author: String(input.author || 'dashboard'), created_at: new Date().toISOString(), text: String(input.text || '').trim() };
290
+ comments.push(next);
291
+ ensureDirForFile(p.reviewJson);
292
+ fs.writeFileSync(p.reviewJson, JSON.stringify(comments, null, 2), 'utf8');
293
+ operational.appendEvent(projectRoot, activeSession || null, {
294
+ type: 'plan_review_comment_added',
295
+ payload: { comment_id: next.id, target: next.target, comment_type: next.type },
296
+ });
297
+ savePlanReviewStatus(projectRoot, { activeSession, status: health.parsePlanReviewStatus(stateText) || 'in_review', note: 'Há comentários de revisão em aberto', author: String(input.author || 'dashboard') });
298
+ return next;
299
+ }
300
+
301
+ function updatePlanReviewCommentStatus(projectRoot, input = {}) {
302
+ const stateText = readTextIfExists(health.oxePaths(projectRoot).state) || '# OXE Estado\n';
303
+ const activeSession = input.activeSession === undefined ? health.parseActiveSession(stateText) : input.activeSession;
304
+ const p = reviewPaths(projectRoot, activeSession || null);
305
+ const comments = readJsonArrayIfExists(p.reviewJson);
306
+ const idx = comments.findIndex((x) => x.id === input.commentId);
307
+ if (idx === -1) return null;
308
+ comments[idx] = { ...comments[idx], status: String(input.status || 'resolved'), updated_at: new Date().toISOString() };
309
+ fs.writeFileSync(p.reviewJson, JSON.stringify(comments, null, 2), 'utf8');
310
+ operational.appendEvent(projectRoot, activeSession || null, {
311
+ type: 'plan_review_comment_updated',
312
+ payload: { comment_id: comments[idx].id, status: comments[idx].status },
313
+ });
314
+ return comments[idx];
315
+ }
316
+
317
+ function loadDashboardContext(projectRoot, opts = {}) {
318
+ const globalPaths = health.oxePaths(projectRoot);
319
+ const stateText = readTextIfExists(globalPaths.state) || '';
320
+ const activeSession = opts.activeSession === undefined ? health.parseActiveSession(stateText) : opts.activeSession;
321
+ const p = reviewPaths(projectRoot, activeSession || null);
322
+ const rootScoped = reviewPaths(projectRoot, null);
323
+ const report = health.buildHealthReport(projectRoot);
324
+ const specText = readScopedText(p.spec, rootScoped.spec);
325
+ const planText = readScopedText(p.plan, rootScoped.plan);
326
+ const verifyText = readScopedText(p.verify, rootScoped.verify);
327
+ const runtimeText = readScopedText(p.runtime, rootScoped.runtime);
328
+ const checkpointsText = readScopedText(p.checkpoints, rootScoped.checkpoints);
329
+ const spec = parseSpec(specText);
330
+ const plan = parsePlan(planText);
331
+ const runtime = parseRuntime(runtimeText);
332
+ const checkpoints = parseCheckpointsIndex(checkpointsText);
333
+ const verify = parseVerify(verifyText);
334
+ const activeRunState = operational.readRunState(projectRoot, activeSession || null);
335
+ const traceEvents = operational.readEvents(projectRoot, activeSession || null);
336
+ const traceSummary = operational.summarizeEvents(traceEvents);
337
+ const memoryLayers = operational.buildMemoryLayers(projectRoot, activeSession || null);
338
+ const capabilityCatalog = operational.readCapabilityCatalog(projectRoot);
339
+ const azurePaths = azure.azurePaths(projectRoot);
340
+ const dashboardPack = report.contextPacks && report.contextPacks.dashboard
341
+ ? report.contextPacks.dashboard
342
+ : contextEngine.inspectContextPack(projectRoot, { workflow: 'dashboard', activeSession: activeSession || null });
343
+ const sessionsRaw = readTextIfExists(globalPaths.sessionsIndex) || '';
344
+ const sessions = parseSessionsIndex(sessionsRaw);
345
+ const sessionPath = activeSession ? path.join(projectRoot, '.oxe', activeSession, 'SESSION.md') : null;
346
+ const sessionRaw = sessionPath ? readTextIfExists(sessionPath) || '' : '';
347
+ const ctx = {
348
+ projectRoot: path.resolve(projectRoot),
349
+ activeSession: activeSession || null,
350
+ phase: report.phase || health.parseStatePhase(stateText),
351
+ healthStatus: report.healthStatus,
352
+ nextStep: report.next,
353
+ planReviewStatus: report.planReviewStatus || 'draft',
354
+ state: { path: globalPaths.state, raw: stateText, parsed: { phase: health.parseStatePhase(stateText), activeSession, runtimeStatus: firstMatch(stateText, /\*\*runtime_status:\*\*\s*([^\n]+)/i) } },
355
+ spec: { path: p.spec, raw: specText, objective: spec.objective, criteria: spec.criteria, uncoveredCriteria: spec.criteria.filter((c) => !plan.tasks.some((t) => (t.aceite || []).includes(c.id))) },
356
+ plan: { path: p.plan, raw: planText, tasks: plan.tasks, waves: plan.waves, totalTasks: plan.totalTasks, selfEvaluation: report.planSelfEvaluation },
357
+ runtime: { path: p.runtime, raw: runtimeText, summary: summarizeText(runtimeText, 800), parsed: runtime },
358
+ activeRun: activeRunState,
359
+ tracing: { path: operational.operationalPaths(projectRoot, activeSession || null).events, events: traceEvents, summary: traceSummary },
360
+ checkpoints: { path: p.checkpoints, raw: checkpointsText, parsed: checkpoints },
361
+ verify: { path: p.verify, raw: verifyText, summary: summarizeText(verifyText, 800), parsed: verify },
362
+ review: { markdownPath: p.planReview, commentsPath: p.reviewJson, comments: readJsonArrayIfExists(p.reviewJson) },
363
+ sessions: { indexPath: globalPaths.sessionsIndex, raw: sessionsRaw, items: sessions, currentPath: sessionPath, current: sessionRaw ? parseSessionDetail(sessionRaw) : null },
364
+ support: { capabilitiesSummary: summarizeText(readTextIfExists(p.capabilitiesIndex) || '', 420), investigationsSummary: summarizeText(readTextIfExists(p.investigationsIndex) || '', 420) },
365
+ capabilities: capabilityCatalog,
366
+ memoryLayers,
367
+ context: {
368
+ pack: dashboardPack,
369
+ packs: report.contextPacks || {},
370
+ quality: report.contextQuality || null,
371
+ freshness: report.packFreshness || {},
372
+ summaries: report.activeSummaryRefs || {},
373
+ semantics: report.semanticsDrift || null,
374
+ },
375
+ azure: report.azureActive && report.azure
376
+ ? {
377
+ ...report.azure,
378
+ profilePath: azurePaths.profile,
379
+ authStatusPath: azurePaths.authStatus,
380
+ inventoryMarkdownPath: azurePaths.inventoryMd,
381
+ serviceBusPath: azurePaths.serviceBusMd,
382
+ eventGridPath: azurePaths.eventGridMd,
383
+ sqlPath: azurePaths.sqlMd,
384
+ }
385
+ : null,
386
+ diagnostics: { reviewWarnings: report.reviewWarn, runtimeWarnings: report.runtimeWarn, planWarnings: report.planWarn, sessionWarnings: report.sessionWarn, capabilityWarnings: report.capabilityWarn, investigationWarnings: report.investigationWarn },
387
+ repositoryContext: readRepositoryContext(rootScoped.codebase),
388
+ };
389
+ if (ctx.azure) {
390
+ const inventorySummary = ctx.azure.inventorySummary || { total: 0, servicebus: 0, eventgrid: 0, sql: 0 };
391
+ ctx.repositoryContext.azure = {
392
+ path: ctx.azure.inventoryMarkdownPath,
393
+ summary: `login=${ctx.azure.authStatus && ctx.azure.authStatus.login_active ? 'ativo' : 'ausente'} · subscription=${ctx.azure.profile && (ctx.azure.profile.subscription_name || ctx.azure.profile.subscription_id) || '—'} · total=${inventorySummary.total} · sb=${inventorySummary.servicebus || 0} · eg=${inventorySummary.eventgrid || 0} · sql=${inventorySummary.sql || 0}`,
394
+ };
395
+ }
396
+ ctx.readiness = computeReadiness(ctx, 70);
397
+ ctx.coverage = buildCoverageMatrix(ctx.spec, ctx.plan, verify);
398
+ ctx.calibration = computeCalibration(ctx.phase, ctx.plan.selfEvaluation.confidence, verify);
399
+ ctx.visual = {
400
+ flow: { nodes: [{ label: 'STATE', status: 'done' }, { label: 'SPEC', status: ctx.spec.raw ? 'done' : 'pending' }, { label: 'PLAN', status: ctx.plan.raw ? 'done' : 'pending' }, { label: 'REVIEW', status: ctx.planReviewStatus === 'approved' ? 'done' : /(rejected|needs_revision)/i.test(ctx.planReviewStatus) ? 'blocked' : 'active' }, { label: 'EXECUTE', status: ctx.runtime.parsed.status === 'running' ? 'active' : ctx.runtime.raw ? 'done' : 'pending' }, { label: 'CHECKPOINTS', status: ctx.readiness.checkpointPending ? 'active' : ctx.checkpoints.parsed.length ? 'done' : 'pending' }, { label: 'VERIFY', status: ctx.verify.raw ? 'done' : 'pending' }, { label: 'LESSONS', status: 'pending' }] },
401
+ artifactGraph: [{ id: 'state', label: 'STATE', path: ctx.state.path, detail: ctx.phase || 'índice global', status: 'done' }, { id: 'spec', label: 'SPEC', path: ctx.spec.path, detail: ctx.spec.objective || 'contrato', status: ctx.spec.raw ? 'done' : 'pending' }, { id: 'plan', label: 'PLAN', path: ctx.plan.path, detail: `${ctx.plan.totalTasks} tarefas`, status: ctx.plan.raw ? 'done' : 'pending' }, { id: 'review', label: 'PLAN REVIEW', path: ctx.review.markdownPath, detail: ctx.planReviewStatus, status: ctx.planReviewStatus === 'approved' ? 'done' : 'active' }, { id: 'runtime', label: 'RUNTIME', path: ctx.runtime.path, detail: ctx.runtime.parsed.status || 'sem status', status: ctx.runtime.raw ? 'active' : 'pending' }, { id: 'active-run', label: 'ACTIVE RUN', path: operational.operationalPaths(projectRoot, activeSession || null).activeRun, detail: ctx.activeRun && ctx.activeRun.run_id ? `${ctx.activeRun.run_id} · ${ctx.activeRun.status}` : 'sem run ativo', status: ctx.activeRun ? 'active' : 'pending' }, { id: 'events', label: 'TRACE', path: operational.operationalPaths(projectRoot, activeSession || null).events, detail: `${ctx.tracing.summary.total} evento(s)`, status: ctx.tracing.summary.total ? 'done' : 'pending' }, { id: 'checkpoints', label: 'CHECKPOINTS', path: ctx.checkpoints.path, detail: `${ctx.checkpoints.parsed.length} gates`, status: ctx.readiness.checkpointPending ? 'active' : 'pending' }, { id: 'verify', label: 'VERIFY', path: ctx.verify.path, detail: ctx.calibration.status, status: ctx.verify.raw ? 'done' : 'pending' }],
402
+ };
403
+ if (ctx.azure) {
404
+ ctx.visual.artifactGraph.push(
405
+ {
406
+ id: 'azure-profile',
407
+ label: 'AZURE PROFILE',
408
+ path: ctx.azure.profilePath,
409
+ detail: ctx.azure.profile && (ctx.azure.profile.subscription_name || ctx.azure.profile.subscription_id) || 'sem subscription',
410
+ status: ctx.azure.authStatus && ctx.azure.authStatus.login_active ? 'done' : 'blocked',
411
+ },
412
+ {
413
+ id: 'azure-inventory',
414
+ label: 'AZURE INVENTORY',
415
+ path: ctx.azure.inventoryMarkdownPath,
416
+ detail: ctx.azure.inventorySummary ? `${ctx.azure.inventorySummary.total} recurso(s)` : 'inventário ausente',
417
+ status: ctx.azure.inventorySyncedAt ? (ctx.azure.inventoryStale && ctx.azure.inventoryStale.stale ? 'warning' : 'done') : 'pending',
418
+ }
419
+ );
420
+ }
421
+ ctx.operationalGraph = ctx.activeRun ? operational.buildOperationalGraph(ctx.activeRun) : { nodes: [], edges: [] };
422
+ ctx.dashboard = { readOnly: false };
423
+ return ctx;
424
+ }
425
+
426
+ function dashboardHtml() {
427
+ return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>OXE Dashboard</title><style>
428
+ :root{--bg:#f4efe6;--bg2:#edf4ed;--panel:#fff;--ink:#1d2f3e;--muted:#617787;--line:#dbe3e8;--brand:#173f58;--ok:#1f7a52;--warn:#9d6b12;--bad:#a53e3e;--info:#235f84;--okbg:#e4f3eb;--warnbg:#fbf0da;--badbg:#f8e8e8;--infobg:#e5eff7;--shadow:0 16px 40px rgba(17,32,44,.08);--radius:18px}*{box-sizing:border-box}body{margin:0;background:linear-gradient(180deg,var(--bg),var(--bg2));font-family:"Segoe UI",Arial,sans-serif;color:var(--ink)}.page{max-width:1560px;margin:0 auto;padding:24px}.hero,.stats,.tabs,.grid2,.grid3,.layout{display:grid;gap:16px}.hero{grid-template-columns:1.2fr .8fr}.stats{grid-template-columns:repeat(5,minmax(0,1fr));margin:0 0 16px}.layout{grid-template-columns:1.45fr .95fr}.grid2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid3{grid-template-columns:repeat(3,minmax(0,1fr))}.panel,.metric,.mini,.item,.entry{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}.panel{padding:18px}.metric,.mini,.item,.entry{padding:14px}.stack,.list,.rail{display:grid;gap:14px}.rail{grid-template-columns:repeat(8,minmax(100px,1fr))}.tabs{grid-template-columns:repeat(7,max-content)}.tab{padding:10px 14px;border:1px solid var(--line);border-radius:999px;background:#fff;cursor:pointer;font-weight:700}.tab.active{background:var(--brand);color:#fff;border-color:var(--brand)}.view{display:none}.view.active{display:block}.label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.value{font-size:24px;font-weight:800;margin-top:6px}.muted{color:var(--muted)}.small{font-size:13px}.badge{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:6px 10px;font-size:11px;text-transform:uppercase;font-weight:800}.done{background:var(--okbg);color:var(--ok)}.active{background:var(--infobg);color:var(--info)}.warning{background:var(--warnbg);color:var(--warn)}.blocked{background:var(--badbg);color:var(--bad)}.pending{background:#edf1f4;color:#567181}.head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;margin-bottom:12px}.actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}button{border:0;border-radius:12px;padding:10px 12px;font-weight:700;cursor:pointer}.primary{background:var(--brand);color:#fff}.secondary{background:#edf2f5;color:var(--ink)}.warnbtn{background:#f2e3c5;color:#81540a}.danger{background:#f5dada;color:#8d3030}input,select,textarea{width:100%;border:1px solid var(--line);border-radius:12px;padding:10px;font:inherit}.form{display:grid;gap:10px}.raw{white-space:pre-wrap;background:#f8faf9;border:1px solid var(--line);border-radius:12px;padding:12px;max-height:220px;overflow:auto}.chip{display:inline-flex;border-radius:999px;padding:4px 8px;background:#eef3f6;color:#355267;font-size:12px;margin:4px 6px 0 0}.node{padding:12px;border:1px solid var(--line);border-radius:16px;background:#fff}.go{background:linear-gradient(180deg,#f2fbf5,#ebf7ef)}.nogo{background:linear-gradient(180deg,#fff5f5,#faeded)}@media (max-width:1200px){.hero,.layout{grid-template-columns:1fr}.rail{grid-template-columns:repeat(4,minmax(100px,1fr))}}@media (max-width:860px){.stats,.grid2,.grid3,.tabs,.rail{grid-template-columns:1fr}.page{padding:16px}}</style></head><body><div class="page"><div class="hero"><div class="panel"><h1 style="margin:0;font-size:42px">OXE Decision Control Room</h1><div class="muted" style="margin-top:10px">Control room para decisões reais de Go / No-Go, sem duplicar a fonte de verdade textual do OXE.</div></div><div class="grid2"><div class="mini"><div class="label">Projeto</div><div class="small" id="hero-project">—</div></div><div class="mini"><div class="label">Sessão</div><div class="value" id="hero-session">—</div></div><div class="mini"><div class="label">Próximo passo</div><div class="small" id="hero-next">—</div></div><div class="mini"><div class="label">Confiança</div><div class="value" id="hero-confidence">—</div></div></div></div><div id="app">Carregando…</div></div><script>
429
+ const app=document.getElementById('app');let ctx=null;let tab='decision';let session=null;
430
+ const esc=(s)=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); const cls=(s)=>{const v=String(s||'').toLowerCase();if(/(healthy|approved|done|go|well-calibrated|passed|ready|completed)/.test(v))return'done';if(/(running|in_review|active|mentioned|acceptable|controlled|replaying)/.test(v))return'active';if(/(rejected|needs_revision|blocked|failed|no-go|overconfident|do_not_execute|aborted|waiting_approval)/.test(v))return'blocked';if(/(risk|warning|underconfident|needs_refinement|paused)/.test(v))return'warning';return'pending';}; const badge=(s,l)=>'<span class="badge '+cls(s)+'">'+esc(l||s||'pending')+'</span>'; const list=(items,empty)=>items&&items.length?'<div class="list">'+items.join('')+'</div>':'<div class="entry small">'+esc(empty)+'</div>'; const runtimeDisabled=()=>ctx&&ctx.dashboard&&ctx.dashboard.readOnly;
431
+ async function api(url,opts={}){const res=await fetch(url,{headers:{'Content-Type':'application/json'},...opts});if(!res.ok)throw new Error(await res.text());return res.json();} async function refresh(){ctx=await api('/api/context'+(session?'?session='+encodeURIComponent(session):''));render();}
432
+ function render(){document.getElementById('hero-project').textContent=ctx.projectRoot||'';document.getElementById('hero-session').textContent=ctx.activeSession||'modo legado';document.getElementById('hero-next').textContent=(ctx.nextStep.step||'')+' · '+(ctx.nextStep.reason||'');document.getElementById('hero-confidence').textContent=ctx.readiness.confidence!=null?ctx.readiness.confidence+'%':'—';
433
+ const flow=(ctx.visual.flow.nodes||[]).map((n)=>'<div class="node"><div class="label">Etapa</div><div style="font-weight:800;font-size:18px;margin-top:6px">'+esc(n.label)+'</div><div style="margin-top:10px">'+badge(n.status)+'</div></div>');
434
+ const graph=(ctx.visual.artifactGraph||[]).map((n)=>'<div class="item"><div class="head"><strong>'+esc(n.label)+'</strong>'+badge(n.status)+'</div><div class="small">'+esc(n.detail||'—')+'</div><div class="small muted" style="margin-top:8px">'+esc(n.path||'')+'</div></div>');
435
+ const waves=(ctx.plan.waves||[]).map((w)=>'<div class="panel"><div class="head"><div><strong>Onda '+esc(w.wave)+'</strong><div class="small muted">'+esc(w.taskIds.length)+' tarefas</div></div>'+badge(String(ctx.runtime.parsed.currentWave)===String(w.wave)?'running':'planned')+'</div>'+w.tasks.map((t)=>'<div class="entry"><div style="font-weight:800">'+esc(t.id)+' '+esc(t.title)+'</div><div class="small muted" style="margin-top:6px">Depende de: '+esc(t.dependsOn.length?t.dependsOn.join(', '):'—')+'</div><div class="small muted">Verificação: '+esc(t.verifyCommand||'—')+'</div><div class="small" style="margin-top:6px">'+(t.aceite||[]).map((x)=>'<span class="chip">'+esc(x)+'</span>').join('')+(t.decisions||[]).map((x)=>'<span class="chip">'+esc(x)+'</span>').join('')+'</div></div>').join('')+'</div>');
436
+ const coverage=(ctx.coverage||[]).map((c)=>'<div class="item"><div class="head"><strong>'+esc(c.id)+'</strong>'+badge(c.verifyStatus,c.verifyStatus)+'</div><div class="small">'+esc(c.criterion)+'</div><div class="small muted" style="margin-top:8px">Plano: '+esc(c.tasks.length?c.tasks.join(', '):'sem cobertura')+'</div><div class="small muted">Verify: '+esc(c.verifySummary||c.verifyHow||'sem evidência explícita')+'</div></div>');
437
+ const checkpoints=(ctx.checkpoints.parsed||[]).map((c)=>'<div class="item"><div class="head"><strong>'+esc(c.id)+'</strong>'+badge(c.status)+'</div><div class="small muted">Tipo: '+esc(c.type)+' · Fase: '+esc(c.phase||'—')+' · Escopo: '+esc(c.scope||'')+'</div><div class="small" style="margin-top:8px">'+esc(c.notes||'—')+'</div></div>');
438
+ const agents=((ctx.runtime.parsed.agents&&ctx.runtime.parsed.agents.length)?ctx.runtime.parsed.agents:(ctx.blueprint&&ctx.blueprint.agents)||[]).map((a)=>'<div class="item"><div class="head"><strong>'+esc(a.id||a.name||'agent')+'</strong>'+badge(a.status||a.model_hint||'planned')+'</div><div class="small muted">'+esc(a.role||a.persona||'—')+'</div><div class="small">Tarefas: '+esc((a.tasks||[]).join(', ')||a.scope||'—')+'</div></div>');
439
+ const comments=(ctx.review.comments||[]).map((c)=>'<div class="item"><div class="small muted">'+esc(c.target)+' · '+esc(c.type)+' · '+esc(c.status)+' · '+esc(c.author)+'</div><div style="margin-top:6px">'+esc(c.text)+'</div>'+(c.status!=='resolved'?'<div class="actions"><button class="secondary" onclick="resolveComment(\\''+esc(c.id)+'\\')">Resolver</button></div>':'')+'</div>');
440
+ const sessions=(ctx.sessions.items||[]).map((s)=>'<div class="item"><div class="head"><div><strong>'+esc(s.id)+' · '+esc(s.name)+'</strong><div class="small muted">Criada: '+esc(s.createdAt)+' · Última atividade: '+esc(s.lastActivity)+'</div></div>'+badge(s.status)+'</div><div class="small">'+esc(s.summary||'—')+'</div><div class="small muted" style="margin-top:8px">'+esc(s.path||'—')+'</div><div class="actions"><button class="secondary" onclick="openSession(\\''+esc(s.path)+'\\')">Abrir sessão</button></div></div>');
441
+ const repo=Object.entries(ctx.repositoryContext||{}).map(([k,v])=>'<div class="item"><strong>'+esc(k.toUpperCase())+'</strong><div class="small" style="margin-top:8px">'+esc(v.summary||'Sem resumo disponível.')+'</div><div class="small muted" style="margin-top:8px">'+esc(v.path||'—')+'</div></div>');
442
+ const contextSelected=((ctx.context&&ctx.context.pack&&ctx.context.pack.selected_artifacts)||[]).map((artifact)=>'<div class="item"><div class="head"><strong>'+esc(artifact.alias)+'</strong>'+badge(artifact.exists?(artifact.using_fallback?'fallback':'selected'):'missing',artifact.exists?(artifact.using_fallback?'fallback':'selected'):'missing')+'</div><div class="small">'+esc(artifact.summary||'')+'</div><div class="small muted" style="margin-top:8px">'+esc(artifact.path||'—')+'</div></div>');
443
+ const contextGaps=((ctx.context&&ctx.context.pack&&ctx.context.pack.gaps)||[]).map((gap)=>'<div class="entry small">['+esc(gap.severity)+'] '+esc(gap.alias)+': '+esc(gap.reason)+'</div>');
444
+ const contextConflicts=((ctx.context&&ctx.context.pack&&ctx.context.pack.conflicts)||[]).map((conflict)=>'<div class="entry small">'+esc(conflict.alias)+': '+esc(conflict.reason)+'</div>');
445
+ const semanticsWarnings=((ctx.context&&ctx.context.semantics&&ctx.context.semantics.audit&&ctx.context.semantics.audit.warnings)||[]).map((warning)=>'<div class="entry small">'+esc(warning)+'</div>');
446
+ const blockers=(ctx.readiness.blockers||[]).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const warnings=(ctx.readiness.warnings||[]).slice(0,8).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const evidence=(ctx.runtime.parsed.evidence||[]).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const blockages=(ctx.runtime.parsed.blockages||[]).map((x)=>'<div class="entry small">'+esc(x)+'</div>'); const history=((ctx.sessions.current&&ctx.sessions.current.history)||[]).map((x)=>'<div class="entry small">'+esc(x.date)+' · '+esc(x.event)+'</div>');
447
+ app.innerHTML='<div class="stats"><div class="metric"><div class="label">Saúde lógica</div><div class="value">'+badge(ctx.healthStatus)+'</div></div><div class="metric"><div class="label">Go / No-Go</div><div class="value">'+badge(ctx.readiness.decision,ctx.readiness.decision)+'</div></div><div class="metric"><div class="label">Review</div><div class="value">'+badge(ctx.planReviewStatus)+'</div></div><div class="metric"><div class="label">Checkpoints</div><div class="value">'+badge(ctx.readiness.checkpointPending?'pending_approval':'clear',ctx.readiness.checkpointPending?'pendente':'ok')+'</div></div><div class="metric"><div class="label">Runtime</div><div class="value">'+badge(ctx.runtime.parsed.status||ctx.state.parsed.runtimeStatus||'pending')+'</div></div></div>'
448
+ +'<div class="tabs">'+['decision','plan','execution','evidence','sessions','context','repository'].map((t)=>'<button class="tab'+(tab===t?' active':'')+'" onclick="setTab(\\''+t+'\\')">'+(t==='repository'?'repository context':t)+'</button>').join('')+'</div>'
449
+ +'<div class="view'+(tab==='decision'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Decision Control</h2><div class="muted">A pergunta principal: podemos executar agora?</div></div></div><div class="'+(ctx.readiness.go?'panel go':'panel nogo')+'" style="padding:18px"><div class="label">Decisão operacional</div><div style="font-size:38px;font-weight:900;margin-top:8px">'+(ctx.readiness.go?'GO':'NO-GO')+'</div><div class="small muted" style="margin-top:8px">'+esc(ctx.nextStep.reason||'—')+'</div><div class="actions">'+badge(ctx.planReviewStatus,'review:'+ctx.planReviewStatus)+badge(ctx.readiness.confidenceBand,'confiança:'+ctx.readiness.confidenceBand)+(ctx.readiness.checkpointPending?badge('pending_approval','checkpoint pendente'):'')+'</div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Decision Rail</h2><div class="muted">Spec → Plan → Review → Execute → Verify → Lessons.</div></div></div><div class="rail">'+flow.join('')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Readiness Map</h2><div class="muted">Bloqueios e sinais ativos antes do execute.</div></div></div><div class="grid2"><div><h3>Bloqueios</h3>'+list(blockers,'Sem bloqueios formais para executar.')+'</div><div><h3>Warnings</h3>'+list(warnings,'Sem warnings relevantes.')+'</div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Review e aprovação</h2><div class="muted">Persistida em STATE.md, PLAN-REVIEW.md e comentários.</div></div></div><div class="grid2"><div><div class="actions"><button class="primary" onclick="changeReview(\\'approved\\')">Aprovar</button><button class="warnbtn" onclick="changeReview(\\'needs_revision\\')">Pedir revisão</button><button class="secondary" onclick="changeReview(\\'in_review\\')">Marcar em revisão</button><button class="danger" onclick="changeReview(\\'rejected\\')">Rejeitar</button></div><div class="form"><input id="comment-target" placeholder="target ex.: wave:1, T2, checkpoint:CP-01"/><select id="comment-type"><option value="note">note</option><option value="risk">risk</option><option value="question">question</option><option value="approval">approval</option></select><textarea id="comment-text" rows="6" placeholder="Comentário de revisão"></textarea><button class="primary" onclick="addComment()">Adicionar comentário</button></div></div><div><h3>Comentários</h3>'+list(comments,'Nenhum comentário de revisão.')+'</div></div></div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Mapa de artefatos</h2><div class="muted">Fonte de verdade visual do ciclo atual.</div></div></div><div class="grid3">'+graph.join('')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Calibração do plano</h2><div class="muted">Confiança prevista versus resultado real.</div></div>'+badge(ctx.calibration.status)+'</div><div class="small">'+esc(ctx.calibration.summary)+'</div></div></div></div></div>'
450
+ +'<div class="view'+(tab==='plan'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Trilha do plano</h2><div class="muted">Ondas, dependências e vínculos com aceite e decisões.</div></div>'+badge(ctx.planReviewStatus)+'</div>'+list(waves,'PLAN.md ausente ou sem tarefas estruturadas.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Matriz de cobertura</h2><div class="muted">SPEC → PLAN → VERIFY em um único quadro.</div></div></div><div class="grid2">'+coverage.join('')+'</div></div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Autoavaliação do plano</h2><div class="muted">Melhor plano atual e faixa de confiança.</div></div></div><div class="grid3"><div class="item"><strong>Melhor plano atual</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.plan.selfEvaluation&&ctx.plan.selfEvaluation.bestPlan)||'—')+'</div></div><div class="item"><strong>Confiança</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.readiness.confidence!=null?ctx.readiness.confidence+'%':'—')+'</div></div><div class="item"><strong>Faixa</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.readiness.confidenceBand)+'</div></div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Lacunas da SPEC</h2><div class="muted">Critérios sem cobertura explícita no plano.</div></div></div>'+list((ctx.spec.uncoveredCriteria||[]).map((c)=>'<div class="entry small">'+esc(c.id)+' · '+esc(c.criterion)+'</div>'),'Sem lacunas detectadas.')+'</div></div></div></div>'
451
+ +'<div class="view'+(tab==='execution'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Runtime operacional</h2><div class="muted">Estado tático da execução.</div></div>'+badge(ctx.runtime.parsed.status||'pending')+'</div><div class="grid2"><div class="item"><strong>Onda atual</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.runtime.parsed.currentWave||'—')+'</div></div><div class="item"><strong>Tarefas ativas</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.runtime.parsed.activeTasks||[]).join(', ')||'—')+'</div></div><div class="item"><strong>Próxima ação</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.runtime.parsed.nextAction||'')+'</div></div><div class="item"><strong>Motivo</strong><div class="small muted" style="margin-top:8px">'+esc(ctx.runtime.parsed.nextReason||'—')+'</div></div></div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Checkpoints</h2><div class="muted">Gates humanos formais.</div></div></div>'+list(checkpoints,'CHECKPOINTS.md ausente ou sem gates formais.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Agentes</h2><div class="muted">Blueprint e runtime multiagente.</div></div></div>'+list(agents,'Sem blueprint de agentes ou runtime multiagente ativo.')+'</div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Evidências</h2><div class="muted">Artefatos e sinais da execução atual.</div></div></div>'+list(evidence,'Sem evidência operacional registrada.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Bloqueios</h2><div class="muted">O que impede avanço agora.</div></div></div>'+list(blockages,'Sem bloqueios explícitos.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Runtime bruto</h2><div class="muted">Leitura direta do artefato operacional.</div></div></div><div class="raw">'+esc(ctx.runtime.summary||'EXECUTION-RUNTIME.md ausente.')+'</div></div></div></div></div>'
452
+ +'<div class="view'+(tab==='evidence'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Verify</h2><div class="muted">Evidência final e validação.</div></div>'+badge(ctx.calibration.status)+'</div><div class="raw">'+esc(ctx.verify.summary||'VERIFY.md ausente.')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Critérios no verify</h2><div class="muted">Evidências detectadas por critério A*.</div></div></div><div class="grid2">'+coverage.join('')+'</div></div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Investigações</h2><div class="muted">Base de evidência antes do plano.</div></div></div><div class="raw">'+esc(ctx.support.investigationsSummary||'INVESTIGATIONS.md ausente.')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Capabilities</h2><div class="muted">Recursos nativos do projeto.</div></div></div><div class="raw">'+esc(ctx.support.capabilitiesSummary||'CAPABILITIES.md ausente.')+'</div></div></div></div></div>'
453
+ +'<div class="view'+(tab==='sessions'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Sessão ativa</h2><div class="muted">Contexto atual resolvido pelo STATE global.</div></div></div><div class="grid2"><div class="item"><strong>ID</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.id)||'—')+'</div></div><div class="item"><strong>Nome</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.name)||ctx.activeSession||'')+'</div></div><div class="item"><strong>Status</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.status)||'—')+'</div></div><div class="item"><strong>Última atividade</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.lastActivity)||'—')+'</div></div></div><div class="item" style="margin-top:14px"><strong>Resumo</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.sessions.current&&ctx.sessions.current.summary)||'Sem SESSION.md ativo ou modo legado.')+'</div><div class="small muted" style="margin-top:8px">'+esc(ctx.sessions.currentPath||'—')+'</div></div><div class="item" style="margin-top:14px"><strong>Tags</strong><div class="small" style="margin-top:8px">'+((ctx.sessions.current&&ctx.sessions.current.tags)||[]).map((x)=>'<span class="chip">'+esc(x)+'</span>').join('')+'</div></div><h3>Histórico</h3>'+list(history,'Sem histórico de sessão disponível.')+'</div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Lista de sessões</h2><div class="muted">Índice real lido de .oxe/SESSIONS.md.</div></div></div>'+list(sessions,'SESSIONS.md ausente ou sem sessões registradas.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Índice bruto</h2><div class="muted">Leitura direta do artefato global.</div></div></div><div class="raw">'+esc(ctx.sessions.raw||'SESSIONS.md ausente.')+'</div></div></div></div></div>'
454
+ +'<div class="view'+(tab==='context'?' active':'')+'"><div class="layout"><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Context Pack</h2><div class="muted">Seleção determinística de artefatos para este estado do OXE.</div></div>'+badge((ctx.context&&ctx.context.quality&&ctx.context.quality.primaryStatus)||((ctx.context&&ctx.context.pack&&ctx.context.pack.context_quality&&ctx.context.pack.context_quality.status)||'pending'))+'</div><div class="grid3"><div class="item"><strong>Workflow primário</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.context&&ctx.context.quality&&ctx.context.quality.primaryWorkflow)||'dashboard')+'</div></div><div class="item"><strong>Score</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.context&&ctx.context.quality&&ctx.context.quality.primaryScore)!=null?String(ctx.context.quality.primaryScore):'—')+'</div></div><div class="item"><strong>Freshness</strong><div class="small muted" style="margin-top:8px">'+esc((ctx.context&&ctx.context.pack&&ctx.context.pack.freshness&&ctx.context.pack.freshness.reason)||'—')+'</div></div></div><div class="small muted" style="margin-top:12px">Pack: '+esc((ctx.context&&ctx.context.pack&&ctx.context.pack.path)||'—')+' · semantics hash: '+esc((ctx.context&&ctx.context.pack&&ctx.context.pack.semantics_hash)||'—')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Artefatos selecionados</h2><div class="muted">Contexto efetivamente priorizado pelo pack.</div></div></div><div class="grid2">'+contextSelected.join('')+'</div></div></div><div class="stack"><div class="panel"><div class="head"><div><h2 style="margin:0">Gaps e conflitos</h2><div class="muted">Lacunas e divergências entre raiz e sessão.</div></div></div><h3>Gaps</h3>'+list(contextGaps,'Sem gaps no pack atual.')+'<h3>Conflitos</h3>'+list(contextConflicts,'Sem conflitos detectados.')+'</div><div class="panel"><div class="head"><div><h2 style="margin:0">Summaries ativos</h2><div class="muted">Referências persistidas para compressão de contexto.</div></div></div><div class="grid2">'+kvItem('Project summary',(ctx.context&&ctx.context.summaries&&ctx.context.summaries.project)||'—')+kvItem('Session summary',(ctx.context&&ctx.context.summaries&&ctx.context.summaries.session)||'—')+kvItem('Phase summary',(ctx.context&&ctx.context.summaries&&ctx.context.summaries.phase)||'—')+'</div></div><div class="panel"><div class="head"><div><h2 style="margin:0">Semântica multi-runtime</h2><div class="muted">Drift auditável entre workflow, wrapper e prompt renderizado.</div></div>'+badge(ctx.context&&ctx.context.semantics&&ctx.context.semantics.ok?'healthy':'warning',ctx.context&&ctx.context.semantics&&ctx.context.semantics.ok?'ok':'drift')+'</div><div class="small muted">Manifest: '+esc((ctx.context&&ctx.context.semantics&&ctx.context.semantics.manifestPath)||'—')+' · mismatch count: '+esc((ctx.context&&ctx.context.semantics&&ctx.context.semantics.audit&&ctx.context.semantics.audit.mismatchCount)||0)+'</div>'+list(semanticsWarnings,'Sem warnings semânticos ativos.')+'</div></div></div></div>'
455
+ +'<div class="view'+(tab==='repository'?' active':'')+'"><div class="panel"><div class="head"><div><h2 style="margin:0">Repository Context</h2><div class="muted">Contexto de repositório como suporte de decisão.</div></div></div><div class="grid3">'+repo.join('')+'</div></div></div>'; enhanceExecutionView();}
456
+ function executionViewElement(){return Array.from(app.querySelectorAll('.view')).find((view)=>view.textContent.includes('Runtime operacional'));}
457
+ function panel(title,body){return '<div class="panel"><div class="head"><div><h2 style="margin:0">'+esc(title)+'</h2></div></div>'+body+'</div>';}
458
+ function kvItem(label,value){return '<div class="item"><strong>'+esc(label)+'</strong><div class="small muted" style="margin-top:8px">'+esc(value)+'</div></div>';}
459
+ function actionButton(label,action,klass){return '<button class="'+klass+'" '+(runtimeDisabled()?'disabled':'')+' onclick="runtimeAction(\\''+action+'\\')">'+label+'</button>';}
460
+ function enhanceExecutionView(){
461
+ const view=executionViewElement();
462
+ if(!view||!ctx)return;
463
+ const stacks=view.querySelectorAll('.stack');
464
+ if(stacks.length<2)return;
465
+ const activeRun=ctx.activeRun||{};
466
+ const graph=ctx.operationalGraph||{nodes:[],edges:[]};
467
+ const trace=((ctx.tracing&&ctx.tracing.events)||[]).slice(-8).reverse().map((event)=>'<div class="entry"><div class="head"><strong>'+esc(event.type)+'</strong>'+badge(event.timestamp?event.timestamp.slice(11,19):'—',event.timestamp?event.timestamp.slice(11,19):'')+'</div><div class="small muted">run='+esc(event.run_id||'—')+' · wave='+esc(event.wave_id||'—')+' · task='+esc(event.task_id||'')+'</div><div class="small" style="margin-top:8px">'+esc(JSON.stringify(event.payload||{}))+'</div></div>').join('')||'<div class="entry small">Sem eventos registrados.</div>';
468
+ const nodes=(graph.nodes||[]).map((node)=>'<div class="item"><div class="head"><strong>'+esc(node.label||node.id)+'</strong>'+badge(node.status||'pending')+'</div><div class="small muted">'+esc(node.kind||'node')+'</div><div class="small" style="margin-top:8px">'+esc(node.detail||'—')+'</div></div>').join('')||'<div class="entry small">Sem nós operacionais.</div>';
469
+ const edges=(graph.edges||[]).map((edge)=>'<div class="entry"><div class="small"><strong>'+esc(edge.from)+'</strong> → <strong>'+esc(edge.to)+'</strong></div><div class="small muted" style="margin-top:6px">'+esc(edge.type||'link')+' · '+esc(edge.status||'—')+'</div><div class="small" style="margin-top:6px">'+esc(edge.reason||edge.label||'—')+'</div></div>').join('')||'<div class="entry small">Sem handoffs ou dependências registradas.</div>';
470
+ const memory=Object.entries(ctx.memoryLayers||{}).filter(([key])=>key!=='readOrder').map(([key,val])=>kvItem(key,Array.isArray(val.source)?val.source.join(' | '):val.source||'—')).join('')||'<div class="entry small">Sem contrato de memória disponível.</div>';
471
+ const azure=ctx.azure||null;
472
+ const azureInventory=azure&&azure.inventorySummary?azure.inventorySummary:{total:0,servicebus:0,eventgrid:0,sql:0,other:0};
473
+ const azurePanel=azure?panel('Azure context operacional','<div class="grid3">'+kvItem('Login',azure.authStatus&&azure.authStatus.login_active?'ativo':'ausente')+kvItem('Subscription',azure.profile&&(azure.profile.subscription_name||azure.profile.subscription_id)||'—')+kvItem('Último sync',azure.inventorySyncedAt||'—')+kvItem('Service Bus',String(azureInventory.servicebus||0))+kvItem('Event Grid',String(azureInventory.eventgrid||0))+kvItem('Azure SQL',String(azureInventory.sql||0))+'</div><div class="small muted" style="margin-top:12px">Inventário: total '+esc(azureInventory.total||0)+' · pending operations: '+esc(azure.pendingOperations||0)+' · '+esc(azure.inventoryStale&&azure.inventoryStale.stale?'stale':'fresh')+'</div>'+(azure.lastOperation?'<div class="entry" style="margin-top:12px"><div class="head"><strong>Última operação</strong>'+badge(azure.lastOperation.phase||'pending')+'</div><div class="small">'+esc(azure.lastOperation.summary||azure.lastOperation.operation_id||'—')+'</div><div class="small muted" style="margin-top:8px">'+esc(azure.lastOperation.operation_id||'—')+'</div></div>':'')):'';
474
+ stacks[0].insertAdjacentHTML('afterbegin',panel('Run control','<div class="grid3">'+kvItem('Run ID',activeRun.run_id||'—')+kvItem('Status',activeRun.status||'pending')+kvItem('Cursor',(activeRun.cursor&&((activeRun.cursor.wave!=null?'wave '+activeRun.cursor.wave:'')+(activeRun.cursor.task?' · '+activeRun.cursor.task:'')+(activeRun.cursor.mode?' · '+activeRun.cursor.mode:'')))||'—')+'</div><div class="actions">'+actionButton('Start','start','primary')+actionButton('Pause','pause','warnbtn')+actionButton('Resume','resume','secondary')+actionButton('Replay','replay','danger')+'</div><div class="small muted" style="margin-top:12px">Transitions: '+esc((activeRun.metrics&&activeRun.metrics.transitions)||0)+' · pause_count: '+esc((activeRun.metrics&&activeRun.metrics.pause_count)||0)+' · replay_count: '+esc((activeRun.metrics&&activeRun.metrics.replay_count)||0)+'</div>'));
475
+ stacks[0].insertAdjacentHTML('beforeend',panel('Mapa operacional','<div class="grid3">'+nodes+'</div><h3>Handoffs e dependências</h3>'+edges)+(azurePanel||''));
476
+ stacks[1].insertAdjacentHTML('afterbegin',panel('Trace operacional','<div class="small muted" style="margin-bottom:12px">Arquivo: '+esc((ctx.tracing&&ctx.tracing.path)||'—')+' · total: '+esc((ctx.tracing&&ctx.tracing.summary&&ctx.tracing.summary.total)||0)+' evento(s)</div>'+trace));
477
+ stacks[1].insertAdjacentHTML('beforeend',panel('Camadas de memória','<div class="small muted" style="margin-bottom:12px">'+esc(((ctx.memoryLayers&&ctx.memoryLayers.readOrder)||[]).join(' ')||'—')+'</div><div class="grid2">'+memory+'</div>'));
478
+ }
479
+ function setTab(next){tab=next;render();}
480
+ async function openSession(path){session=path||null;await refresh();tab='sessions';render();}
481
+ async function changeReview(status){const note=prompt('Justificativa curta para o novo estado do plano:',status==='approved'?'Plano aprovado para execução.':'');if(note==null)return;await api('/api/review/status',{method:'POST',body:JSON.stringify({status,note,author:'user',activeSession:session})});await refresh();}
482
+ async function addComment(){const target=document.getElementById('comment-target').value||'plan';const type=document.getElementById('comment-type').value||'note';const text=document.getElementById('comment-text').value.trim();if(!text)return alert('Escreva um comentário.');await api('/api/review/comment',{method:'POST',body:JSON.stringify({target,type,text,author:'user',activeSession:session})});document.getElementById('comment-text').value='';await refresh();}
483
+ async function resolveComment(id){await api('/api/review/comment-status',{method:'POST',body:JSON.stringify({commentId:id,status:'resolved',activeSession:session})});await refresh();}
484
+ async function runtimeAction(action){if(runtimeDisabled())return;const reason=prompt('Motivo da transição de runtime:',action==='start'?'iniciar execução':'');if(reason==null)return;const wave=prompt('Onda atual (opcional):',ctx.activeRun&&ctx.activeRun.current_wave!=null?String(ctx.activeRun.current_wave):'');if(wave===null)return;const task=prompt('Tarefa atual (opcional):',ctx.activeRun&&ctx.activeRun.cursor&&ctx.activeRun.cursor.task?String(ctx.activeRun.cursor.task):'');if(task===null)return;await api('/api/runtime/action',{method:'POST',body:JSON.stringify({action,reason,activeSession:session,wave:wave?Number(wave):null,task:task||null})});await refresh();}
485
+ window.setTab=setTab;window.openSession=openSession;window.changeReview=changeReview;window.addComment=addComment;window.resolveComment=resolveComment;window.runtimeAction=runtimeAction;refresh().catch((err)=>{app.innerHTML='<div class="panel"><h2>Erro</h2><div class="muted">'+esc(err.message)+'</div></div>';});
486
+ </script></body></html>`;
487
+ }
488
+
489
+ function readJsonBody(req) {
490
+ return new Promise((resolve, reject) => {
491
+ let raw = '';
492
+ req.on('data', (chunk) => {
493
+ raw += chunk;
494
+ if (raw.length > 1024 * 1024) {
495
+ reject(new Error('Payload demasiado grande.'));
496
+ req.destroy();
497
+ }
498
+ });
499
+ req.on('end', () => {
500
+ if (!raw.trim()) return resolve({});
501
+ try {
502
+ resolve(JSON.parse(raw));
503
+ } catch (err) {
504
+ reject(new Error('JSON inválido.'));
505
+ }
506
+ });
507
+ req.on('error', reject);
508
+ });
509
+ }
510
+
511
+ function sendJson(res, status, body) {
512
+ const payload = JSON.stringify(body, null, 2);
513
+ res.writeHead(status, {
514
+ 'Content-Type': 'application/json; charset=utf-8',
515
+ 'Content-Length': Buffer.byteLength(payload),
516
+ 'Cache-Control': 'no-store',
517
+ });
518
+ res.end(payload);
519
+ }
520
+
521
+ function createDashboardServer(projectRoot, opts = {}) {
522
+ const root = path.resolve(projectRoot);
523
+ const server = http.createServer(async (req, res) => {
524
+ try {
525
+ const reqUrl = new URL(req.url || '/', 'http://127.0.0.1');
526
+ if (req.method === 'GET' && reqUrl.pathname === '/') {
527
+ const html = dashboardHtml();
528
+ res.writeHead(200, {
529
+ 'Content-Type': 'text/html; charset=utf-8',
530
+ 'Content-Length': Buffer.byteLength(html),
531
+ 'Cache-Control': 'no-store',
532
+ });
533
+ res.end(html);
534
+ return;
535
+ }
536
+
537
+ if (req.method === 'GET' && reqUrl.pathname === '/api/context') {
538
+ const activeSession = reqUrl.searchParams.get('session') || opts.activeSession || null;
539
+ sendJson(res, 200, { ...loadDashboardContext(root, { activeSession }), dashboard: { readOnly: Boolean(opts.readOnly) } });
540
+ return;
541
+ }
542
+
543
+ if (opts.readOnly && req.method === 'POST' && (reqUrl.pathname.startsWith('/api/review/') || reqUrl.pathname.startsWith('/api/runtime/'))) {
544
+ sendJson(res, 403, { error: 'Dashboard em modo read-only.' });
545
+ return;
546
+ }
547
+
548
+ if (req.method === 'POST' && reqUrl.pathname === '/api/review/status') {
549
+ const body = await readJsonBody(req);
550
+ const result = savePlanReviewStatus(root, body || {});
551
+ sendJson(res, 200, result);
552
+ return;
553
+ }
554
+
555
+ if (req.method === 'POST' && reqUrl.pathname === '/api/review/comment') {
556
+ const body = await readJsonBody(req);
557
+ const result = addPlanReviewComment(root, body || {});
558
+ sendJson(res, 200, result);
559
+ return;
560
+ }
561
+
562
+ if (req.method === 'POST' && reqUrl.pathname === '/api/review/comment-status') {
563
+ const body = await readJsonBody(req);
564
+ const result = updatePlanReviewCommentStatus(root, body || {});
565
+ if (!result) {
566
+ sendJson(res, 404, { error: 'Comentário não encontrado.' });
567
+ return;
568
+ }
569
+ const stateText = readTextIfExists(health.oxePaths(root).state) || '';
570
+ savePlanReviewStatus(root, {
571
+ activeSession: body.activeSession,
572
+ status: health.parsePlanReviewStatus(stateText) || 'in_review',
573
+ note: 'Estado de comentários de revisão atualizado',
574
+ author: body.author || 'dashboard',
575
+ });
576
+ sendJson(res, 200, result);
577
+ return;
578
+ }
579
+
580
+ if (req.method === 'POST' && reqUrl.pathname === '/api/runtime/action') {
581
+ const body = await readJsonBody(req);
582
+ const result = operational.applyRuntimeAction(root, body.activeSession || opts.activeSession || null, body || {});
583
+ sendJson(res, 200, result);
584
+ return;
585
+ }
586
+
587
+ sendJson(res, 404, { error: 'Rota não encontrada.' });
588
+ } catch (err) {
589
+ sendJson(res, 500, { error: err && err.message ? err.message : 'Erro interno.' });
590
+ }
591
+ });
592
+ return server;
593
+ }
594
+
595
+ module.exports = {
596
+ loadDashboardContext,
597
+ savePlanReviewStatus,
598
+ addPlanReviewComment,
599
+ updatePlanReviewCommentStatus,
600
+ createDashboardServer,
601
+ parsePlan,
602
+ parseSpec,
603
+ parseVerify,
604
+ buildCoverageMatrix,
605
+ };