oxe-cc 0.6.6 → 0.7.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 (48) hide show
  1. package/.cursor/commands/oxe-capabilities.md +11 -0
  2. package/.cursor/commands/oxe-dashboard.md +11 -0
  3. package/.github/copilot-instructions.md +13 -1
  4. package/.github/prompts/oxe-capabilities.prompt.md +12 -0
  5. package/.github/prompts/oxe-dashboard.prompt.md +12 -0
  6. package/CHANGELOG.md +33 -0
  7. package/README.md +147 -11
  8. package/assets/oxe-framework-artifacts-paper.png +0 -0
  9. package/bin/banner.txt +1 -1
  10. package/bin/lib/oxe-azure.cjs +1445 -0
  11. package/bin/lib/oxe-dashboard.cjs +588 -0
  12. package/bin/lib/oxe-install-resolve.cjs +4 -1
  13. package/bin/lib/oxe-operational.cjs +670 -0
  14. package/bin/lib/oxe-project-health.cjs +372 -28
  15. package/bin/oxe-cc.js +1517 -312
  16. package/commands/oxe/capabilities.md +13 -0
  17. package/commands/oxe/dashboard.md +14 -0
  18. package/lib/sdk/README.md +9 -7
  19. package/lib/sdk/index.cjs +56 -0
  20. package/lib/sdk/index.d.ts +73 -0
  21. package/oxe/templates/ACTIVE-RUN.template.json +32 -0
  22. package/oxe/templates/CAPABILITIES.template.md +7 -0
  23. package/oxe/templates/CAPABILITY.template.md +45 -0
  24. package/oxe/templates/CHECKPOINTS.template.md +7 -0
  25. package/oxe/templates/EXECUTION-RUNTIME.template.md +68 -0
  26. package/oxe/templates/INVESTIGATION.template.md +38 -0
  27. package/oxe/templates/NOTES.template.md +16 -0
  28. package/oxe/templates/PLAN-REVIEW.template.md +31 -0
  29. package/oxe/templates/RESEARCH.template.md +11 -4
  30. package/oxe/templates/SPEC.template.md +6 -4
  31. package/oxe/templates/STATE.md +45 -7
  32. package/oxe/templates/config.template.json +11 -3
  33. package/oxe/workflows/ask.md +10 -1
  34. package/oxe/workflows/capabilities.md +23 -0
  35. package/oxe/workflows/dashboard.md +23 -0
  36. package/oxe/workflows/discuss.md +11 -9
  37. package/oxe/workflows/execute.md +57 -35
  38. package/oxe/workflows/help.md +256 -225
  39. package/oxe/workflows/obs.md +70 -20
  40. package/oxe/workflows/plan.md +83 -74
  41. package/oxe/workflows/quick.md +16 -11
  42. package/oxe/workflows/references/adaptive-discovery.md +27 -0
  43. package/oxe/workflows/research.md +12 -8
  44. package/oxe/workflows/retro.md +30 -5
  45. package/oxe/workflows/scan.md +1 -0
  46. package/oxe/workflows/spec.md +65 -48
  47. package/oxe/workflows/verify.md +52 -37
  48. 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 = [
@@ -23,10 +25,11 @@ const ALLOWED_CONFIG_KEYS = [
23
25
  'security_in_verify',
24
26
  'install',
25
27
  'plugins',
26
- 'workstreams',
27
- 'milestones',
28
- 'scale_adaptive',
29
- ];
28
+ 'workstreams',
29
+ 'milestones',
30
+ 'scale_adaptive',
31
+ 'azure',
32
+ ];
30
33
 
31
34
  /**
32
35
  * Profiles de execução OXE que controlam rigor do workflow.
@@ -125,9 +128,17 @@ function loadOxeConfigMerged(targetProject) {
125
128
  compact_max_age_days: 0,
126
129
  scan_focus_globs: [],
127
130
  scan_ignore_globs: [],
128
- spec_required_sections: [],
129
- plan_max_tasks_per_wave: 0,
130
- };
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
+ };
131
142
  const p = path.join(targetProject, '.oxe', 'config.json');
132
143
  if (!fs.existsSync(p)) return { config: defaults, path: null, parseError: null };
133
144
  try {
@@ -226,11 +237,49 @@ function validateConfigShape(cfg) {
226
237
  if (cfg.after_verify_suggest_uat != null && typeof cfg.after_verify_suggest_uat !== 'boolean') {
227
238
  typeErrors.push('after_verify_suggest_uat deve ser boolean');
228
239
  }
229
- if (cfg.scale_adaptive != null && typeof cfg.scale_adaptive !== 'boolean') {
230
- typeErrors.push('scale_adaptive deve ser boolean');
231
- }
232
- return { unknownKeys, typeErrors };
233
- }
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
+ }
234
283
 
235
284
  /**
236
285
  * @param {string} stateText
@@ -310,6 +359,19 @@ function parseActiveSession(stateText) {
310
359
  if (!raw || raw === '—' || /^none$/i.test(raw)) return null;
311
360
  return raw.replace(/\\/g, '/');
312
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
+ }
313
375
 
314
376
  /**
315
377
  * @param {Date | null} scanDate
@@ -339,14 +401,26 @@ function oxePaths(target) {
339
401
  return {
340
402
  oxe,
341
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'),
342
411
  sessionsIndex: path.join(oxe, 'SESSIONS.md'),
343
412
  globalDir: path.join(oxe, 'global'),
344
413
  globalLessons: path.join(oxe, 'global', 'LESSONS.md'),
345
414
  globalMilestones: path.join(oxe, 'global', 'MILESTONES.md'),
346
415
  globalMilestonesDir: path.join(oxe, 'global', 'milestones'),
347
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'),
348
422
  spec: path.join(oxe, 'SPEC.md'),
349
- plan: path.join(oxe, 'PLAN.md'),
423
+ plan: path.join(oxe, 'PLAN.md'),
350
424
  quick: path.join(oxe, 'QUICK.md'),
351
425
  verify: path.join(oxe, 'VERIFY.md'),
352
426
  discuss: path.join(oxe, 'DISCUSS.md'),
@@ -371,6 +445,15 @@ function scopedOxePaths(target, activeSession) {
371
445
  scopedRoot: sessionRoot,
372
446
  sessionRoot,
373
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'),
374
457
  spec: path.join(sessionRoot, 'spec', 'SPEC.md'),
375
458
  discuss: path.join(sessionRoot, 'spec', 'DISCUSS.md'),
376
459
  plan: path.join(sessionRoot, 'plan', 'PLAN.md'),
@@ -637,6 +720,113 @@ function installationCompletenessWarnings(target) {
637
720
  if (!fs.existsSync(p.globalMilestones)) warns.push('.oxe/global/MILESTONES.md ausente');
638
721
  if (!fs.existsSync(p.globalMilestonesDir)) warns.push('.oxe/global/milestones/ ausente');
639
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
+ }
640
830
  return warns;
641
831
  }
642
832
 
@@ -653,8 +843,9 @@ function suggestNextStep(target, cfg = {}) {
653
843
  const threshold = Number(cfg.plan_confidence_threshold) || 70;
654
844
  const has = (/** @type {string} */ f) => fs.existsSync(f);
655
845
  const mapsComplete = EXPECTED_CODEBASE_MAPS.every((f) => has(path.join(p.codebase, f)));
656
-
657
- if (!has(p.oxe) || !has(p.state)) {
846
+ const azureActive = azure.isAzureContextEnabled(target, cfg);
847
+
848
+ if (!has(p.oxe) || !has(p.state)) {
658
849
  return {
659
850
  step: 'scan',
660
851
  cursorCmd: '/oxe-scan',
@@ -665,16 +856,54 @@ function suggestNextStep(target, cfg = {}) {
665
856
 
666
857
  const phase = parseStatePhase(stateText);
667
858
 
668
- if (!mapsComplete && !has(p.quick)) {
859
+ if (!mapsComplete && !has(p.quick)) {
669
860
  return {
670
861
  step: 'scan',
671
862
  cursorCmd: '/oxe-scan',
672
863
  reason: 'Mapas do codebase incompletos e sem QUICK.md — atualize o contexto com scan',
673
864
  artifacts: ['.oxe/codebase/'],
674
- };
675
- }
676
-
677
- 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))) {
678
907
  return {
679
908
  step: 'execute',
680
909
  cursorCmd: '/oxe-execute',
@@ -719,8 +948,53 @@ function suggestNextStep(target, cfg = {}) {
719
948
  artifacts: ['.oxe/PLAN.md', '.oxe/STATE.md'],
720
949
  };
721
950
  }
722
-
723
- if (!has(p.verify)) {
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)) {
724
998
  return {
725
999
  step: 'execute',
726
1000
  cursorCmd: '/oxe-execute',
@@ -813,10 +1087,13 @@ function buildHealthReport(target) {
813
1087
  const retroDate = parseLastRetroDate(stateText);
814
1088
  const staleLessons = isStaleLessons(retroDate, Number(config.lessons_max_age_days) || 0);
815
1089
  const phaseWarn = phase ? phaseCoherenceWarnings(phase, p) : [];
1090
+ const runtimeWarn = runtimeWarnings(stateText, p);
816
1091
  const sumWarn = verifyGapsWithoutSummaryWarning(p.verify, p.summary);
817
1092
  const specReq = Array.isArray(config.spec_required_sections) ? config.spec_required_sections : [];
818
1093
  const specWarn = specSectionWarnings(p.spec, specReq.map(String));
819
1094
  const threshold = Number(config.plan_confidence_threshold) || 70;
1095
+ const capabilityWarn = capabilityWarnings(p);
1096
+ const investigationWarn = investigationWarnings(p);
820
1097
  const planWarn = [
821
1098
  ...planWaveWarningsFixed(p.plan, Number(config.plan_max_tasks_per_wave) || 0),
822
1099
  ...planTaskAceiteWarnings(p.plan),
@@ -825,14 +1102,53 @@ function buildHealthReport(target) {
825
1102
  ];
826
1103
  const sessionWarn = sessionWarnings(target, activeSession);
827
1104
  const installWarn = installationCompletenessWarnings(target);
1105
+ const reviewWarn = planReviewWarnings(stateText, p);
828
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
+ }
829
1134
  const next = suggestNextStep(target, {
830
1135
  discuss_before_plan: config.discuss_before_plan,
831
1136
  plan_confidence_threshold: threshold,
1137
+ azure: config.azure,
832
1138
  });
833
1139
  const hardFailure = Boolean(parseError) || sessionWarn.some((w) => /não existe|sem SESSION\.md/i.test(w));
834
1140
  const warningCount =
835
- phaseWarn.length + specWarn.length + planWarn.length + sessionWarn.length + installWarn.length + (sumWarn ? 1 : 0);
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);
836
1152
  const healthStatus = hardFailure ? 'broken' : warningCount > 0 ? 'warning' : 'healthy';
837
1153
 
838
1154
  return {
@@ -849,17 +1165,40 @@ function buildHealthReport(target) {
849
1165
  retroDate,
850
1166
  staleLessons,
851
1167
  phaseWarn,
1168
+ runtimeWarn,
1169
+ reviewWarn,
1170
+ capabilityWarn,
1171
+ investigationWarn,
852
1172
  sessionWarn,
853
1173
  installWarn,
854
1174
  summaryGapWarn: sumWarn,
855
1175
  specWarn,
856
1176
  planWarn,
857
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,
858
1197
  healthStatus,
859
1198
  next,
860
- scanFocusGlobs: config.scan_focus_globs,
861
- scanIgnoreGlobs: config.scan_ignore_globs,
862
- };
1199
+ scanFocusGlobs: config.scan_focus_globs,
1200
+ scanIgnoreGlobs: config.scan_ignore_globs,
1201
+ };
863
1202
  }
864
1203
 
865
1204
  module.exports = {
@@ -878,12 +1217,17 @@ module.exports = {
878
1217
  parseLastCompactDate,
879
1218
  parseLastRetroDate,
880
1219
  parseActiveSession,
1220
+ parsePlanReviewStatus,
881
1221
  isStaleScan,
882
1222
  isStaleLessons,
883
1223
  planAgentsWarnings,
884
1224
  installationCompletenessWarnings,
885
1225
  parsePlanSelfEvaluation,
886
1226
  planSelfEvaluationWarnings,
1227
+ runtimeWarnings,
1228
+ planReviewWarnings,
1229
+ capabilityWarnings,
1230
+ investigationWarnings,
887
1231
  phaseCoherenceWarnings,
888
1232
  verifyGapsWithoutSummaryWarning,
889
1233
  specSectionWarnings,