oxe-cc 0.6.6 → 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.
- package/.cursor/commands/oxe-capabilities.md +11 -0
- package/.cursor/commands/oxe-dashboard.md +11 -0
- package/.github/prompts/oxe-capabilities.prompt.md +12 -0
- package/.github/prompts/oxe-dashboard.prompt.md +12 -0
- package/CHANGELOG.md +33 -0
- package/README.md +147 -11
- package/assets/oxe-framework-artifacts-paper.png +0 -0
- package/bin/banner.txt +1 -1
- package/bin/lib/oxe-azure.cjs +1445 -0
- package/bin/lib/oxe-dashboard.cjs +588 -0
- package/bin/lib/oxe-install-resolve.cjs +4 -1
- package/bin/lib/oxe-operational.cjs +670 -0
- package/bin/lib/oxe-project-health.cjs +372 -28
- package/bin/oxe-cc.js +1517 -312
- package/commands/oxe/capabilities.md +13 -0
- package/commands/oxe/dashboard.md +14 -0
- package/lib/sdk/README.md +9 -7
- package/lib/sdk/index.cjs +56 -0
- package/lib/sdk/index.d.ts +73 -0
- package/oxe/templates/ACTIVE-RUN.template.json +32 -0
- package/oxe/templates/CAPABILITIES.template.md +7 -0
- package/oxe/templates/CAPABILITY.template.md +45 -0
- package/oxe/templates/CHECKPOINTS.template.md +7 -0
- package/oxe/templates/EXECUTION-RUNTIME.template.md +68 -0
- package/oxe/templates/INVESTIGATION.template.md +38 -0
- package/oxe/templates/NOTES.template.md +16 -0
- package/oxe/templates/PLAN-REVIEW.template.md +31 -0
- package/oxe/templates/RESEARCH.template.md +11 -4
- package/oxe/templates/SPEC.template.md +6 -4
- package/oxe/templates/STATE.md +45 -7
- package/oxe/templates/config.template.json +11 -3
- package/oxe/workflows/ask.md +10 -1
- package/oxe/workflows/capabilities.md +23 -0
- package/oxe/workflows/dashboard.md +23 -0
- package/oxe/workflows/discuss.md +11 -9
- package/oxe/workflows/execute.md +57 -35
- package/oxe/workflows/help.md +256 -225
- package/oxe/workflows/obs.md +70 -20
- package/oxe/workflows/plan.md +83 -74
- package/oxe/workflows/quick.md +16 -11
- package/oxe/workflows/references/adaptive-discovery.md +27 -0
- package/oxe/workflows/research.md +12 -8
- package/oxe/workflows/retro.md +30 -5
- package/oxe/workflows/scan.md +1 -0
- package/oxe/workflows/spec.md +65 -48
- package/oxe/workflows/verify.md +52 -37
- 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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 +
|
|
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,
|