oxe-cc 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.cursor/commands/oxe-ask.md +11 -0
  2. package/.cursor/commands/oxe-capabilities.md +11 -0
  3. package/.cursor/commands/oxe-dashboard.md +11 -0
  4. package/.github/prompts/oxe-ask.prompt.md +12 -0
  5. package/.github/prompts/oxe-capabilities.prompt.md +12 -0
  6. package/.github/prompts/oxe-dashboard.prompt.md +12 -0
  7. package/CHANGELOG.md +33 -0
  8. package/README.md +189 -34
  9. package/assets/oxe-framework-artifacts-paper.png +0 -0
  10. package/bin/banner.txt +1 -1
  11. package/bin/lib/oxe-azure.cjs +1445 -0
  12. package/bin/lib/oxe-dashboard.cjs +588 -0
  13. package/bin/lib/oxe-install-resolve.cjs +4 -1
  14. package/bin/lib/oxe-operational.cjs +670 -0
  15. package/bin/lib/oxe-project-health.cjs +655 -118
  16. package/bin/oxe-cc.js +1404 -17
  17. package/commands/oxe/ask.md +14 -0
  18. package/commands/oxe/capabilities.md +13 -0
  19. package/commands/oxe/dashboard.md +14 -0
  20. package/lib/sdk/README.md +9 -7
  21. package/lib/sdk/index.cjs +56 -0
  22. package/lib/sdk/index.d.ts +73 -0
  23. package/oxe/templates/ACTIVE-RUN.template.json +32 -0
  24. package/oxe/templates/CAPABILITIES.template.md +7 -0
  25. package/oxe/templates/CAPABILITY.template.md +45 -0
  26. package/oxe/templates/CHECKPOINTS.template.md +7 -0
  27. package/oxe/templates/CONFIG.md +3 -2
  28. package/oxe/templates/EXECUTION-RUNTIME.template.md +68 -0
  29. package/oxe/templates/INVESTIGATION.template.md +38 -0
  30. package/oxe/templates/NOTES.template.md +16 -0
  31. package/oxe/templates/PLAN-REVIEW.template.md +31 -0
  32. package/oxe/templates/PLAN.template.md +22 -7
  33. package/oxe/templates/RESEARCH.template.md +11 -4
  34. package/oxe/templates/SPEC.template.md +6 -4
  35. package/oxe/templates/STATE.md +45 -7
  36. package/oxe/templates/config.template.json +14 -5
  37. package/oxe/workflows/ask.md +71 -0
  38. package/oxe/workflows/capabilities.md +23 -0
  39. package/oxe/workflows/dashboard.md +23 -0
  40. package/oxe/workflows/discuss.md +11 -9
  41. package/oxe/workflows/execute.md +46 -17
  42. package/oxe/workflows/help.md +273 -239
  43. package/oxe/workflows/next.md +10 -8
  44. package/oxe/workflows/obs.md +70 -20
  45. package/oxe/workflows/plan-agent.md +2 -1
  46. package/oxe/workflows/plan.md +70 -21
  47. package/oxe/workflows/quick.md +14 -6
  48. package/oxe/workflows/references/adaptive-discovery.md +27 -0
  49. package/oxe/workflows/references/flow-robustness-contract.md +80 -0
  50. package/oxe/workflows/research.md +12 -8
  51. package/oxe/workflows/retro.md +30 -5
  52. package/oxe/workflows/scan.md +1 -0
  53. package/oxe/workflows/spec.md +58 -33
  54. package/oxe/workflows/verify.md +40 -10
  55. package/package.json +2 -2
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const operational = require('./oxe-operational.cjs');
6
+ const azure = require('./oxe-azure.cjs');
5
7
 
6
8
  /** @type {string[]} */
7
9
  const ALLOWED_CONFIG_KEYS = [
@@ -17,15 +19,17 @@ const ALLOWED_CONFIG_KEYS = [
17
19
  'scan_ignore_globs',
18
20
  'spec_required_sections',
19
21
  'plan_max_tasks_per_wave',
20
- 'profile',
21
- 'verification_depth',
22
- 'security_in_verify',
22
+ 'profile',
23
+ 'verification_depth',
24
+ 'plan_confidence_threshold',
25
+ 'security_in_verify',
23
26
  'install',
24
27
  'plugins',
25
- 'workstreams',
26
- 'milestones',
27
- 'scale_adaptive',
28
- ];
28
+ 'workstreams',
29
+ 'milestones',
30
+ 'scale_adaptive',
31
+ 'azure',
32
+ ];
29
33
 
30
34
  /**
31
35
  * Profiles de execução OXE que controlam rigor do workflow.
@@ -116,16 +120,25 @@ function loadOxeConfigMerged(targetProject) {
116
120
  discuss_before_plan: false,
117
121
  after_verify_suggest_pr: true,
118
122
  after_verify_draft_commit: true,
119
- after_verify_suggest_uat: false,
120
- verification_depth: 'standard',
121
- default_verify_command: '',
123
+ after_verify_suggest_uat: false,
124
+ verification_depth: 'standard',
125
+ plan_confidence_threshold: 70,
126
+ default_verify_command: '',
122
127
  scan_max_age_days: 0,
123
128
  compact_max_age_days: 0,
124
129
  scan_focus_globs: [],
125
130
  scan_ignore_globs: [],
126
- spec_required_sections: [],
127
- plan_max_tasks_per_wave: 0,
128
- };
131
+ spec_required_sections: [],
132
+ plan_max_tasks_per_wave: 0,
133
+ azure: {
134
+ enabled: false,
135
+ default_resource_group: '',
136
+ preferred_locations: [],
137
+ inventory_max_age_hours: 24,
138
+ resource_graph_auto_install: true,
139
+ vpn_required: false,
140
+ },
141
+ };
129
142
  const p = path.join(targetProject, '.oxe', 'config.json');
130
143
  if (!fs.existsSync(p)) return { config: defaults, path: null, parseError: null };
131
144
  try {
@@ -211,21 +224,62 @@ function validateConfigShape(cfg) {
211
224
  typeErrors.push(`profile deve ser um de: ${EXECUTION_PROFILES.join(', ')}`);
212
225
  }
213
226
  }
214
- if (cfg.verification_depth != null) {
215
- if (typeof cfg.verification_depth !== 'string') {
216
- typeErrors.push('verification_depth deve ser string');
217
- } else if (!VERIFICATION_DEPTHS.includes(cfg.verification_depth)) {
218
- typeErrors.push(`verification_depth deve ser um de: ${VERIFICATION_DEPTHS.join(', ')}`);
219
- }
220
- }
227
+ if (cfg.verification_depth != null) {
228
+ if (typeof cfg.verification_depth !== 'string') {
229
+ typeErrors.push('verification_depth deve ser string');
230
+ } else if (!VERIFICATION_DEPTHS.includes(cfg.verification_depth)) {
231
+ typeErrors.push(`verification_depth deve ser um de: ${VERIFICATION_DEPTHS.join(', ')}`);
232
+ }
233
+ }
234
+ if (cfg.plan_confidence_threshold != null && typeof cfg.plan_confidence_threshold !== 'number') {
235
+ typeErrors.push('plan_confidence_threshold deve ser número (percentual de 0 a 100)');
236
+ }
221
237
  if (cfg.after_verify_suggest_uat != null && typeof cfg.after_verify_suggest_uat !== 'boolean') {
222
238
  typeErrors.push('after_verify_suggest_uat deve ser boolean');
223
239
  }
224
- if (cfg.scale_adaptive != null && typeof cfg.scale_adaptive !== 'boolean') {
225
- typeErrors.push('scale_adaptive deve ser boolean');
226
- }
227
- return { unknownKeys, typeErrors };
228
- }
240
+ if (cfg.scale_adaptive != null && typeof cfg.scale_adaptive !== 'boolean') {
241
+ typeErrors.push('scale_adaptive deve ser boolean');
242
+ }
243
+ if (cfg.azure != null) {
244
+ if (typeof cfg.azure !== 'object' || Array.isArray(cfg.azure)) {
245
+ typeErrors.push('azure deve ser um objeto');
246
+ } else {
247
+ const azureCfg = /** @type {Record<string, unknown>} */ (cfg.azure);
248
+ const allowedAzureKeys = [
249
+ 'enabled',
250
+ 'default_resource_group',
251
+ 'preferred_locations',
252
+ 'inventory_max_age_hours',
253
+ 'resource_graph_auto_install',
254
+ 'vpn_required',
255
+ ];
256
+ for (const key of Object.keys(azureCfg)) {
257
+ if (!allowedAzureKeys.includes(key)) {
258
+ typeErrors.push(`azure: chave desconhecida "${key}"`);
259
+ }
260
+ }
261
+ if (azureCfg.enabled != null && typeof azureCfg.enabled !== 'boolean') {
262
+ typeErrors.push('azure.enabled deve ser boolean');
263
+ }
264
+ if (azureCfg.default_resource_group != null && typeof azureCfg.default_resource_group !== 'string') {
265
+ typeErrors.push('azure.default_resource_group deve ser string');
266
+ }
267
+ if (azureCfg.preferred_locations != null && !Array.isArray(azureCfg.preferred_locations)) {
268
+ typeErrors.push('azure.preferred_locations deve ser array de strings');
269
+ }
270
+ if (azureCfg.inventory_max_age_hours != null && typeof azureCfg.inventory_max_age_hours !== 'number') {
271
+ typeErrors.push('azure.inventory_max_age_hours deve ser número');
272
+ }
273
+ if (azureCfg.resource_graph_auto_install != null && typeof azureCfg.resource_graph_auto_install !== 'boolean') {
274
+ typeErrors.push('azure.resource_graph_auto_install deve ser boolean');
275
+ }
276
+ if (azureCfg.vpn_required != null && typeof azureCfg.vpn_required !== 'boolean') {
277
+ typeErrors.push('azure.vpn_required deve ser boolean');
278
+ }
279
+ }
280
+ }
281
+ return { unknownKeys, typeErrors };
282
+ }
229
283
 
230
284
  /**
231
285
  * @param {string} stateText
@@ -286,12 +340,38 @@ function parseLastCompactDate(stateText) {
286
340
  * @param {string} stateText
287
341
  * @returns {Date | null}
288
342
  */
289
- function parseLastRetroDate(stateText) {
290
- const m = stateText.match(/\blast_retro\s*:\s*(\d{4}-\d{2}-\d{2})/i);
291
- if (!m) return null;
292
- const iso = Date.parse(m[1]);
293
- return Number.isNaN(iso) ? null : new Date(iso);
294
- }
343
+ function parseLastRetroDate(stateText) {
344
+ const m = stateText.match(/\blast_retro\s*:\s*(\d{4}-\d{2}-\d{2})/i);
345
+ if (!m) return null;
346
+ const iso = Date.parse(m[1]);
347
+ return Number.isNaN(iso) ? null : new Date(iso);
348
+ }
349
+
350
+ /**
351
+ * @param {string} stateText
352
+ * @returns {string | null}
353
+ */
354
+ function parseActiveSession(stateText) {
355
+ if (!stateText) return null;
356
+ const m = stateText.match(/\*\*active_session:\*\*\s*`?([^\n`]+?)`?\s*(?:\n|$)/i);
357
+ if (!m) return null;
358
+ const raw = m[1].trim();
359
+ if (!raw || raw === '—' || /^none$/i.test(raw)) return null;
360
+ return raw.replace(/\\/g, '/');
361
+ }
362
+
363
+ /**
364
+ * @param {string} stateText
365
+ * @returns {string | null}
366
+ */
367
+ function parsePlanReviewStatus(stateText) {
368
+ if (!stateText) return null;
369
+ const m = stateText.match(/\*\*plan_review_status:\*\*\s*`?([^\n`]+?)`?\s*(?:\n|$)/i);
370
+ if (!m) return null;
371
+ const raw = m[1].trim();
372
+ if (!raw || raw === '—' || /^none$/i.test(raw)) return null;
373
+ return raw;
374
+ }
295
375
 
296
376
  /**
297
377
  * @param {Date | null} scanDate
@@ -316,13 +396,31 @@ function isStaleLessons(retroDate, maxAgeDays) {
316
396
  /**
317
397
  * @param {string} target
318
398
  */
319
- function oxePaths(target) {
320
- const oxe = path.join(target, '.oxe');
321
- return {
322
- oxe,
323
- state: path.join(oxe, 'STATE.md'),
324
- spec: path.join(oxe, 'SPEC.md'),
325
- plan: path.join(oxe, 'PLAN.md'),
399
+ function oxePaths(target) {
400
+ const oxe = path.join(target, '.oxe');
401
+ return {
402
+ oxe,
403
+ state: path.join(oxe, 'STATE.md'),
404
+ runtime: path.join(oxe, 'EXECUTION-RUNTIME.md'),
405
+ checkpoints: path.join(oxe, 'CHECKPOINTS.md'),
406
+ capabilitiesIndex: path.join(oxe, 'CAPABILITIES.md'),
407
+ capabilitiesDir: path.join(oxe, 'capabilities'),
408
+ investigationsIndex: path.join(oxe, 'INVESTIGATIONS.md'),
409
+ investigationsDir: path.join(oxe, 'investigations'),
410
+ dashboardDir: path.join(oxe, 'dashboard'),
411
+ sessionsIndex: path.join(oxe, 'SESSIONS.md'),
412
+ globalDir: path.join(oxe, 'global'),
413
+ globalLessons: path.join(oxe, 'global', 'LESSONS.md'),
414
+ globalMilestones: path.join(oxe, 'global', 'MILESTONES.md'),
415
+ globalMilestonesDir: path.join(oxe, 'global', 'milestones'),
416
+ sessionsDir: path.join(oxe, 'sessions'),
417
+ planReview: path.join(oxe, 'PLAN-REVIEW.md'),
418
+ planReviewComments: path.join(oxe, 'plan-review-comments.json'),
419
+ activeRun: path.join(oxe, 'ACTIVE-RUN.json'),
420
+ runsDir: path.join(oxe, 'runs'),
421
+ events: path.join(oxe, 'OXE-EVENTS.ndjson'),
422
+ spec: path.join(oxe, 'SPEC.md'),
423
+ plan: path.join(oxe, 'PLAN.md'),
326
424
  quick: path.join(oxe, 'QUICK.md'),
327
425
  verify: path.join(oxe, 'VERIFY.md'),
328
426
  discuss: path.join(oxe, 'DISCUSS.md'),
@@ -330,8 +428,41 @@ function oxePaths(target) {
330
428
  codebase: path.join(oxe, 'codebase'),
331
429
  lessons: path.join(oxe, 'LESSONS.md'),
332
430
  planAgents: path.join(oxe, 'plan-agents.json'),
333
- };
334
- }
431
+ };
432
+ }
433
+
434
+ /**
435
+ * @param {string} target
436
+ * @param {string | null} activeSession
437
+ */
438
+ function scopedOxePaths(target, activeSession) {
439
+ const base = oxePaths(target);
440
+ if (!activeSession) return { ...base, activeSession: null, scopedRoot: base.oxe };
441
+ const sessionRoot = path.join(base.oxe, ...activeSession.split('/'));
442
+ return {
443
+ ...base,
444
+ activeSession,
445
+ scopedRoot: sessionRoot,
446
+ sessionRoot,
447
+ sessionManifest: path.join(sessionRoot, 'SESSION.md'),
448
+ planReview: path.join(sessionRoot, 'plan', 'PLAN-REVIEW.md'),
449
+ planReviewComments: path.join(sessionRoot, 'plan', 'plan-review-comments.json'),
450
+ runtime: path.join(sessionRoot, 'execution', 'EXECUTION-RUNTIME.md'),
451
+ checkpoints: path.join(sessionRoot, 'execution', 'CHECKPOINTS.md'),
452
+ activeRun: path.join(sessionRoot, 'execution', 'ACTIVE-RUN.json'),
453
+ runsDir: path.join(sessionRoot, 'execution', 'runs'),
454
+ events: path.join(sessionRoot, 'execution', 'OXE-EVENTS.ndjson'),
455
+ investigationsIndex: path.join(sessionRoot, 'research', 'INVESTIGATIONS.md'),
456
+ investigationsDir: path.join(sessionRoot, 'research', 'investigations'),
457
+ spec: path.join(sessionRoot, 'spec', 'SPEC.md'),
458
+ discuss: path.join(sessionRoot, 'spec', 'DISCUSS.md'),
459
+ plan: path.join(sessionRoot, 'plan', 'PLAN.md'),
460
+ quick: path.join(sessionRoot, 'plan', 'QUICK.md'),
461
+ verify: path.join(sessionRoot, 'verification', 'VERIFY.md'),
462
+ summary: path.join(sessionRoot, 'verification', 'SUMMARY.md'),
463
+ executionState: path.join(sessionRoot, 'execution', 'STATE.md'),
464
+ };
465
+ }
335
466
 
336
467
  /**
337
468
  * Valida o arquivo plan-agents.json (se existir) e retorna avisos.
@@ -459,7 +590,7 @@ function planTaskAceiteWarnings(planPath) {
459
590
  return out;
460
591
  }
461
592
 
462
- function planWaveWarningsFixed(planPath, maxPerWave) {
593
+ function planWaveWarningsFixed(planPath, maxPerWave) {
463
594
  if (!maxPerWave || maxPerWave <= 0 || !fs.existsSync(planPath)) return [];
464
595
  const raw = fs.readFileSync(planPath, 'utf8');
465
596
  const lines = raw.split('\n');
@@ -480,21 +611,241 @@ function planWaveWarningsFixed(planPath, maxPerWave) {
480
611
  w.push(`PLAN.md: onda ${wN} tem ${count} tarefas (máximo configurado: ${maxPerWave} — considere dividir ondas)`);
481
612
  }
482
613
  }
483
- return w;
484
- }
614
+ return w;
615
+ }
616
+
617
+ /**
618
+ * @param {string} planPath
619
+ * @returns {{
620
+ * hasSection: boolean,
621
+ * bestPlan: string | null,
622
+ * confidence: number | null,
623
+ * warnings: string[],
624
+ * }}
625
+ */
626
+ function parsePlanSelfEvaluation(planPath) {
627
+ const empty = { hasSection: false, bestPlan: null, confidence: null, warnings: [] };
628
+ if (!fs.existsSync(planPath)) return empty;
629
+ const raw = fs.readFileSync(planPath, 'utf8');
630
+ const m = raw.match(/##\s*Autoavaliação do Plano\s*([\s\S]*?)(?=\n## |\n#[^\#]|$)/i);
631
+ if (!m) {
632
+ return {
633
+ ...empty,
634
+ warnings: ['PLAN.md sem a seção obrigatória "## Autoavaliação do Plano"'],
635
+ };
636
+ }
637
+ const body = m[1];
638
+ const best = body.match(/\*\*Melhor plano atual:\*\*\s*(sim|não|nao)/i);
639
+ const confidence = body.match(/\*\*Confiança:\*\*\s*(\d{1,3})\s*%/i);
640
+ /** @type {string[]} */
641
+ const warnings = [];
642
+ const rubricLabels = [
643
+ 'Completude dos requisitos',
644
+ 'Dependências conhecidas',
645
+ 'Risco técnico',
646
+ 'Impacto no código existente',
647
+ 'Clareza da validação / testes',
648
+ 'Lacunas externas / decisões pendentes',
649
+ ];
650
+ if (!best) warnings.push('PLAN.md: autoavaliação sem "Melhor plano atual: sim|não"');
651
+ if (!confidence) warnings.push('PLAN.md: autoavaliação sem "Confiança: NN%"');
652
+ if (!/\*\*Principais incertezas:\*\*/i.test(body)) warnings.push('PLAN.md: autoavaliação sem "Principais incertezas"');
653
+ if (!/\*\*Alternativas descartadas:\*\*/i.test(body)) warnings.push('PLAN.md: autoavaliação sem "Alternativas descartadas"');
654
+ if (!/\*\*Condição para replanejar:\*\*/i.test(body)) warnings.push('PLAN.md: autoavaliação sem "Condição para replanejar"');
655
+ for (const label of rubricLabels) {
656
+ if (!body.includes(label)) warnings.push(`PLAN.md: rubrica sem "${label}"`);
657
+ }
658
+ const parsedConfidence = confidence ? Number(confidence[1]) : null;
659
+ if (parsedConfidence != null && (parsedConfidence < 0 || parsedConfidence > 100)) {
660
+ warnings.push('PLAN.md: confiança fora do intervalo 0–100%');
661
+ }
662
+ return {
663
+ hasSection: true,
664
+ bestPlan: best ? best[1].toLowerCase().replace('nao', 'não') : null,
665
+ confidence: parsedConfidence,
666
+ warnings,
667
+ };
668
+ }
669
+
670
+ /**
671
+ * @param {string} planPath
672
+ * @param {number} threshold
673
+ * @returns {string[]}
674
+ */
675
+ function planSelfEvaluationWarnings(planPath, threshold) {
676
+ const info = parsePlanSelfEvaluation(planPath);
677
+ const warns = [...info.warnings];
678
+ if (!fs.existsSync(planPath)) return warns;
679
+ if (info.bestPlan === 'não') warns.push('PLAN.md: autoavaliação declara que este não é o melhor plano atual');
680
+ if (info.confidence != null && info.confidence < threshold) {
681
+ warns.push(`PLAN.md: confiança ${info.confidence}% abaixo do limiar executável (${threshold}%)`);
682
+ }
683
+ return warns;
684
+ }
685
+
686
+ /**
687
+ * @param {string} target
688
+ * @param {string | null} activeSession
689
+ * @returns {string[]}
690
+ */
691
+ function sessionWarnings(target, activeSession) {
692
+ if (!activeSession) return [];
693
+ const base = oxePaths(target);
694
+ const scoped = scopedOxePaths(target, activeSession);
695
+ /** @type {string[]} */
696
+ const warns = [];
697
+ if (!/^sessions\/s\d{3}-/.test(activeSession)) {
698
+ warns.push(`active_session "${activeSession}" não segue o formato sessions/sNNN-slug`);
699
+ }
700
+ if (!fs.existsSync(scoped.sessionRoot)) {
701
+ warns.push(`active_session aponta para ${activeSession}, mas a pasta da sessão não existe em .oxe/`);
702
+ return warns;
703
+ }
704
+ if (!fs.existsSync(scoped.sessionManifest)) warns.push(`Sessão ativa ${activeSession} sem SESSION.md`);
705
+ if (!fs.existsSync(base.sessionsIndex)) warns.push('Sessão ativa definida, mas .oxe/SESSIONS.md não existe');
706
+ return warns;
707
+ }
708
+
709
+ /**
710
+ * @param {string} target
711
+ * @returns {string[]}
712
+ */
713
+ function installationCompletenessWarnings(target) {
714
+ const p = oxePaths(target);
715
+ /** @type {string[]} */
716
+ const warns = [];
717
+ if (!fs.existsSync(p.oxe)) return warns;
718
+ if (!fs.existsSync(p.globalDir)) warns.push('.oxe/global/ ausente');
719
+ if (!fs.existsSync(p.globalLessons)) warns.push('.oxe/global/LESSONS.md ausente');
720
+ if (!fs.existsSync(p.globalMilestones)) warns.push('.oxe/global/MILESTONES.md ausente');
721
+ if (!fs.existsSync(p.globalMilestonesDir)) warns.push('.oxe/global/milestones/ ausente');
722
+ if (!fs.existsSync(p.sessionsDir)) warns.push('.oxe/sessions/ ausente');
723
+ if (!fs.existsSync(p.capabilitiesDir)) warns.push('.oxe/capabilities/ ausente');
724
+ if (!fs.existsSync(p.capabilitiesIndex)) warns.push('.oxe/CAPABILITIES.md ausente');
725
+ if (!fs.existsSync(p.investigationsDir)) warns.push('.oxe/investigations/ ausente');
726
+ if (!fs.existsSync(p.investigationsIndex)) warns.push('.oxe/INVESTIGATIONS.md ausente');
727
+ if (!fs.existsSync(p.runtime)) warns.push('.oxe/EXECUTION-RUNTIME.md ausente');
728
+ if (!fs.existsSync(p.checkpoints)) warns.push('.oxe/CHECKPOINTS.md ausente');
729
+ if (!fs.existsSync(p.activeRun)) warns.push('.oxe/ACTIVE-RUN.json ausente');
730
+ if (!fs.existsSync(p.runsDir)) warns.push('.oxe/runs/ ausente');
731
+ if (!fs.existsSync(p.events)) warns.push('.oxe/OXE-EVENTS.ndjson ausente');
732
+ if (azure.isAzureContextEnabled(target)) {
733
+ const azurePaths = azure.azurePaths(target);
734
+ if (!fs.existsSync(azurePaths.root)) warns.push('.oxe/cloud/azure/ ausente');
735
+ if (!fs.existsSync(azurePaths.operationsDir)) warns.push('.oxe/cloud/azure/operations/ ausente');
736
+ if (!fs.existsSync(azurePaths.profile)) warns.push('.oxe/cloud/azure/profile.json ausente');
737
+ if (!fs.existsSync(azurePaths.authStatus)) warns.push('.oxe/cloud/azure/auth-status.json ausente');
738
+ }
739
+ return warns;
740
+ }
741
+
742
+ /**
743
+ * @param {string} stateText
744
+ * @param {ReturnType<typeof scopedOxePaths>} p
745
+ * @returns {string[]}
746
+ */
747
+ function runtimeWarnings(stateText, p) {
748
+ /** @type {string[]} */
749
+ const warns = [];
750
+ const checkpointPending = /\*\*checkpoint_status:\*\*\s*`?pending_approval`?/i.test(stateText);
751
+ const runtimeBlocked = /\*\*runtime_status:\*\*\s*`?(blocked|waiting_approval|failed)`?/i.test(stateText);
752
+ if (checkpointPending && !fs.existsSync(p.checkpoints)) {
753
+ warns.push('STATE.md indica checkpoint pendente, mas o índice de checkpoints não existe');
754
+ }
755
+ if (runtimeBlocked && !fs.existsSync(p.runtime)) {
756
+ warns.push('STATE.md indica runtime bloqueado, mas EXECUTION-RUNTIME.md não existe');
757
+ }
758
+ if (fs.existsSync(p.runtime)) {
759
+ const raw = fs.readFileSync(p.runtime, 'utf8');
760
+ if (!/##\s*Checkpoints/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem seção "Checkpoints"');
761
+ if (!/##\s*Agentes ativos/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem seção "Agentes ativos"');
762
+ if (!/Run ID/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem referência explícita de Run ID');
763
+ if (!/Tracing operacional/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem seção "Tracing operacional"');
764
+ }
765
+ if (!fs.existsSync(p.activeRun)) {
766
+ warns.push('ACTIVE-RUN.json não existe para o escopo atual');
767
+ }
768
+ if (!fs.existsSync(p.events)) {
769
+ warns.push('OXE-EVENTS.ndjson não existe para o escopo atual');
770
+ }
771
+ const runState = operational.readRunState(path.dirname(p.oxe), p.activeSession || null);
772
+ const checkpointRows = [];
773
+ if (fs.existsSync(p.checkpoints)) {
774
+ const checkpointText = fs.readFileSync(p.checkpoints, 'utf8');
775
+ for (const line of checkpointText.split('\n')) {
776
+ const match = line.match(/^\|\s*(CP-[^|]+)\|\s*[^|]*\|\s*[^|]*\|\s*[^|]*\|\s*([^|]+)\|/i);
777
+ if (!match) continue;
778
+ checkpointRows.push({ id: match[1].trim(), status: match[2].trim() });
779
+ }
780
+ }
781
+ for (const warn of operational.runtimeStateWarnings(runState, checkpointRows)) warns.push(warn);
782
+ return warns;
783
+ }
784
+
785
+ /**
786
+ * @param {ReturnType<typeof scopedOxePaths>} p
787
+ * @returns {string[]}
788
+ */
789
+ function capabilityWarnings(p) {
790
+ /** @type {string[]} */
791
+ const warns = [];
792
+ if (fs.existsSync(p.capabilitiesDir) && !fs.existsSync(p.capabilitiesIndex)) {
793
+ warns.push('Existem capabilities em .oxe/capabilities/, mas .oxe/CAPABILITIES.md não existe');
794
+ }
795
+ for (const warn of operational.capabilityCatalogWarnings(path.dirname(p.oxe))) warns.push(warn);
796
+ return warns;
797
+ }
798
+
799
+ /**
800
+ * @param {ReturnType<typeof scopedOxePaths>} p
801
+ * @returns {string[]}
802
+ */
803
+ function investigationWarnings(p) {
804
+ /** @type {string[]} */
805
+ const warns = [];
806
+ if (fs.existsSync(p.investigationsDir) && !fs.existsSync(p.investigationsIndex)) {
807
+ warns.push('Existe pasta de investigações, mas falta o índice INVESTIGATIONS.md');
808
+ }
809
+ return warns;
810
+ }
811
+
812
+ /**
813
+ * @param {string} stateText
814
+ * @param {ReturnType<typeof scopedOxePaths>} p
815
+ * @returns {string[]}
816
+ */
817
+ function planReviewWarnings(stateText, p) {
818
+ /** @type {string[]} */
819
+ const warns = [];
820
+ const reviewStatus = parsePlanReviewStatus(stateText);
821
+ if (fs.existsSync(p.plan) && !reviewStatus) {
822
+ warns.push('PLAN.md existe, mas STATE.md não declara plan_review_status');
823
+ }
824
+ if (reviewStatus && !fs.existsSync(p.planReview)) {
825
+ warns.push('STATE.md declara revisão do plano, mas PLAN-REVIEW.md não existe');
826
+ }
827
+ if (reviewStatus === 'needs_revision' || reviewStatus === 'rejected') {
828
+ warns.push(`Plano em estado ${reviewStatus} — revisão adicional necessária antes de executar`);
829
+ }
830
+ return warns;
831
+ }
485
832
 
486
833
  /**
487
834
  * Próximo passo único (espelha o workflow next.md).
488
835
  * @param {string} target
489
836
  * @param {{ discuss_before_plan?: boolean }} cfg
490
837
  */
491
- function suggestNextStep(target, cfg = {}) {
492
- const p = oxePaths(target);
493
- const discussBefore = Boolean(cfg.discuss_before_plan);
494
- const has = (/** @type {string} */ f) => fs.existsSync(f);
495
- const mapsComplete = EXPECTED_CODEBASE_MAPS.every((f) => has(path.join(p.codebase, f)));
496
-
497
- if (!has(p.oxe) || !has(p.state)) {
838
+ function suggestNextStep(target, cfg = {}) {
839
+ const base = oxePaths(target);
840
+ const stateText = fs.existsSync(base.state) ? fs.readFileSync(base.state, 'utf8') : '';
841
+ const p = scopedOxePaths(target, parseActiveSession(stateText));
842
+ const discussBefore = Boolean(cfg.discuss_before_plan);
843
+ const threshold = Number(cfg.plan_confidence_threshold) || 70;
844
+ const has = (/** @type {string} */ f) => fs.existsSync(f);
845
+ const mapsComplete = EXPECTED_CODEBASE_MAPS.every((f) => has(path.join(p.codebase, f)));
846
+ const azureActive = azure.isAzureContextEnabled(target, cfg);
847
+
848
+ if (!has(p.oxe) || !has(p.state)) {
498
849
  return {
499
850
  step: 'scan',
500
851
  cursorCmd: '/oxe-scan',
@@ -503,19 +854,56 @@ function suggestNextStep(target, cfg = {}) {
503
854
  };
504
855
  }
505
856
 
506
- const stateText = fs.readFileSync(p.state, 'utf8');
507
- const phase = parseStatePhase(stateText);
857
+ const phase = parseStatePhase(stateText);
508
858
 
509
- if (!mapsComplete && !has(p.quick)) {
859
+ if (!mapsComplete && !has(p.quick)) {
510
860
  return {
511
861
  step: 'scan',
512
862
  cursorCmd: '/oxe-scan',
513
863
  reason: 'Mapas do codebase incompletos e sem QUICK.md — atualize o contexto com scan',
514
864
  artifacts: ['.oxe/codebase/'],
515
- };
516
- }
517
-
518
- if (phase === 'quick_active' || (has(p.quick) && !has(p.plan))) {
865
+ };
866
+ }
867
+
868
+ if (azureActive) {
869
+ const azureHealth = azure.azureDoctor(target, cfg, {
870
+ autoInstall: false,
871
+ write: false,
872
+ });
873
+ if (!azureHealth.authStatus || !azureHealth.authStatus.login_active) {
874
+ return {
875
+ step: 'azure-auth',
876
+ cursorCmd: 'npx oxe-cc azure auth login',
877
+ reason: 'Contexto Azure ativo, mas sem sessão Azure CLI autenticada',
878
+ artifacts: ['.oxe/cloud/azure/profile.json', '.oxe/cloud/azure/auth-status.json'],
879
+ };
880
+ }
881
+ if (!azureHealth.profile || !azureHealth.profile.subscription_id) {
882
+ return {
883
+ step: 'azure-auth',
884
+ cursorCmd: 'npx oxe-cc azure auth set-subscription --subscription <id-ou-nome>',
885
+ reason: 'Contexto Azure ativo, mas a subscription operacional ainda não está definida',
886
+ artifacts: ['.oxe/cloud/azure/profile.json', '.oxe/cloud/azure/auth-status.json'],
887
+ };
888
+ }
889
+ const maxAgeHours = cfg.azure && cfg.azure.inventory_max_age_hours != null
890
+ ? Number(cfg.azure.inventory_max_age_hours)
891
+ : 24;
892
+ const syncedAt = Date.parse(String(azureHealth.inventory && azureHealth.inventory.synced_at || ''));
893
+ const staleInventory =
894
+ !azureHealth.inventory ||
895
+ (maxAgeHours > 0 && !Number.isNaN(syncedAt) && ((Date.now() - syncedAt) / (1000 * 60 * 60)) > maxAgeHours);
896
+ if (staleInventory) {
897
+ return {
898
+ step: 'azure-sync',
899
+ cursorCmd: 'npx oxe-cc azure sync',
900
+ reason: 'Contexto Azure ativo, mas o inventário está ausente ou stale',
901
+ artifacts: ['.oxe/cloud/azure/inventory.json', '.oxe/cloud/azure/INVENTORY.md'],
902
+ };
903
+ }
904
+ }
905
+
906
+ if (phase === 'quick_active' || (has(p.quick) && !has(p.plan))) {
519
907
  return {
520
908
  step: 'execute',
521
909
  cursorCmd: '/oxe-execute',
@@ -542,16 +930,71 @@ function suggestNextStep(target, cfg = {}) {
542
930
  };
543
931
  }
544
932
 
545
- if (!has(p.plan)) {
546
- return {
547
- step: 'plan',
548
- cursorCmd: '/oxe-plan',
549
- reason: 'SPEC existe mas PLAN.md não — gere o plano com verificação por tarefa',
550
- artifacts: ['.oxe/PLAN.md'],
551
- };
552
- }
553
-
554
- if (!has(p.verify)) {
933
+ if (!has(p.plan)) {
934
+ return {
935
+ step: 'plan',
936
+ cursorCmd: '/oxe-plan',
937
+ reason: 'SPEC existe mas PLAN.md não — gere o plano com verificação por tarefa',
938
+ artifacts: ['.oxe/PLAN.md'],
939
+ };
940
+ }
941
+
942
+ const selfEval = parsePlanSelfEvaluation(p.plan);
943
+ if (selfEval.bestPlan === 'não' || (selfEval.confidence != null && selfEval.confidence < threshold)) {
944
+ return {
945
+ step: 'plan',
946
+ cursorCmd: '/oxe-plan --replan',
947
+ reason: `O plano atual ainda não atingiu confiança executável (limiar ${threshold}%)`,
948
+ artifacts: ['.oxe/PLAN.md', '.oxe/STATE.md'],
949
+ };
950
+ }
951
+
952
+ const reviewStatus = parsePlanReviewStatus(stateText);
953
+ if (phase === 'plan_ready' && (reviewStatus === 'needs_revision' || reviewStatus === 'rejected')) {
954
+ return {
955
+ step: 'plan',
956
+ cursorCmd: '/oxe-plan --replan',
957
+ reason: `Revisão do plano em estado ${reviewStatus} — ajuste o plano antes de executar`,
958
+ artifacts: ['.oxe/PLAN.md', '.oxe/PLAN-REVIEW.md', '.oxe/STATE.md'],
959
+ };
960
+ }
961
+ if (phase === 'plan_ready' && (!reviewStatus || reviewStatus === 'draft' || reviewStatus === 'in_review')) {
962
+ return {
963
+ step: 'dashboard',
964
+ cursorCmd: '/oxe-dashboard',
965
+ reason: 'Plano pronto, mas ainda não passou por revisão/aprovação visual',
966
+ artifacts: ['.oxe/PLAN.md', '.oxe/PLAN-REVIEW.md', '.oxe/STATE.md'],
967
+ };
968
+ }
969
+
970
+ const activeRun = operational.readRunState(target, parseActiveSession(stateText));
971
+ if (activeRun && activeRun.status === 'waiting_approval') {
972
+ return {
973
+ step: 'dashboard',
974
+ cursorCmd: '/oxe-dashboard',
975
+ reason: 'ACTIVE-RUN está aguardando aprovação formal antes de continuar',
976
+ artifacts: ['.oxe/ACTIVE-RUN.json', '.oxe/CHECKPOINTS.md', '.oxe/OXE-EVENTS.ndjson'],
977
+ };
978
+ }
979
+ if (activeRun && activeRun.status === 'paused') {
980
+ return {
981
+ step: 'execute',
982
+ cursorCmd: '/oxe-execute',
983
+ reason: 'ACTIVE-RUN está pausado — retome a execução a partir do cursor atual',
984
+ artifacts: ['.oxe/ACTIVE-RUN.json', '.oxe/EXECUTION-RUNTIME.md'],
985
+ };
986
+ }
987
+
988
+ if (/\*\*checkpoint_status:\*\*\s*`?pending_approval`?/i.test(stateText)) {
989
+ return {
990
+ step: 'execute',
991
+ cursorCmd: '/oxe-execute',
992
+ reason: 'Há checkpoint pendente de aprovação — resolva a aprovação antes de avançar a execução',
993
+ artifacts: ['.oxe/CHECKPOINTS.md', '.oxe/STATE.md'],
994
+ };
995
+ }
996
+
997
+ if (!has(p.verify)) {
555
998
  return {
556
999
  step: 'execute',
557
1000
  cursorCmd: '/oxe-execute',
@@ -622,56 +1065,140 @@ function suggestNextStep(target, cfg = {}) {
622
1065
  /**
623
1066
  * @param {string} target
624
1067
  */
625
- function buildHealthReport(target) {
626
- const { config, path: cfgPath, parseError } = loadOxeConfigMerged(target);
627
- const shape = validateConfigShape(config);
628
- const p = oxePaths(target);
629
- let stateText = '';
630
- if (fs.existsSync(p.state)) {
631
- try {
632
- stateText = fs.readFileSync(p.state, 'utf8');
633
- } catch {
634
- stateText = '';
635
- }
636
- }
637
- const phase = parseStatePhase(stateText);
1068
+ function buildHealthReport(target) {
1069
+ const { config, path: cfgPath, parseError } = loadOxeConfigMerged(target);
1070
+ const shape = validateConfigShape(config);
1071
+ const base = oxePaths(target);
1072
+ let stateText = '';
1073
+ if (fs.existsSync(base.state)) {
1074
+ try {
1075
+ stateText = fs.readFileSync(base.state, 'utf8');
1076
+ } catch {
1077
+ stateText = '';
1078
+ }
1079
+ }
1080
+ const activeSession = parseActiveSession(stateText);
1081
+ const p = scopedOxePaths(target, activeSession);
1082
+ const phase = parseStatePhase(stateText);
638
1083
  const scanDate = parseLastScanDate(stateText);
639
1084
  const stale = isStaleScan(scanDate, Number(config.scan_max_age_days) || 0);
640
1085
  const compactDate = parseLastCompactDate(stateText);
641
1086
  const staleCompact = isStaleScan(compactDate, Number(config.compact_max_age_days) || 0);
642
1087
  const retroDate = parseLastRetroDate(stateText);
643
- const staleLessons = isStaleLessons(retroDate, Number(config.lessons_max_age_days) || 0);
644
- const phaseWarn = phase ? phaseCoherenceWarnings(phase, p) : [];
645
- const sumWarn = verifyGapsWithoutSummaryWarning(p.verify, p.summary);
646
- const specReq = Array.isArray(config.spec_required_sections) ? config.spec_required_sections : [];
647
- const specWarn = specSectionWarnings(p.spec, specReq.map(String));
648
- const planWarn = [
649
- ...planWaveWarningsFixed(p.plan, Number(config.plan_max_tasks_per_wave) || 0),
650
- ...planTaskAceiteWarnings(p.plan),
651
- ...planAgentsWarnings(target),
652
- ];
653
- const next = suggestNextStep(target, { discuss_before_plan: config.discuss_before_plan });
654
-
655
- return {
1088
+ const staleLessons = isStaleLessons(retroDate, Number(config.lessons_max_age_days) || 0);
1089
+ const phaseWarn = phase ? phaseCoherenceWarnings(phase, p) : [];
1090
+ const runtimeWarn = runtimeWarnings(stateText, p);
1091
+ const sumWarn = verifyGapsWithoutSummaryWarning(p.verify, p.summary);
1092
+ const specReq = Array.isArray(config.spec_required_sections) ? config.spec_required_sections : [];
1093
+ const specWarn = specSectionWarnings(p.spec, specReq.map(String));
1094
+ const threshold = Number(config.plan_confidence_threshold) || 70;
1095
+ const capabilityWarn = capabilityWarnings(p);
1096
+ const investigationWarn = investigationWarnings(p);
1097
+ const planWarn = [
1098
+ ...planWaveWarningsFixed(p.plan, Number(config.plan_max_tasks_per_wave) || 0),
1099
+ ...planTaskAceiteWarnings(p.plan),
1100
+ ...planSelfEvaluationWarnings(p.plan, threshold),
1101
+ ...planAgentsWarnings(target),
1102
+ ];
1103
+ const sessionWarn = sessionWarnings(target, activeSession);
1104
+ const installWarn = installationCompletenessWarnings(target);
1105
+ const reviewWarn = planReviewWarnings(stateText, p);
1106
+ const planSelfEvaluation = parsePlanSelfEvaluation(p.plan);
1107
+ const activeRun = operational.readRunState(target, activeSession);
1108
+ const eventsSummary = operational.summarizeEvents(operational.readEvents(target, activeSession));
1109
+ const memoryLayers = operational.buildMemoryLayers(target, activeSession);
1110
+ const azureActive = azure.isAzureContextEnabled(target, config);
1111
+ const azureReport = azureActive
1112
+ ? azure.azureDoctor(target, config, {
1113
+ autoInstall: false,
1114
+ write: false,
1115
+ })
1116
+ : null;
1117
+ const azureInventorySyncedAt = azureReport && azureReport.inventory ? azureReport.inventory.synced_at || null : null;
1118
+ const azureInventoryMaxAgeHours = config.azure && config.azure.inventory_max_age_hours != null
1119
+ ? Number(config.azure.inventory_max_age_hours)
1120
+ : 24;
1121
+ let azureInventoryStale = { stale: false, hours: null };
1122
+ if (azureInventorySyncedAt) {
1123
+ const syncedAt = Date.parse(String(azureInventorySyncedAt));
1124
+ if (!Number.isNaN(syncedAt)) {
1125
+ const ageHours = Math.floor((Date.now() - syncedAt) / (1000 * 60 * 60));
1126
+ azureInventoryStale = {
1127
+ stale: azureInventoryMaxAgeHours > 0 ? ageHours > azureInventoryMaxAgeHours : false,
1128
+ hours: ageHours,
1129
+ };
1130
+ }
1131
+ } else if (azureActive) {
1132
+ azureInventoryStale = { stale: true, hours: null };
1133
+ }
1134
+ const next = suggestNextStep(target, {
1135
+ discuss_before_plan: config.discuss_before_plan,
1136
+ plan_confidence_threshold: threshold,
1137
+ azure: config.azure,
1138
+ });
1139
+ const hardFailure = Boolean(parseError) || sessionWarn.some((w) => /não existe|sem SESSION\.md/i.test(w));
1140
+ const warningCount =
1141
+ phaseWarn.length +
1142
+ runtimeWarn.length +
1143
+ reviewWarn.length +
1144
+ specWarn.length +
1145
+ planWarn.length +
1146
+ capabilityWarn.length +
1147
+ investigationWarn.length +
1148
+ sessionWarn.length +
1149
+ installWarn.length +
1150
+ (azureReport ? azureReport.warnings.length : 0) +
1151
+ (sumWarn ? 1 : 0);
1152
+ const healthStatus = hardFailure ? 'broken' : warningCount > 0 ? 'warning' : 'healthy';
1153
+
1154
+ return {
656
1155
  configPath: cfgPath,
657
1156
  configParseError: parseError,
658
1157
  unknownConfigKeys: shape.unknownKeys,
659
- typeErrors: shape.typeErrors,
660
- phase,
661
- scanDate,
1158
+ typeErrors: shape.typeErrors,
1159
+ phase,
1160
+ activeSession,
1161
+ scanDate,
662
1162
  stale,
663
1163
  compactDate,
664
1164
  staleCompact,
665
1165
  retroDate,
666
- staleLessons,
667
- phaseWarn,
668
- summaryGapWarn: sumWarn,
669
- specWarn,
670
- planWarn,
671
- next,
672
- scanFocusGlobs: config.scan_focus_globs,
673
- scanIgnoreGlobs: config.scan_ignore_globs,
674
- };
1166
+ staleLessons,
1167
+ phaseWarn,
1168
+ runtimeWarn,
1169
+ reviewWarn,
1170
+ capabilityWarn,
1171
+ investigationWarn,
1172
+ sessionWarn,
1173
+ installWarn,
1174
+ summaryGapWarn: sumWarn,
1175
+ specWarn,
1176
+ planWarn,
1177
+ planSelfEvaluation,
1178
+ planReviewStatus: parsePlanReviewStatus(stateText),
1179
+ activeRun,
1180
+ eventsSummary,
1181
+ memoryLayers,
1182
+ azureActive,
1183
+ azure: azureReport
1184
+ ? {
1185
+ profile: azureReport.profile,
1186
+ authStatus: azureReport.authStatus,
1187
+ inventorySummary: azureReport.inventorySummary,
1188
+ inventoryPath: azureReport.paths.inventory,
1189
+ operationsPath: azureReport.paths.operationsDir,
1190
+ inventorySyncedAt: azureInventorySyncedAt,
1191
+ inventoryStale: azureInventoryStale,
1192
+ pendingOperations: azure.listAzureOperations(target).filter((operation) => operation.phase === 'waiting_approval').length,
1193
+ lastOperation: azure.listAzureOperations(target)[0] || null,
1194
+ warnings: azureReport.warnings,
1195
+ }
1196
+ : null,
1197
+ healthStatus,
1198
+ next,
1199
+ scanFocusGlobs: config.scan_focus_globs,
1200
+ scanIgnoreGlobs: config.scan_ignore_globs,
1201
+ };
675
1202
  }
676
1203
 
677
1204
  module.exports = {
@@ -686,18 +1213,28 @@ module.exports = {
686
1213
  loadOxeConfigMerged,
687
1214
  validateConfigShape,
688
1215
  parseStatePhase,
689
- parseLastScanDate,
690
- parseLastCompactDate,
691
- parseLastRetroDate,
692
- isStaleScan,
693
- isStaleLessons,
694
- planAgentsWarnings,
695
- phaseCoherenceWarnings,
1216
+ parseLastScanDate,
1217
+ parseLastCompactDate,
1218
+ parseLastRetroDate,
1219
+ parseActiveSession,
1220
+ parsePlanReviewStatus,
1221
+ isStaleScan,
1222
+ isStaleLessons,
1223
+ planAgentsWarnings,
1224
+ installationCompletenessWarnings,
1225
+ parsePlanSelfEvaluation,
1226
+ planSelfEvaluationWarnings,
1227
+ runtimeWarnings,
1228
+ planReviewWarnings,
1229
+ capabilityWarnings,
1230
+ investigationWarnings,
1231
+ phaseCoherenceWarnings,
696
1232
  verifyGapsWithoutSummaryWarning,
697
1233
  specSectionWarnings,
698
1234
  planWaveWarningsFixed,
699
1235
  planTaskAceiteWarnings,
700
- suggestNextStep,
701
- buildHealthReport,
702
- oxePaths,
703
- };
1236
+ suggestNextStep,
1237
+ buildHealthReport,
1238
+ oxePaths,
1239
+ scopedOxePaths,
1240
+ };