oxe-cc 0.7.0 → 0.9.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 (157) hide show
  1. package/.cursor/commands/oxe-ask.md +34 -0
  2. package/.cursor/commands/oxe-capabilities.md +34 -0
  3. package/.cursor/commands/oxe-checkpoint.md +34 -0
  4. package/.cursor/commands/oxe-compact.md +33 -0
  5. package/.cursor/commands/oxe-dashboard.md +34 -0
  6. package/.cursor/commands/oxe-debug.md +34 -0
  7. package/.cursor/commands/oxe-discuss.md +34 -0
  8. package/.cursor/commands/oxe-execute.md +34 -0
  9. package/.cursor/commands/oxe-forensics.md +34 -0
  10. package/.cursor/commands/oxe-help.md +33 -0
  11. package/.cursor/commands/oxe-loop.md +34 -0
  12. package/.cursor/commands/oxe-milestone.md +34 -0
  13. package/.cursor/commands/oxe-next.md +33 -0
  14. package/.cursor/commands/oxe-obs.md +34 -0
  15. package/.cursor/commands/oxe-plan-agent.md +33 -0
  16. package/.cursor/commands/oxe-plan.md +34 -0
  17. package/.cursor/commands/oxe-project.md +34 -0
  18. package/.cursor/commands/oxe-quick.md +34 -0
  19. package/.cursor/commands/oxe-research.md +34 -0
  20. package/.cursor/commands/oxe-retro.md +34 -0
  21. package/.cursor/commands/oxe-review-pr.md +34 -0
  22. package/.cursor/commands/oxe-route.md +34 -0
  23. package/.cursor/commands/oxe-scan.md +34 -0
  24. package/.cursor/commands/oxe-security.md +34 -0
  25. package/.cursor/commands/oxe-session.md +34 -0
  26. package/.cursor/commands/oxe-skill.md +45 -0
  27. package/.cursor/commands/oxe-spec.md +34 -0
  28. package/.cursor/commands/oxe-ui-review.md +34 -0
  29. package/.cursor/commands/oxe-ui-spec.md +34 -0
  30. package/.cursor/commands/oxe-update.md +33 -0
  31. package/.cursor/commands/oxe-validate-gaps.md +34 -0
  32. package/.cursor/commands/oxe-verify.md +34 -0
  33. package/.cursor/commands/oxe-workstream.md +34 -0
  34. package/.cursor/commands/oxe.md +38 -2
  35. package/.github/copilot-instructions.md +20 -5
  36. package/.github/prompts/oxe-ask.prompt.md +33 -0
  37. package/.github/prompts/oxe-capabilities.prompt.md +33 -0
  38. package/.github/prompts/oxe-checkpoint.prompt.md +45 -12
  39. package/.github/prompts/oxe-compact.prompt.md +44 -11
  40. package/.github/prompts/oxe-dashboard.prompt.md +33 -0
  41. package/.github/prompts/oxe-debug.prompt.md +45 -12
  42. package/.github/prompts/oxe-discuss.prompt.md +33 -0
  43. package/.github/prompts/oxe-execute.prompt.md +45 -12
  44. package/.github/prompts/oxe-forensics.prompt.md +45 -12
  45. package/.github/prompts/oxe-help.prompt.md +42 -9
  46. package/.github/prompts/oxe-loop.prompt.md +45 -12
  47. package/.github/prompts/oxe-milestone.prompt.md +45 -12
  48. package/.github/prompts/oxe-next.prompt.md +42 -9
  49. package/.github/prompts/oxe-obs.prompt.md +45 -12
  50. package/.github/prompts/oxe-plan-agent.prompt.md +43 -10
  51. package/.github/prompts/oxe-plan.prompt.md +45 -12
  52. package/.github/prompts/oxe-project.prompt.md +45 -12
  53. package/.github/prompts/oxe-quick.prompt.md +45 -12
  54. package/.github/prompts/oxe-research.prompt.md +45 -12
  55. package/.github/prompts/oxe-retro.prompt.md +45 -12
  56. package/.github/prompts/oxe-review-pr.prompt.md +45 -12
  57. package/.github/prompts/oxe-route.prompt.md +45 -12
  58. package/.github/prompts/oxe-scan.prompt.md +45 -12
  59. package/.github/prompts/oxe-security.prompt.md +45 -12
  60. package/.github/prompts/oxe-session.prompt.md +33 -0
  61. package/.github/prompts/oxe-skill.prompt.md +45 -0
  62. package/.github/prompts/oxe-spec.prompt.md +45 -12
  63. package/.github/prompts/oxe-ui-review.prompt.md +45 -12
  64. package/.github/prompts/oxe-ui-spec.prompt.md +45 -12
  65. package/.github/prompts/oxe-update.prompt.md +44 -11
  66. package/.github/prompts/oxe-validate-gaps.prompt.md +45 -12
  67. package/.github/prompts/oxe-verify.prompt.md +45 -12
  68. package/.github/prompts/oxe-workstream.prompt.md +45 -12
  69. package/.github/prompts/oxe.prompt.md +45 -12
  70. package/AGENTS.md +6 -4
  71. package/CHANGELOG.md +45 -0
  72. package/README.md +38 -8
  73. package/bin/banner.txt +1 -1
  74. package/bin/lib/oxe-agent-install.cjs +69 -55
  75. package/bin/lib/oxe-context-engine.cjs +866 -0
  76. package/bin/lib/oxe-dashboard.cjs +605 -588
  77. package/bin/lib/oxe-operational.cjs +105 -0
  78. package/bin/lib/oxe-plugins.cjs +115 -0
  79. package/bin/lib/oxe-project-health.cjs +1139 -666
  80. package/bin/lib/oxe-runtime-semantics.cjs +459 -0
  81. package/bin/lib/oxe-security.cjs +64 -0
  82. package/bin/oxe-cc.js +615 -46
  83. package/commands/oxe/ask.md +33 -0
  84. package/commands/oxe/capabilities.md +33 -0
  85. package/commands/oxe/checkpoint.md +49 -16
  86. package/commands/oxe/compact.md +43 -10
  87. package/commands/oxe/dashboard.md +33 -0
  88. package/commands/oxe/debug.md +49 -16
  89. package/commands/oxe/discuss.md +33 -0
  90. package/commands/oxe/execute.md +49 -16
  91. package/commands/oxe/forensics.md +49 -16
  92. package/commands/oxe/help.md +44 -11
  93. package/commands/oxe/loop.md +50 -17
  94. package/commands/oxe/milestone.md +49 -16
  95. package/commands/oxe/next.md +45 -12
  96. package/commands/oxe/obs.md +49 -16
  97. package/commands/oxe/oxe.md +49 -16
  98. package/commands/oxe/plan-agent.md +48 -15
  99. package/commands/oxe/plan.md +48 -15
  100. package/commands/oxe/project.md +49 -16
  101. package/commands/oxe/quick.md +49 -16
  102. package/commands/oxe/research.md +49 -16
  103. package/commands/oxe/retro.md +49 -16
  104. package/commands/oxe/review-pr.md +49 -16
  105. package/commands/oxe/route.md +44 -11
  106. package/commands/oxe/scan.md +49 -16
  107. package/commands/oxe/security.md +49 -16
  108. package/commands/oxe/session.md +33 -0
  109. package/commands/oxe/skill.md +49 -0
  110. package/commands/oxe/spec.md +47 -14
  111. package/commands/oxe/ui-review.md +49 -16
  112. package/commands/oxe/ui-spec.md +49 -16
  113. package/commands/oxe/update.md +49 -16
  114. package/commands/oxe/validate-gaps.md +49 -16
  115. package/commands/oxe/verify.md +48 -15
  116. package/commands/oxe/workstream.md +49 -16
  117. package/lib/sdk/index.cjs +140 -7
  118. package/lib/sdk/index.d.ts +266 -1
  119. package/oxe/templates/HYPOTHESES.template.md +33 -0
  120. package/oxe/templates/PLAN.template.md +53 -22
  121. package/oxe/templates/SESSION.template.md +2 -0
  122. package/oxe/templates/SKILL.template.md +26 -0
  123. package/oxe/templates/WORKFLOW_AUTHORING.md +18 -2
  124. package/oxe/templates/config.template.json +16 -14
  125. package/oxe/workflows/ask.md +28 -7
  126. package/oxe/workflows/capabilities.md +2 -0
  127. package/oxe/workflows/dashboard.md +12 -2
  128. package/oxe/workflows/debug.md +9 -4
  129. package/oxe/workflows/discuss.md +12 -6
  130. package/oxe/workflows/execute.md +34 -12
  131. package/oxe/workflows/forensics.md +14 -9
  132. package/oxe/workflows/help.md +20 -9
  133. package/oxe/workflows/loop.md +13 -7
  134. package/oxe/workflows/next.md +6 -4
  135. package/oxe/workflows/plan-agent.md +3 -2
  136. package/oxe/workflows/plan.md +26 -3
  137. package/oxe/workflows/quick.md +10 -3
  138. package/oxe/workflows/references/reasoning-discovery.md +28 -0
  139. package/oxe/workflows/references/reasoning-execution.md +29 -0
  140. package/oxe/workflows/references/reasoning-planning.md +32 -0
  141. package/oxe/workflows/references/reasoning-review.md +29 -0
  142. package/oxe/workflows/references/reasoning-status.md +24 -0
  143. package/oxe/workflows/references/workflow-runtime-contracts.json +879 -0
  144. package/oxe/workflows/research.md +8 -2
  145. package/oxe/workflows/retro.md +7 -2
  146. package/oxe/workflows/review-pr.md +12 -8
  147. package/oxe/workflows/route.md +16 -13
  148. package/oxe/workflows/security.md +3 -2
  149. package/oxe/workflows/session.md +44 -0
  150. package/oxe/workflows/skill.md +44 -0
  151. package/oxe/workflows/spec.md +21 -18
  152. package/oxe/workflows/ui-review.md +13 -7
  153. package/oxe/workflows/update.md +3 -1
  154. package/oxe/workflows/validate-gaps.md +12 -6
  155. package/oxe/workflows/verify-audit.md +73 -0
  156. package/oxe/workflows/verify.md +40 -16
  157. package/package.json +4 -3
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const operational = require('./oxe-operational.cjs');
6
- const azure = require('./oxe-azure.cjs');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const operational = require('./oxe-operational.cjs');
7
+ const azure = require('./oxe-azure.cjs');
7
8
 
8
9
  /** @type {string[]} */
9
10
  const ALLOWED_CONFIG_KEYS = [
@@ -19,17 +20,18 @@ const ALLOWED_CONFIG_KEYS = [
19
20
  'scan_ignore_globs',
20
21
  'spec_required_sections',
21
22
  'plan_max_tasks_per_wave',
22
- 'profile',
23
- 'verification_depth',
24
- 'plan_confidence_threshold',
25
- 'security_in_verify',
23
+ 'profile',
24
+ 'verification_depth',
25
+ 'plan_confidence_threshold',
26
+ 'security_in_verify',
26
27
  'install',
27
28
  'plugins',
28
- 'workstreams',
29
- 'milestones',
30
- 'scale_adaptive',
31
- 'azure',
32
- ];
29
+ 'workstreams',
30
+ 'milestones',
31
+ 'scale_adaptive',
32
+ 'permissions',
33
+ 'azure',
34
+ ];
33
35
 
34
36
  /**
35
37
  * Profiles de execução OXE que controlam rigor do workflow.
@@ -113,6 +115,25 @@ function expandExecutionProfile(profile) {
113
115
  }
114
116
 
115
117
  /**
118
+ * Lê um JSON config de um caminho; retorna null se não existir ou falhar.
119
+ * @param {string} filePath
120
+ * @returns {Record<string, unknown> | null}
121
+ */
122
+ function _readJsonConfig(filePath) {
123
+ try {
124
+ if (!fs.existsSync(filePath)) return null;
125
+ const raw = fs.readFileSync(filePath, 'utf8');
126
+ const j = JSON.parse(raw);
127
+ if (!j || typeof j !== 'object' || Array.isArray(j)) return null;
128
+ return j;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Carrega e mescla config em 3 níveis: system < user < project (project tem maior prioridade).
136
+ * Retorna { config, path, parseError, sources }.
116
137
  * @param {string} targetProject
117
138
  */
118
139
  function loadOxeConfigMerged(targetProject) {
@@ -120,37 +141,82 @@ function loadOxeConfigMerged(targetProject) {
120
141
  discuss_before_plan: false,
121
142
  after_verify_suggest_pr: true,
122
143
  after_verify_draft_commit: true,
123
- after_verify_suggest_uat: false,
124
- verification_depth: 'standard',
125
- plan_confidence_threshold: 70,
126
- default_verify_command: '',
144
+ after_verify_suggest_uat: false,
145
+ verification_depth: 'standard',
146
+ plan_confidence_threshold: 70,
147
+ default_verify_command: '',
127
148
  scan_max_age_days: 0,
128
149
  compact_max_age_days: 0,
129
150
  scan_focus_globs: [],
130
151
  scan_ignore_globs: [],
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
- };
142
- const p = path.join(targetProject, '.oxe', 'config.json');
143
- if (!fs.existsSync(p)) return { config: defaults, path: null, parseError: null };
144
- try {
145
- const raw = fs.readFileSync(p, 'utf8');
146
- const j = JSON.parse(raw);
147
- if (!j || typeof j !== 'object' || Array.isArray(j)) return { config: defaults, path: p, parseError: 'não é um objeto' };
148
- // Expandir profile antes de mesclar com o config explícito (keys explícitas prevalecem)
149
- const profileExpansion = (typeof j.profile === 'string') ? expandExecutionProfile(j.profile) : {};
150
- return { config: { ...defaults, ...profileExpansion, ...j }, path: p, parseError: null };
151
- } catch (e) {
152
- return { config: defaults, path: p, parseError: e.message };
152
+ spec_required_sections: [],
153
+ plan_max_tasks_per_wave: 0,
154
+ azure: {
155
+ enabled: false,
156
+ default_resource_group: '',
157
+ preferred_locations: [],
158
+ inventory_max_age_hours: 24,
159
+ resource_graph_auto_install: true,
160
+ vpn_required: false,
161
+ },
162
+ };
163
+
164
+ const sources = { system: null, user: null, project: null };
165
+
166
+ // Nível system: OXE_SYSTEM_CONFIG env var → fallback OS-specific
167
+ const systemPath = process.env.OXE_SYSTEM_CONFIG
168
+ || (process.platform === 'win32'
169
+ ? path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'oxe', 'config.json')
170
+ : '/etc/oxe/config.json');
171
+ const systemCfg = _readJsonConfig(systemPath);
172
+ if (systemCfg) sources.system = systemPath;
173
+
174
+ // Nível user: ~/.oxe/config.json
175
+ const home = process.env.HOME || process.env.USERPROFILE || '';
176
+ const userPath = home ? path.join(home, '.oxe', 'config.json') : null;
177
+ const userCfg = userPath ? _readJsonConfig(userPath) : null;
178
+ if (userCfg) sources.user = userPath;
179
+
180
+ // Nível project: .oxe/config.json (comportamento existente)
181
+ const projectPath = path.join(targetProject, '.oxe', 'config.json');
182
+ let projectCfg = null;
183
+ let projectParseError = null;
184
+ if (fs.existsSync(projectPath)) {
185
+ try {
186
+ const raw = fs.readFileSync(projectPath, 'utf8');
187
+ const j = JSON.parse(raw);
188
+ if (!j || typeof j !== 'object' || Array.isArray(j)) {
189
+ projectParseError = 'não é um objeto';
190
+ } else {
191
+ projectCfg = j;
192
+ }
193
+ } catch (e) {
194
+ projectParseError = String(e.message || e);
195
+ }
153
196
  }
197
+ if (projectCfg) sources.project = projectPath;
198
+
199
+ // Merge: system < user < project (project vence)
200
+ const merged = { ...defaults };
201
+ for (const layer of [systemCfg, userCfg, projectCfg]) {
202
+ if (!layer) continue;
203
+ // Expandir profile se presente nesta camada
204
+ if (typeof layer.profile === 'string') {
205
+ Object.assign(merged, expandExecutionProfile(layer.profile));
206
+ }
207
+ // Azure: merge aninhado para não sobrescrever campos não especificados
208
+ if (layer.azure && typeof layer.azure === 'object' && !Array.isArray(layer.azure)) {
209
+ merged.azure = { .../** @type {any} */ (merged.azure), ...layer.azure };
210
+ const layerWithoutAzure = { ...layer };
211
+ delete layerWithoutAzure.azure;
212
+ Object.assign(merged, layerWithoutAzure);
213
+ } else {
214
+ Object.assign(merged, layer);
215
+ }
216
+ }
217
+
218
+ const primaryPath = sources.project || sources.user || sources.system || null;
219
+ return { config: merged, path: primaryPath, parseError: projectParseError, sources };
154
220
  }
155
221
 
156
222
  /**
@@ -224,62 +290,101 @@ function validateConfigShape(cfg) {
224
290
  typeErrors.push(`profile deve ser um de: ${EXECUTION_PROFILES.join(', ')}`);
225
291
  }
226
292
  }
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
- }
293
+ if (cfg.verification_depth != null) {
294
+ if (typeof cfg.verification_depth !== 'string') {
295
+ typeErrors.push('verification_depth deve ser string');
296
+ } else if (!VERIFICATION_DEPTHS.includes(cfg.verification_depth)) {
297
+ typeErrors.push(`verification_depth deve ser um de: ${VERIFICATION_DEPTHS.join(', ')}`);
298
+ }
299
+ }
300
+ if (cfg.plan_confidence_threshold != null && typeof cfg.plan_confidence_threshold !== 'number') {
301
+ typeErrors.push('plan_confidence_threshold deve ser número (percentual de 0 a 100)');
302
+ }
237
303
  if (cfg.after_verify_suggest_uat != null && typeof cfg.after_verify_suggest_uat !== 'boolean') {
238
304
  typeErrors.push('after_verify_suggest_uat deve ser boolean');
239
305
  }
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
- }
306
+ if (cfg.scale_adaptive != null && typeof cfg.scale_adaptive !== 'boolean') {
307
+ typeErrors.push('scale_adaptive deve ser boolean');
308
+ }
309
+ if (cfg.azure != null) {
310
+ if (typeof cfg.azure !== 'object' || Array.isArray(cfg.azure)) {
311
+ typeErrors.push('azure deve ser um objeto');
312
+ } else {
313
+ const azureCfg = /** @type {Record<string, unknown>} */ (cfg.azure);
314
+ const allowedAzureKeys = [
315
+ 'enabled',
316
+ 'default_resource_group',
317
+ 'preferred_locations',
318
+ 'inventory_max_age_hours',
319
+ 'resource_graph_auto_install',
320
+ 'vpn_required',
321
+ ];
322
+ for (const key of Object.keys(azureCfg)) {
323
+ if (!allowedAzureKeys.includes(key)) {
324
+ typeErrors.push(`azure: chave desconhecida "${key}"`);
325
+ }
326
+ }
327
+ if (azureCfg.enabled != null && typeof azureCfg.enabled !== 'boolean') {
328
+ typeErrors.push('azure.enabled deve ser boolean');
329
+ }
330
+ if (azureCfg.default_resource_group != null && typeof azureCfg.default_resource_group !== 'string') {
331
+ typeErrors.push('azure.default_resource_group deve ser string');
332
+ }
333
+ if (azureCfg.preferred_locations != null && !Array.isArray(azureCfg.preferred_locations)) {
334
+ typeErrors.push('azure.preferred_locations deve ser array de strings');
335
+ }
336
+ if (azureCfg.inventory_max_age_hours != null && typeof azureCfg.inventory_max_age_hours !== 'number') {
337
+ typeErrors.push('azure.inventory_max_age_hours deve ser número');
338
+ }
339
+ if (azureCfg.resource_graph_auto_install != null && typeof azureCfg.resource_graph_auto_install !== 'boolean') {
340
+ typeErrors.push('azure.resource_graph_auto_install deve ser boolean');
341
+ }
342
+ if (azureCfg.vpn_required != null && typeof azureCfg.vpn_required !== 'boolean') {
343
+ typeErrors.push('azure.vpn_required deve ser boolean');
344
+ }
345
+ }
346
+ }
347
+ if (cfg.permissions != null) {
348
+ if (!Array.isArray(cfg.permissions)) {
349
+ typeErrors.push('permissions deve ser array de regras { pattern, action, scope? }');
350
+ } else {
351
+ const VALID_PERM_ACTIONS = ['allow', 'deny', 'ask'];
352
+ const VALID_PERM_SCOPES = ['execute', 'apply', 'all'];
353
+ for (let i = 0; i < cfg.permissions.length; i++) {
354
+ const r = cfg.permissions[i];
355
+ if (!r || typeof r !== 'object' || Array.isArray(r)) {
356
+ typeErrors.push(`permissions[${i}] deve ser objeto`);
357
+ continue;
358
+ }
359
+ if (typeof r.pattern !== 'string' || r.pattern.length === 0) {
360
+ typeErrors.push(`permissions[${i}].pattern deve ser string não-vazia`);
361
+ }
362
+ if (!VALID_PERM_ACTIONS.includes(r.action)) {
363
+ typeErrors.push(`permissions[${i}].action deve ser um de: ${VALID_PERM_ACTIONS.join(', ')}`);
364
+ }
365
+ if (r.scope != null && !VALID_PERM_SCOPES.includes(r.scope)) {
366
+ typeErrors.push(`permissions[${i}].scope deve ser um de: ${VALID_PERM_SCOPES.join(', ')}`);
367
+ }
368
+ }
369
+ }
370
+ }
371
+ if (cfg.plugins != null) {
372
+ if (!Array.isArray(cfg.plugins)) {
373
+ typeErrors.push('plugins deve ser array');
374
+ } else {
375
+ for (let i = 0; i < cfg.plugins.length; i++) {
376
+ const p = cfg.plugins[i];
377
+ if (typeof p === 'string') continue; // aceitar strings legado
378
+ if (!p || typeof p !== 'object' || Array.isArray(p)) {
379
+ typeErrors.push(`plugins[${i}] deve ser string ou objeto { source: string }`);
380
+ } else if (typeof p.source !== 'string' || p.source.length === 0) {
381
+ typeErrors.push(`plugins[${i}].source deve ser string não-vazia`);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ return { unknownKeys, typeErrors };
387
+ }
283
388
 
284
389
  /**
285
390
  * @param {string} stateText
@@ -340,38 +445,38 @@ function parseLastCompactDate(stateText) {
340
445
  * @param {string} stateText
341
446
  * @returns {Date | null}
342
447
  */
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
- }
448
+ function parseLastRetroDate(stateText) {
449
+ const m = stateText.match(/\blast_retro\s*:\s*(\d{4}-\d{2}-\d{2})/i);
450
+ if (!m) return null;
451
+ const iso = Date.parse(m[1]);
452
+ return Number.isNaN(iso) ? null : new Date(iso);
453
+ }
454
+
455
+ /**
456
+ * @param {string} stateText
457
+ * @returns {string | null}
458
+ */
459
+ function parseActiveSession(stateText) {
460
+ if (!stateText) return null;
461
+ const m = stateText.match(/\*\*active_session:\*\*\s*`?([^\n`]+?)`?\s*(?:\n|$)/i);
462
+ if (!m) return null;
463
+ const raw = m[1].trim();
464
+ if (!raw || raw === '—' || /^none$/i.test(raw)) return null;
465
+ return raw.replace(/\\/g, '/');
466
+ }
467
+
468
+ /**
469
+ * @param {string} stateText
470
+ * @returns {string | null}
471
+ */
472
+ function parsePlanReviewStatus(stateText) {
473
+ if (!stateText) return null;
474
+ const m = stateText.match(/\*\*plan_review_status:\*\*\s*`?([^\n`]+?)`?\s*(?:\n|$)/i);
475
+ if (!m) return null;
476
+ const raw = m[1].trim();
477
+ if (!raw || raw === '—' || /^none$/i.test(raw)) return null;
478
+ return raw;
479
+ }
375
480
 
376
481
  /**
377
482
  * @param {Date | null} scanDate
@@ -396,31 +501,38 @@ function isStaleLessons(retroDate, maxAgeDays) {
396
501
  /**
397
502
  * @param {string} target
398
503
  */
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'),
504
+ function oxePaths(target) {
505
+ const oxe = path.join(target, '.oxe');
506
+ return {
507
+ oxe,
508
+ installDir: path.join(oxe, 'install'),
509
+ contextDir: path.join(oxe, 'context'),
510
+ contextIndex: path.join(oxe, 'context', 'index.json'),
511
+ contextPacksDir: path.join(oxe, 'context', 'packs'),
512
+ contextSummariesDir: path.join(oxe, 'context', 'summaries'),
513
+ state: path.join(oxe, 'STATE.md'),
514
+ runtime: path.join(oxe, 'EXECUTION-RUNTIME.md'),
515
+ checkpoints: path.join(oxe, 'CHECKPOINTS.md'),
516
+ capabilitiesIndex: path.join(oxe, 'CAPABILITIES.md'),
517
+ capabilitiesDir: path.join(oxe, 'capabilities'),
518
+ investigationsIndex: path.join(oxe, 'INVESTIGATIONS.md'),
519
+ investigationsDir: path.join(oxe, 'investigations'),
520
+ dashboardDir: path.join(oxe, 'dashboard'),
521
+ sessionsIndex: path.join(oxe, 'SESSIONS.md'),
522
+ globalDir: path.join(oxe, 'global'),
523
+ globalLessons: path.join(oxe, 'global', 'LESSONS.md'),
524
+ globalMilestones: path.join(oxe, 'global', 'MILESTONES.md'),
525
+ globalMilestonesDir: path.join(oxe, 'global', 'milestones'),
526
+ sessionsDir: path.join(oxe, 'sessions'),
527
+ planReview: path.join(oxe, 'PLAN-REVIEW.md'),
528
+ planReviewComments: path.join(oxe, 'plan-review-comments.json'),
529
+ activeRun: path.join(oxe, 'ACTIVE-RUN.json'),
530
+ runsDir: path.join(oxe, 'runs'),
531
+ events: path.join(oxe, 'OXE-EVENTS.ndjson'),
532
+ copilotManifest: path.join(oxe, 'install', 'copilot-vscode.json'),
533
+ runtimeSemanticsManifest: path.join(oxe, 'install', 'runtime-semantics.json'),
534
+ spec: path.join(oxe, 'SPEC.md'),
535
+ plan: path.join(oxe, 'PLAN.md'),
424
536
  quick: path.join(oxe, 'QUICK.md'),
425
537
  verify: path.join(oxe, 'VERIFY.md'),
426
538
  discuss: path.join(oxe, 'DISCUSS.md'),
@@ -428,41 +540,247 @@ function oxePaths(target) {
428
540
  codebase: path.join(oxe, 'codebase'),
429
541
  lessons: path.join(oxe, 'LESSONS.md'),
430
542
  planAgents: path.join(oxe, 'plan-agents.json'),
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
- }
543
+ };
544
+ }
545
+
546
+ /**
547
+ * @param {string} target
548
+ * @param {string | null} activeSession
549
+ */
550
+ function scopedOxePaths(target, activeSession) {
551
+ const base = oxePaths(target);
552
+ if (!activeSession) return { ...base, activeSession: null, scopedRoot: base.oxe };
553
+ const sessionRoot = path.join(base.oxe, ...activeSession.split('/'));
554
+ return {
555
+ ...base,
556
+ activeSession,
557
+ scopedRoot: sessionRoot,
558
+ sessionRoot,
559
+ sessionManifest: path.join(sessionRoot, 'SESSION.md'),
560
+ planReview: path.join(sessionRoot, 'plan', 'PLAN-REVIEW.md'),
561
+ planReviewComments: path.join(sessionRoot, 'plan', 'plan-review-comments.json'),
562
+ runtime: path.join(sessionRoot, 'execution', 'EXECUTION-RUNTIME.md'),
563
+ checkpoints: path.join(sessionRoot, 'execution', 'CHECKPOINTS.md'),
564
+ activeRun: path.join(sessionRoot, 'execution', 'ACTIVE-RUN.json'),
565
+ runsDir: path.join(sessionRoot, 'execution', 'runs'),
566
+ events: path.join(sessionRoot, 'execution', 'OXE-EVENTS.ndjson'),
567
+ investigationsIndex: path.join(sessionRoot, 'research', 'INVESTIGATIONS.md'),
568
+ investigationsDir: path.join(sessionRoot, 'research', 'investigations'),
569
+ spec: path.join(sessionRoot, 'spec', 'SPEC.md'),
570
+ discuss: path.join(sessionRoot, 'spec', 'DISCUSS.md'),
571
+ plan: path.join(sessionRoot, 'plan', 'PLAN.md'),
572
+ quick: path.join(sessionRoot, 'plan', 'QUICK.md'),
573
+ verify: path.join(sessionRoot, 'verification', 'VERIFY.md'),
574
+ summary: path.join(sessionRoot, 'verification', 'SUMMARY.md'),
575
+ executionState: path.join(sessionRoot, 'execution', 'STATE.md'),
576
+ };
577
+ }
578
+
579
+ /**
580
+ * @param {string} target
581
+ */
582
+ function copilotWorkspacePaths(target) {
583
+ return {
584
+ root: path.join(target, '.github'),
585
+ promptsDir: path.join(target, '.github', 'prompts'),
586
+ instructions: path.join(target, '.github', 'copilot-instructions.md'),
587
+ manifest: path.join(target, '.oxe', 'install', 'copilot-vscode.json'),
588
+ };
589
+ }
590
+
591
+ function copilotLegacyHome() {
592
+ if (process.env.COPILOT_CONFIG_DIR) return path.resolve(process.env.COPILOT_CONFIG_DIR);
593
+ if (process.env.COPILOT_HOME) return path.resolve(process.env.COPILOT_HOME);
594
+ return path.join(os.homedir(), '.copilot');
595
+ }
596
+
597
+ function copilotLegacyPaths() {
598
+ const root = copilotLegacyHome();
599
+ return {
600
+ root,
601
+ promptsDir: path.join(root, 'prompts'),
602
+ instructions: path.join(root, 'copilot-instructions.md'),
603
+ };
604
+ }
605
+
606
+ /**
607
+ * @param {string} filePath
608
+ */
609
+ function readJsonFileSafe(filePath) {
610
+ if (!fs.existsSync(filePath)) return { ok: false, data: null, error: null };
611
+ try {
612
+ return { ok: true, data: JSON.parse(fs.readFileSync(filePath, 'utf8')), error: null };
613
+ } catch (error) {
614
+ return { ok: false, data: null, error: error instanceof Error ? error.message : String(error) };
615
+ }
616
+ }
617
+
618
+ /**
619
+ * @param {string} dir
620
+ * @returns {string[]}
621
+ */
622
+ function listOxePromptFiles(dir) {
623
+ if (!fs.existsSync(dir)) return [];
624
+ return fs
625
+ .readdirSync(dir, { withFileTypes: true })
626
+ .filter((entry) => entry.isFile() && /^oxe-.*\.prompt\.md$/i.test(entry.name))
627
+ .map((entry) => path.join(dir, entry.name))
628
+ .sort();
629
+ }
630
+
631
+ /**
632
+ * @param {string} filePath
633
+ * @returns {boolean}
634
+ */
635
+ function hasOxeInstructionBlock(filePath) {
636
+ if (!fs.existsSync(filePath)) return false;
637
+ const text = fs.readFileSync(filePath, 'utf8');
638
+ return text.includes('<!-- oxe-cc:install-begin -->') && text.includes('<!-- oxe-cc:install-end -->');
639
+ }
640
+
641
+ /**
642
+ * @param {string} filePath
643
+ * @returns {boolean}
644
+ */
645
+ function hasOtherManagedInstructionBlocks(filePath) {
646
+ if (!fs.existsSync(filePath)) return false;
647
+ const text = fs.readFileSync(filePath, 'utf8');
648
+ const withoutOxe = text
649
+ .replace(/<!-- oxe-cc:install-begin -->[\s\S]*?<!-- oxe-cc:install-end -->/g, '')
650
+ .trim();
651
+ if (!withoutOxe) return false;
652
+ return /<!--[^>]*(managed|configuration|install-begin|install-end|get-shit-done|gsd)[^>]*-->/i.test(withoutOxe);
653
+ }
654
+
655
+ /**
656
+ * @param {string} filePath
657
+ * @param {string} target
658
+ * @returns {string[]}
659
+ */
660
+ function promptWorkflowPathWarnings(filePath, target) {
661
+ if (!fs.existsSync(filePath)) return [];
662
+ const text = fs.readFileSync(filePath, 'utf8');
663
+ const hasClassicRef = /(^|[`"'(\s])oxe\/workflows\//i.test(text);
664
+ const hasNestedRef = /(^|[`"'(\s])\.oxe\/workflows\//i.test(text);
665
+ const classicExists = fs.existsSync(path.join(target, 'oxe', 'workflows'));
666
+ const nestedExists = fs.existsSync(path.join(target, '.oxe', 'workflows'));
667
+ /** @type {string[]} */
668
+ const warnings = [];
669
+ if (nestedExists && !classicExists && hasClassicRef && !hasNestedRef) {
670
+ warnings.push(`${path.basename(filePath)} aponta para oxe/workflows/, mas este projeto usa .oxe/workflows/`);
671
+ }
672
+ if (classicExists && !nestedExists && hasNestedRef && !hasClassicRef) {
673
+ warnings.push(`${path.basename(filePath)} aponta para .oxe/workflows/, mas este projeto usa oxe/workflows/`);
674
+ }
675
+ return warnings;
676
+ }
677
+
678
+ /**
679
+ * @param {string} target
680
+ */
681
+ function copilotIntegrationReport(target) {
682
+ const workspace = copilotWorkspacePaths(target);
683
+ const legacy = copilotLegacyPaths();
684
+ const workspacePromptFiles = listOxePromptFiles(workspace.promptsDir);
685
+ const legacyPromptFiles = listOxePromptFiles(legacy.promptsDir);
686
+ const workspaceHasInstructions = fs.existsSync(workspace.instructions);
687
+ const workspaceHasOxeBlock = hasOxeInstructionBlock(workspace.instructions);
688
+ const legacyHasInstructions = fs.existsSync(legacy.instructions);
689
+ const legacyHasOxeBlock = hasOxeInstructionBlock(legacy.instructions);
690
+ const legacyHasOtherManagedBlocks = hasOtherManagedInstructionBlocks(legacy.instructions);
691
+ const manifestRaw = readJsonFileSafe(workspace.manifest);
692
+ const manifest = manifestRaw.ok ? manifestRaw.data : null;
693
+ const promptPathWarnings = [];
694
+ for (const filePath of workspacePromptFiles) {
695
+ for (const warning of promptWorkflowPathWarnings(filePath, target)) promptPathWarnings.push(warning);
696
+ }
697
+ for (const warning of promptWorkflowPathWarnings(workspace.instructions, target)) promptPathWarnings.push(warning);
698
+
699
+ /** @type {string[]} */
700
+ const warnings = [];
701
+ const workspaceDetected =
702
+ workspacePromptFiles.length > 0 || workspaceHasInstructions || fs.existsSync(workspace.manifest);
703
+ const legacyDetected = legacyPromptFiles.length > 0 || legacyHasOxeBlock;
704
+
705
+ if (workspacePromptFiles.length > 0 && !workspaceHasInstructions) {
706
+ warnings.push('Prompts OXE do Copilot VS Code existem no workspace, mas .github/copilot-instructions.md está ausente');
707
+ } else if (workspaceHasInstructions && !workspaceHasOxeBlock) {
708
+ warnings.push('.github/copilot-instructions.md existe, mas não contém o bloco OXE');
709
+ }
710
+ if (workspaceHasInstructions && workspacePromptFiles.length === 0 && !legacyDetected) {
711
+ warnings.push('.github/copilot-instructions.md existe, mas .github/prompts/ não contém prompt files OXE');
712
+ }
713
+ if (!workspaceDetected && legacyDetected) {
714
+ warnings.push('Prompts OXE do Copilot VS Code foram encontrados apenas no legado global ~/.copilot/; sincronize .github/ no workspace');
715
+ }
716
+ if (legacyPromptFiles.length > 0) {
717
+ warnings.push('Instalação legado do Copilot VS Code detectada em ~/.copilot/prompts/; trate como resíduo ou execute uninstall --copilot-legacy-clean');
718
+ }
719
+ if (legacyHasOxeBlock) {
720
+ warnings.push('Bloco OXE legado detectado em ~/.copilot/copilot-instructions.md');
721
+ }
722
+ if (legacyHasOtherManagedBlocks) {
723
+ warnings.push('copilot-instructions global contém blocos geridos por outro framework; isso pode contaminar respostas do Copilot');
724
+ }
725
+ if (!manifestRaw.ok && fs.existsSync(workspace.manifest)) {
726
+ warnings.push(`Manifesto Copilot VS Code inválido: ${manifestRaw.error}`);
727
+ } else if (!fs.existsSync(workspace.manifest) && workspacePromptFiles.length > 0) {
728
+ warnings.push('Manifesto .oxe/install/copilot-vscode.json ausente para a integração Copilot VS Code');
729
+ } else if (manifest && Array.isArray(manifest.prompt_files)) {
730
+ const actualPromptNames = workspacePromptFiles.map((filePath) => path.basename(filePath)).sort();
731
+ const expectedPromptNames = manifest.prompt_files.map((value) => String(value)).sort();
732
+ for (const name of expectedPromptNames) {
733
+ if (!actualPromptNames.includes(name)) {
734
+ warnings.push(`Manifesto Copilot VS Code referencia ${name}, mas o arquivo não existe em .github/prompts/`);
735
+ }
736
+ }
737
+ }
738
+ for (const warning of promptPathWarnings) warnings.push(warning);
739
+
740
+ let status = 'not_installed';
741
+ if (
742
+ workspacePromptFiles.length > 0 &&
743
+ workspaceHasInstructions &&
744
+ workspaceHasOxeBlock &&
745
+ promptPathWarnings.length === 0
746
+ ) {
747
+ status = warnings.length ? 'warning' : 'healthy';
748
+ } else if (workspaceDetected || legacyDetected) {
749
+ status =
750
+ promptPathWarnings.length > 0 || (workspacePromptFiles.length > 0 && (!workspaceHasInstructions || !workspaceHasOxeBlock))
751
+ ? 'broken'
752
+ : 'warning';
753
+ }
754
+
755
+ return {
756
+ status,
757
+ detected: workspaceDetected || legacyDetected,
758
+ target: 'workspace',
759
+ promptSource:
760
+ workspacePromptFiles.length > 0 ? 'workspace' : legacyDetected ? 'legacy_global' : 'missing',
761
+ workspace: {
762
+ root: workspace.root,
763
+ promptsDir: workspace.promptsDir,
764
+ instructions: workspace.instructions,
765
+ manifest: workspace.manifest,
766
+ promptFiles: workspacePromptFiles,
767
+ hasInstructions: workspaceHasInstructions,
768
+ hasOxeBlock: workspaceHasOxeBlock,
769
+ },
770
+ legacy: {
771
+ root: legacy.root,
772
+ promptsDir: legacy.promptsDir,
773
+ instructions: legacy.instructions,
774
+ promptFiles: legacyPromptFiles,
775
+ hasInstructions: legacyHasInstructions,
776
+ hasOxeBlock: legacyHasOxeBlock,
777
+ hasOtherManagedBlocks: legacyHasOtherManagedBlocks,
778
+ detected: legacyDetected,
779
+ },
780
+ manifest,
781
+ warnings,
782
+ };
783
+ }
466
784
 
467
785
  /**
468
786
  * Valida o arquivo plan-agents.json (se existir) e retorna avisos.
@@ -590,7 +908,7 @@ function planTaskAceiteWarnings(planPath) {
590
908
  return out;
591
909
  }
592
910
 
593
- function planWaveWarningsFixed(planPath, maxPerWave) {
911
+ function planWaveWarningsFixed(planPath, maxPerWave) {
594
912
  if (!maxPerWave || maxPerWave <= 0 || !fs.existsSync(planPath)) return [];
595
913
  const raw = fs.readFileSync(planPath, 'utf8');
596
914
  const lines = raw.split('\n');
@@ -611,241 +929,241 @@ function planWaveWarningsFixed(planPath, maxPerWave) {
611
929
  w.push(`PLAN.md: onda ${wN} tem ${count} tarefas (máximo configurado: ${maxPerWave} — considere dividir ondas)`);
612
930
  }
613
931
  }
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
- }
932
+ return w;
933
+ }
934
+
935
+ /**
936
+ * @param {string} planPath
937
+ * @returns {{
938
+ * hasSection: boolean,
939
+ * bestPlan: string | null,
940
+ * confidence: number | null,
941
+ * warnings: string[],
942
+ * }}
943
+ */
944
+ function parsePlanSelfEvaluation(planPath) {
945
+ const empty = { hasSection: false, bestPlan: null, confidence: null, warnings: [] };
946
+ if (!fs.existsSync(planPath)) return empty;
947
+ const raw = fs.readFileSync(planPath, 'utf8');
948
+ const m = raw.match(/##\s*Autoavaliação do Plano\s*([\s\S]*?)(?=\n## |\n#[^\#]|$)/i);
949
+ if (!m) {
950
+ return {
951
+ ...empty,
952
+ warnings: ['PLAN.md sem a seção obrigatória "## Autoavaliação do Plano"'],
953
+ };
954
+ }
955
+ const body = m[1];
956
+ const best = body.match(/\*\*Melhor plano atual:\*\*\s*(sim|não|nao)/i);
957
+ const confidence = body.match(/\*\*Confiança:\*\*\s*(\d{1,3})\s*%/i);
958
+ /** @type {string[]} */
959
+ const warnings = [];
960
+ const rubricLabels = [
961
+ 'Completude dos requisitos',
962
+ 'Dependências conhecidas',
963
+ 'Risco técnico',
964
+ 'Impacto no código existente',
965
+ 'Clareza da validação / testes',
966
+ 'Lacunas externas / decisões pendentes',
967
+ ];
968
+ if (!best) warnings.push('PLAN.md: autoavaliação sem "Melhor plano atual: sim|não"');
969
+ if (!confidence) warnings.push('PLAN.md: autoavaliação sem "Confiança: NN%"');
970
+ if (!/\*\*Principais incertezas:\*\*/i.test(body)) warnings.push('PLAN.md: autoavaliação sem "Principais incertezas"');
971
+ if (!/\*\*Alternativas descartadas:\*\*/i.test(body)) warnings.push('PLAN.md: autoavaliação sem "Alternativas descartadas"');
972
+ if (!/\*\*Condição para replanejar:\*\*/i.test(body)) warnings.push('PLAN.md: autoavaliação sem "Condição para replanejar"');
973
+ for (const label of rubricLabels) {
974
+ if (!body.includes(label)) warnings.push(`PLAN.md: rubrica sem "${label}"`);
975
+ }
976
+ const parsedConfidence = confidence ? Number(confidence[1]) : null;
977
+ if (parsedConfidence != null && (parsedConfidence < 0 || parsedConfidence > 100)) {
978
+ warnings.push('PLAN.md: confiança fora do intervalo 0–100%');
979
+ }
980
+ return {
981
+ hasSection: true,
982
+ bestPlan: best ? best[1].toLowerCase().replace('nao', 'não') : null,
983
+ confidence: parsedConfidence,
984
+ warnings,
985
+ };
986
+ }
987
+
988
+ /**
989
+ * @param {string} planPath
990
+ * @param {number} threshold
991
+ * @returns {string[]}
992
+ */
993
+ function planSelfEvaluationWarnings(planPath, threshold) {
994
+ const info = parsePlanSelfEvaluation(planPath);
995
+ const warns = [...info.warnings];
996
+ if (!fs.existsSync(planPath)) return warns;
997
+ if (info.bestPlan === 'não') warns.push('PLAN.md: autoavaliação declara que este não é o melhor plano atual');
998
+ if (info.confidence != null && info.confidence < threshold) {
999
+ warns.push(`PLAN.md: confiança ${info.confidence}% abaixo do limiar executável (${threshold}%)`);
1000
+ }
1001
+ return warns;
1002
+ }
1003
+
1004
+ /**
1005
+ * @param {string} target
1006
+ * @param {string | null} activeSession
1007
+ * @returns {string[]}
1008
+ */
1009
+ function sessionWarnings(target, activeSession) {
1010
+ if (!activeSession) return [];
1011
+ const base = oxePaths(target);
1012
+ const scoped = scopedOxePaths(target, activeSession);
1013
+ /** @type {string[]} */
1014
+ const warns = [];
1015
+ if (!/^sessions\/s\d{3}-/.test(activeSession)) {
1016
+ warns.push(`active_session "${activeSession}" não segue o formato sessions/sNNN-slug`);
1017
+ }
1018
+ if (!fs.existsSync(scoped.sessionRoot)) {
1019
+ warns.push(`active_session aponta para ${activeSession}, mas a pasta da sessão não existe em .oxe/`);
1020
+ return warns;
1021
+ }
1022
+ if (!fs.existsSync(scoped.sessionManifest)) warns.push(`Sessão ativa ${activeSession} sem SESSION.md`);
1023
+ if (!fs.existsSync(base.sessionsIndex)) warns.push('Sessão ativa definida, mas .oxe/SESSIONS.md não existe');
1024
+ return warns;
1025
+ }
1026
+
1027
+ /**
1028
+ * @param {string} target
1029
+ * @returns {string[]}
1030
+ */
1031
+ function installationCompletenessWarnings(target) {
1032
+ const p = oxePaths(target);
1033
+ /** @type {string[]} */
1034
+ const warns = [];
1035
+ if (!fs.existsSync(p.oxe)) return warns;
1036
+ if (!fs.existsSync(p.globalDir)) warns.push('.oxe/global/ ausente');
1037
+ if (!fs.existsSync(p.globalLessons)) warns.push('.oxe/global/LESSONS.md ausente');
1038
+ if (!fs.existsSync(p.globalMilestones)) warns.push('.oxe/global/MILESTONES.md ausente');
1039
+ if (!fs.existsSync(p.globalMilestonesDir)) warns.push('.oxe/global/milestones/ ausente');
1040
+ if (!fs.existsSync(p.sessionsDir)) warns.push('.oxe/sessions/ ausente');
1041
+ if (!fs.existsSync(p.capabilitiesDir)) warns.push('.oxe/capabilities/ ausente');
1042
+ if (!fs.existsSync(p.capabilitiesIndex)) warns.push('.oxe/CAPABILITIES.md ausente');
1043
+ if (!fs.existsSync(p.investigationsDir)) warns.push('.oxe/investigations/ ausente');
1044
+ if (!fs.existsSync(p.investigationsIndex)) warns.push('.oxe/INVESTIGATIONS.md ausente');
1045
+ if (!fs.existsSync(p.runtime)) warns.push('.oxe/EXECUTION-RUNTIME.md ausente');
1046
+ if (!fs.existsSync(p.checkpoints)) warns.push('.oxe/CHECKPOINTS.md ausente');
1047
+ if (!fs.existsSync(p.activeRun)) warns.push('.oxe/ACTIVE-RUN.json ausente');
1048
+ if (!fs.existsSync(p.runsDir)) warns.push('.oxe/runs/ ausente');
1049
+ if (!fs.existsSync(p.events)) warns.push('.oxe/OXE-EVENTS.ndjson ausente');
1050
+ if (azure.isAzureContextEnabled(target)) {
1051
+ const azurePaths = azure.azurePaths(target);
1052
+ if (!fs.existsSync(azurePaths.root)) warns.push('.oxe/cloud/azure/ ausente');
1053
+ if (!fs.existsSync(azurePaths.operationsDir)) warns.push('.oxe/cloud/azure/operations/ ausente');
1054
+ if (!fs.existsSync(azurePaths.profile)) warns.push('.oxe/cloud/azure/profile.json ausente');
1055
+ if (!fs.existsSync(azurePaths.authStatus)) warns.push('.oxe/cloud/azure/auth-status.json ausente');
1056
+ }
1057
+ return warns;
1058
+ }
1059
+
1060
+ /**
1061
+ * @param {string} stateText
1062
+ * @param {ReturnType<typeof scopedOxePaths>} p
1063
+ * @returns {string[]}
1064
+ */
1065
+ function runtimeWarnings(stateText, p) {
1066
+ /** @type {string[]} */
1067
+ const warns = [];
1068
+ const checkpointPending = /\*\*checkpoint_status:\*\*\s*`?pending_approval`?/i.test(stateText);
1069
+ const runtimeBlocked = /\*\*runtime_status:\*\*\s*`?(blocked|waiting_approval|failed)`?/i.test(stateText);
1070
+ if (checkpointPending && !fs.existsSync(p.checkpoints)) {
1071
+ warns.push('STATE.md indica checkpoint pendente, mas o índice de checkpoints não existe');
1072
+ }
1073
+ if (runtimeBlocked && !fs.existsSync(p.runtime)) {
1074
+ warns.push('STATE.md indica runtime bloqueado, mas EXECUTION-RUNTIME.md não existe');
1075
+ }
1076
+ if (fs.existsSync(p.runtime)) {
1077
+ const raw = fs.readFileSync(p.runtime, 'utf8');
1078
+ if (!/##\s*Checkpoints/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem seção "Checkpoints"');
1079
+ if (!/##\s*Agentes ativos/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem seção "Agentes ativos"');
1080
+ if (!/Run ID/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem referência explícita de Run ID');
1081
+ if (!/Tracing operacional/i.test(raw)) warns.push('EXECUTION-RUNTIME.md sem seção "Tracing operacional"');
1082
+ }
1083
+ if (!fs.existsSync(p.activeRun)) {
1084
+ warns.push('ACTIVE-RUN.json não existe para o escopo atual');
1085
+ }
1086
+ if (!fs.existsSync(p.events)) {
1087
+ warns.push('OXE-EVENTS.ndjson não existe para o escopo atual');
1088
+ }
1089
+ const runState = operational.readRunState(path.dirname(p.oxe), p.activeSession || null);
1090
+ const checkpointRows = [];
1091
+ if (fs.existsSync(p.checkpoints)) {
1092
+ const checkpointText = fs.readFileSync(p.checkpoints, 'utf8');
1093
+ for (const line of checkpointText.split('\n')) {
1094
+ const match = line.match(/^\|\s*(CP-[^|]+)\|\s*[^|]*\|\s*[^|]*\|\s*[^|]*\|\s*([^|]+)\|/i);
1095
+ if (!match) continue;
1096
+ checkpointRows.push({ id: match[1].trim(), status: match[2].trim() });
1097
+ }
1098
+ }
1099
+ for (const warn of operational.runtimeStateWarnings(runState, checkpointRows)) warns.push(warn);
1100
+ return warns;
1101
+ }
1102
+
1103
+ /**
1104
+ * @param {ReturnType<typeof scopedOxePaths>} p
1105
+ * @returns {string[]}
1106
+ */
1107
+ function capabilityWarnings(p) {
1108
+ /** @type {string[]} */
1109
+ const warns = [];
1110
+ if (fs.existsSync(p.capabilitiesDir) && !fs.existsSync(p.capabilitiesIndex)) {
1111
+ warns.push('Existem capabilities em .oxe/capabilities/, mas .oxe/CAPABILITIES.md não existe');
1112
+ }
1113
+ for (const warn of operational.capabilityCatalogWarnings(path.dirname(p.oxe))) warns.push(warn);
1114
+ return warns;
1115
+ }
1116
+
1117
+ /**
1118
+ * @param {ReturnType<typeof scopedOxePaths>} p
1119
+ * @returns {string[]}
1120
+ */
1121
+ function investigationWarnings(p) {
1122
+ /** @type {string[]} */
1123
+ const warns = [];
1124
+ if (fs.existsSync(p.investigationsDir) && !fs.existsSync(p.investigationsIndex)) {
1125
+ warns.push('Existe pasta de investigações, mas falta o índice INVESTIGATIONS.md');
1126
+ }
1127
+ return warns;
1128
+ }
1129
+
1130
+ /**
1131
+ * @param {string} stateText
1132
+ * @param {ReturnType<typeof scopedOxePaths>} p
1133
+ * @returns {string[]}
1134
+ */
1135
+ function planReviewWarnings(stateText, p) {
1136
+ /** @type {string[]} */
1137
+ const warns = [];
1138
+ const reviewStatus = parsePlanReviewStatus(stateText);
1139
+ if (fs.existsSync(p.plan) && !reviewStatus) {
1140
+ warns.push('PLAN.md existe, mas STATE.md não declara plan_review_status');
1141
+ }
1142
+ if (reviewStatus && !fs.existsSync(p.planReview)) {
1143
+ warns.push('STATE.md declara revisão do plano, mas PLAN-REVIEW.md não existe');
1144
+ }
1145
+ if (reviewStatus === 'needs_revision' || reviewStatus === 'rejected') {
1146
+ warns.push(`Plano em estado ${reviewStatus} — revisão adicional necessária antes de executar`);
1147
+ }
1148
+ return warns;
1149
+ }
832
1150
 
833
1151
  /**
834
1152
  * Próximo passo único (espelha o workflow next.md).
835
1153
  * @param {string} target
836
1154
  * @param {{ discuss_before_plan?: boolean }} cfg
837
1155
  */
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)) {
1156
+ function suggestNextStep(target, cfg = {}) {
1157
+ const base = oxePaths(target);
1158
+ const stateText = fs.existsSync(base.state) ? fs.readFileSync(base.state, 'utf8') : '';
1159
+ const p = scopedOxePaths(target, parseActiveSession(stateText));
1160
+ const discussBefore = Boolean(cfg.discuss_before_plan);
1161
+ const threshold = Number(cfg.plan_confidence_threshold) || 70;
1162
+ const has = (/** @type {string} */ f) => fs.existsSync(f);
1163
+ const mapsComplete = EXPECTED_CODEBASE_MAPS.every((f) => has(path.join(p.codebase, f)));
1164
+ const azureActive = azure.isAzureContextEnabled(target, cfg);
1165
+
1166
+ if (!has(p.oxe) || !has(p.state)) {
849
1167
  return {
850
1168
  step: 'scan',
851
1169
  cursorCmd: '/oxe-scan',
@@ -854,56 +1172,56 @@ function suggestNextStep(target, cfg = {}) {
854
1172
  };
855
1173
  }
856
1174
 
857
- const phase = parseStatePhase(stateText);
1175
+ const phase = parseStatePhase(stateText);
858
1176
 
859
- if (!mapsComplete && !has(p.quick)) {
1177
+ if (!mapsComplete && !has(p.quick)) {
860
1178
  return {
861
1179
  step: 'scan',
862
1180
  cursorCmd: '/oxe-scan',
863
1181
  reason: 'Mapas do codebase incompletos e sem QUICK.md — atualize o contexto com scan',
864
1182
  artifacts: ['.oxe/codebase/'],
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))) {
1183
+ };
1184
+ }
1185
+
1186
+ if (azureActive) {
1187
+ const azureHealth = azure.azureDoctor(target, cfg, {
1188
+ autoInstall: false,
1189
+ write: false,
1190
+ });
1191
+ if (!azureHealth.authStatus || !azureHealth.authStatus.login_active) {
1192
+ return {
1193
+ step: 'azure-auth',
1194
+ cursorCmd: 'npx oxe-cc azure auth login',
1195
+ reason: 'Contexto Azure ativo, mas sem sessão Azure CLI autenticada',
1196
+ artifacts: ['.oxe/cloud/azure/profile.json', '.oxe/cloud/azure/auth-status.json'],
1197
+ };
1198
+ }
1199
+ if (!azureHealth.profile || !azureHealth.profile.subscription_id) {
1200
+ return {
1201
+ step: 'azure-auth',
1202
+ cursorCmd: 'npx oxe-cc azure auth set-subscription --subscription <id-ou-nome>',
1203
+ reason: 'Contexto Azure ativo, mas a subscription operacional ainda não está definida',
1204
+ artifacts: ['.oxe/cloud/azure/profile.json', '.oxe/cloud/azure/auth-status.json'],
1205
+ };
1206
+ }
1207
+ const maxAgeHours = cfg.azure && cfg.azure.inventory_max_age_hours != null
1208
+ ? Number(cfg.azure.inventory_max_age_hours)
1209
+ : 24;
1210
+ const syncedAt = Date.parse(String(azureHealth.inventory && azureHealth.inventory.synced_at || ''));
1211
+ const staleInventory =
1212
+ !azureHealth.inventory ||
1213
+ (maxAgeHours > 0 && !Number.isNaN(syncedAt) && ((Date.now() - syncedAt) / (1000 * 60 * 60)) > maxAgeHours);
1214
+ if (staleInventory) {
1215
+ return {
1216
+ step: 'azure-sync',
1217
+ cursorCmd: 'npx oxe-cc azure sync',
1218
+ reason: 'Contexto Azure ativo, mas o inventário está ausente ou stale',
1219
+ artifacts: ['.oxe/cloud/azure/inventory.json', '.oxe/cloud/azure/INVENTORY.md'],
1220
+ };
1221
+ }
1222
+ }
1223
+
1224
+ if (phase === 'quick_active' || (has(p.quick) && !has(p.plan))) {
907
1225
  return {
908
1226
  step: 'execute',
909
1227
  cursorCmd: '/oxe-execute',
@@ -930,71 +1248,71 @@ function suggestNextStep(target, cfg = {}) {
930
1248
  };
931
1249
  }
932
1250
 
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)) {
1251
+ if (!has(p.plan)) {
1252
+ return {
1253
+ step: 'plan',
1254
+ cursorCmd: '/oxe-plan',
1255
+ reason: 'SPEC existe mas PLAN.md não — gere o plano com verificação por tarefa',
1256
+ artifacts: ['.oxe/PLAN.md'],
1257
+ };
1258
+ }
1259
+
1260
+ const selfEval = parsePlanSelfEvaluation(p.plan);
1261
+ if (selfEval.bestPlan === 'não' || (selfEval.confidence != null && selfEval.confidence < threshold)) {
1262
+ return {
1263
+ step: 'plan',
1264
+ cursorCmd: '/oxe-plan --replan',
1265
+ reason: `O plano atual ainda não atingiu confiança executável (limiar ${threshold}%)`,
1266
+ artifacts: ['.oxe/PLAN.md', '.oxe/STATE.md'],
1267
+ };
1268
+ }
1269
+
1270
+ const reviewStatus = parsePlanReviewStatus(stateText);
1271
+ if (phase === 'plan_ready' && (reviewStatus === 'needs_revision' || reviewStatus === 'rejected')) {
1272
+ return {
1273
+ step: 'plan',
1274
+ cursorCmd: '/oxe-plan --replan',
1275
+ reason: `Revisão do plano em estado ${reviewStatus} — ajuste o plano antes de executar`,
1276
+ artifacts: ['.oxe/PLAN.md', '.oxe/PLAN-REVIEW.md', '.oxe/STATE.md'],
1277
+ };
1278
+ }
1279
+ if (phase === 'plan_ready' && (!reviewStatus || reviewStatus === 'draft' || reviewStatus === 'in_review')) {
1280
+ return {
1281
+ step: 'dashboard',
1282
+ cursorCmd: '/oxe-dashboard',
1283
+ reason: 'Plano pronto, mas ainda não passou por revisão/aprovação visual',
1284
+ artifacts: ['.oxe/PLAN.md', '.oxe/PLAN-REVIEW.md', '.oxe/STATE.md'],
1285
+ };
1286
+ }
1287
+
1288
+ const activeRun = operational.readRunState(target, parseActiveSession(stateText));
1289
+ if (activeRun && activeRun.status === 'waiting_approval') {
1290
+ return {
1291
+ step: 'dashboard',
1292
+ cursorCmd: '/oxe-dashboard',
1293
+ reason: 'ACTIVE-RUN está aguardando aprovação formal antes de continuar',
1294
+ artifacts: ['.oxe/ACTIVE-RUN.json', '.oxe/CHECKPOINTS.md', '.oxe/OXE-EVENTS.ndjson'],
1295
+ };
1296
+ }
1297
+ if (activeRun && activeRun.status === 'paused') {
1298
+ return {
1299
+ step: 'execute',
1300
+ cursorCmd: '/oxe-execute',
1301
+ reason: 'ACTIVE-RUN está pausado — retome a execução a partir do cursor atual',
1302
+ artifacts: ['.oxe/ACTIVE-RUN.json', '.oxe/EXECUTION-RUNTIME.md'],
1303
+ };
1304
+ }
1305
+
1306
+ if (/\*\*checkpoint_status:\*\*\s*`?pending_approval`?/i.test(stateText)) {
1307
+ return {
1308
+ step: 'execute',
1309
+ cursorCmd: '/oxe-execute',
1310
+ reason: 'Há checkpoint pendente de aprovação — resolva a aprovação antes de avançar a execução',
1311
+ artifacts: ['.oxe/CHECKPOINTS.md', '.oxe/STATE.md'],
1312
+ };
1313
+ }
1314
+
1315
+ if (!has(p.verify)) {
998
1316
  return {
999
1317
  step: 'execute',
1000
1318
  cursorCmd: '/oxe-execute',
@@ -1065,140 +1383,292 @@ function suggestNextStep(target, cfg = {}) {
1065
1383
  /**
1066
1384
  * @param {string} target
1067
1385
  */
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);
1386
+ function buildHealthReport(target) {
1387
+ const contextEngine = require('./oxe-context-engine.cjs');
1388
+ const runtimeSemantics = require('./oxe-runtime-semantics.cjs');
1389
+ const { config, path: cfgPath, parseError } = loadOxeConfigMerged(target);
1390
+ const shape = validateConfigShape(config);
1391
+ const base = oxePaths(target);
1392
+ let stateText = '';
1393
+ if (fs.existsSync(base.state)) {
1394
+ try {
1395
+ stateText = fs.readFileSync(base.state, 'utf8');
1396
+ } catch {
1397
+ stateText = '';
1398
+ }
1399
+ }
1400
+ const activeSession = parseActiveSession(stateText);
1401
+ const p = scopedOxePaths(target, activeSession);
1402
+ const phase = parseStatePhase(stateText);
1083
1403
  const scanDate = parseLastScanDate(stateText);
1084
1404
  const stale = isStaleScan(scanDate, Number(config.scan_max_age_days) || 0);
1085
1405
  const compactDate = parseLastCompactDate(stateText);
1086
1406
  const staleCompact = isStaleScan(compactDate, Number(config.compact_max_age_days) || 0);
1087
1407
  const retroDate = parseLastRetroDate(stateText);
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 {
1408
+ const staleLessons = isStaleLessons(retroDate, Number(config.lessons_max_age_days) || 0);
1409
+ const phaseWarn = phase ? phaseCoherenceWarnings(phase, p) : [];
1410
+ const runtimeWarn = runtimeWarnings(stateText, p);
1411
+ const sumWarn = verifyGapsWithoutSummaryWarning(p.verify, p.summary);
1412
+ const specReq = Array.isArray(config.spec_required_sections) ? config.spec_required_sections : [];
1413
+ const specWarn = specSectionWarnings(p.spec, specReq.map(String));
1414
+ const threshold = Number(config.plan_confidence_threshold) || 70;
1415
+ const capabilityWarn = capabilityWarnings(p);
1416
+ const investigationWarn = investigationWarnings(p);
1417
+ const planWarn = [
1418
+ ...planWaveWarningsFixed(p.plan, Number(config.plan_max_tasks_per_wave) || 0),
1419
+ ...planTaskAceiteWarnings(p.plan),
1420
+ ...planSelfEvaluationWarnings(p.plan, threshold),
1421
+ ...planAgentsWarnings(target),
1422
+ ];
1423
+ const sessionWarn = sessionWarnings(target, activeSession);
1424
+ const installWarn = installationCompletenessWarnings(target);
1425
+ const copilot = copilotIntegrationReport(target);
1426
+ const copilotWarn = copilot.warnings;
1427
+ const reviewWarn = planReviewWarnings(stateText, p);
1428
+ const planSelfEvaluation = parsePlanSelfEvaluation(p.plan);
1429
+ const activeRun = operational.readRunState(target, activeSession);
1430
+ const eventsSummary = operational.summarizeEvents(operational.readEvents(target, activeSession));
1431
+ const memoryLayers = operational.buildMemoryLayers(target, activeSession);
1432
+ const azureActive = azure.isAzureContextEnabled(target, config);
1433
+ const azureReport = azureActive
1434
+ ? azure.azureDoctor(target, config, {
1435
+ autoInstall: false,
1436
+ write: false,
1437
+ })
1438
+ : null;
1439
+ const azureInventorySyncedAt = azureReport && azureReport.inventory ? azureReport.inventory.synced_at || null : null;
1440
+ const azureInventoryMaxAgeHours = config.azure && config.azure.inventory_max_age_hours != null
1441
+ ? Number(config.azure.inventory_max_age_hours)
1442
+ : 24;
1443
+ let azureInventoryStale = { stale: false, hours: null };
1444
+ if (azureInventorySyncedAt) {
1445
+ const syncedAt = Date.parse(String(azureInventorySyncedAt));
1446
+ if (!Number.isNaN(syncedAt)) {
1447
+ const ageHours = Math.floor((Date.now() - syncedAt) / (1000 * 60 * 60));
1448
+ azureInventoryStale = {
1449
+ stale: azureInventoryMaxAgeHours > 0 ? ageHours > azureInventoryMaxAgeHours : false,
1450
+ hours: ageHours,
1451
+ };
1452
+ }
1453
+ } else if (azureActive) {
1454
+ azureInventoryStale = { stale: true, hours: null };
1455
+ }
1456
+ const next = suggestNextStep(target, {
1457
+ discuss_before_plan: config.discuss_before_plan,
1458
+ plan_confidence_threshold: threshold,
1459
+ azure: config.azure,
1460
+ });
1461
+ /** @type {string[]} */
1462
+ const contextWarn = [];
1463
+ /** @type {Record<string, unknown>} */
1464
+ const contextPacks = {};
1465
+ /** @type {Record<string, unknown>} */
1466
+ const packFreshness = {};
1467
+ /** @type {{ project: string | null, session: string | null, phase: string | null }} */
1468
+ let activeSummaryRefs = { project: null, session: null, phase: null };
1469
+ /** @type {{ primaryWorkflow: string | null, primaryScore: number | null, primaryStatus: string | null, byWorkflow: Record<string, unknown> }} */
1470
+ let contextQuality = {
1471
+ primaryWorkflow: null,
1472
+ primaryScore: null,
1473
+ primaryStatus: null,
1474
+ byWorkflow: {},
1475
+ };
1476
+ // Bloco A — resolução de paths e refs de summaries
1477
+ try {
1478
+ const ctxPaths = contextEngine.contextPaths(target, activeSession);
1479
+ activeSummaryRefs = {
1480
+ project: ctxPaths.projectSummaryJson,
1481
+ session: ctxPaths.sessionSummaryJson,
1482
+ phase: ctxPaths.phaseSummaryJson,
1483
+ };
1484
+ } catch (err) {
1485
+ contextWarn.push(`Contexto — falha ao resolver paths (contextPaths): ${err instanceof Error ? err.message : String(err)}`);
1486
+ }
1487
+ // Bloco B — inspeção de context packs por workflow
1488
+ try {
1489
+ const contextMaterialized = fs.existsSync(base.contextIndex);
1490
+ const candidateWorkflows = Array.from(
1491
+ new Set(
1492
+ ['dashboard', next.step, phase === 'planning' ? 'plan' : null, phase === 'executing' ? 'execute' : null, phase === 'verifying' ? 'verify' : null]
1493
+ .filter(Boolean)
1494
+ .map(String)
1495
+ )
1496
+ ).filter((workflow) => Boolean(runtimeSemantics.getWorkflowContract(workflow)));
1497
+ for (const workflow of candidateWorkflows) {
1498
+ let pack;
1499
+ try {
1500
+ pack = contextEngine.inspectContextPack(target, { workflow, activeSession });
1501
+ } catch (err) {
1502
+ contextWarn.push(`Context pack ${workflow} — falha na inspeção: ${err instanceof Error ? err.message : String(err)}`);
1503
+ continue;
1504
+ }
1505
+ contextPacks[workflow] = {
1506
+ path: pack.path || contextEngine.resolvePackFile(target, workflow, activeSession),
1507
+ context_tier: pack.context_tier,
1508
+ semantics_hash: pack.semantics_hash,
1509
+ read_order: pack.read_order,
1510
+ selected_artifacts: (pack.selected_artifacts || []).map((artifact) => ({
1511
+ alias: artifact.alias,
1512
+ path: artifact.path,
1513
+ exists: artifact.exists,
1514
+ required: artifact.required,
1515
+ using_fallback: artifact.using_fallback,
1516
+ scope: artifact.scope,
1517
+ summary: artifact.summary,
1518
+ })),
1519
+ gaps: pack.gaps,
1520
+ conflicts: pack.conflicts,
1521
+ fallback_required: pack.fallback_required,
1522
+ freshness: pack.freshness,
1523
+ context_quality: pack.context_quality,
1524
+ };
1525
+ packFreshness[workflow] = pack.freshness;
1526
+ contextQuality.byWorkflow[workflow] = pack.context_quality;
1527
+ if (contextMaterialized && pack.fallback_required) {
1528
+ contextWarn.push(`Context pack ${workflow} exige fallback explícito para leitura direta.`);
1529
+ }
1530
+ if (contextMaterialized && pack.freshness && pack.freshness.stale) {
1531
+ contextWarn.push(`Context pack ${workflow} stale (${pack.freshness.reason}).`);
1532
+ }
1533
+ }
1534
+ if (contextPacks.dashboard) {
1535
+ contextQuality.primaryWorkflow = 'dashboard';
1536
+ contextQuality.primaryScore = contextPacks.dashboard.context_quality.score;
1537
+ contextQuality.primaryStatus = contextPacks.dashboard.context_quality.status;
1538
+ } else {
1539
+ const firstWorkflow = Object.keys(contextPacks)[0] || null;
1540
+ if (firstWorkflow) {
1541
+ contextQuality.primaryWorkflow = firstWorkflow;
1542
+ contextQuality.primaryScore = contextPacks[firstWorkflow].context_quality.score;
1543
+ contextQuality.primaryStatus = contextPacks[firstWorkflow].context_quality.status;
1544
+ }
1545
+ }
1546
+ } catch (err) {
1547
+ contextWarn.push(`Contexto — falha ao inspecionar context packs: ${err instanceof Error ? err.message : String(err)}`);
1548
+ }
1549
+ const semanticsManifest = readJsonFileSafe(base.runtimeSemanticsManifest);
1550
+ const semanticsAudit = runtimeSemantics.auditRuntimeTargets(target);
1551
+ /** @type {string[]} */
1552
+ const semanticsWarn = [];
1553
+ const semanticTargetsPresent = [
1554
+ path.join(target, '.github', 'prompts'),
1555
+ path.join(target, 'commands', 'oxe'),
1556
+ path.join(target, '.cursor', 'commands'),
1557
+ ].some((dirPath) => fs.existsSync(dirPath));
1558
+ if (semanticTargetsPresent && !semanticsManifest.ok) {
1559
+ semanticsWarn.push('runtime-semantics.json ausente ou inválido — rode `npx oxe-cc update` para sincronizar o manifest semântico.');
1560
+ }
1561
+ if (semanticsManifest.error) {
1562
+ semanticsWarn.push(`runtime-semantics.json inválido: ${semanticsManifest.error}`);
1563
+ }
1564
+ if (semanticsManifest.data && semanticsManifest.data.contract_version && semanticsManifest.data.contract_version !== runtimeSemantics.CONTRACT_VERSION) {
1565
+ semanticsWarn.push(
1566
+ `Manifest semântico em versão ${semanticsManifest.data.contract_version}; esperado ${runtimeSemantics.CONTRACT_VERSION}.`
1567
+ );
1568
+ }
1569
+ if (semanticsAudit.registryIssues.length) {
1570
+ semanticsWarn.push(...semanticsAudit.registryIssues);
1571
+ }
1572
+ if (semanticsAudit.mismatches.length) {
1573
+ semanticsWarn.push(`${semanticsAudit.mismatches.length} wrapper(s) com drift semântico detectado.`);
1574
+ }
1575
+ const semanticsDrift = {
1576
+ ok: semanticsWarn.length === 0 && semanticsAudit.ok,
1577
+ contractVersion: runtimeSemantics.CONTRACT_VERSION,
1578
+ manifestPath: base.runtimeSemanticsManifest,
1579
+ manifest: semanticsManifest.data,
1580
+ audit: {
1581
+ ok: semanticsAudit.ok,
1582
+ warnings: semanticsAudit.warnings,
1583
+ mismatchCount: semanticsAudit.mismatches.length,
1584
+ mismatches: semanticsAudit.mismatches,
1585
+ targets: Object.fromEntries(
1586
+ Object.entries(semanticsAudit.targets || {}).map(([name, value]) => [
1587
+ name,
1588
+ {
1589
+ path: value.path,
1590
+ checked: value.checked,
1591
+ missing: value.missing,
1592
+ },
1593
+ ])
1594
+ ),
1595
+ },
1596
+ };
1597
+ const hardFailure = Boolean(parseError) || sessionWarn.some((w) => /não existe|sem SESSION\.md/i.test(w));
1598
+ const warningCount =
1599
+ phaseWarn.length +
1600
+ runtimeWarn.length +
1601
+ reviewWarn.length +
1602
+ specWarn.length +
1603
+ planWarn.length +
1604
+ capabilityWarn.length +
1605
+ investigationWarn.length +
1606
+ sessionWarn.length +
1607
+ installWarn.length +
1608
+ copilotWarn.length +
1609
+ contextWarn.length +
1610
+ semanticsWarn.length +
1611
+ (azureReport ? azureReport.warnings.length : 0) +
1612
+ (sumWarn ? 1 : 0);
1613
+ const healthStatus = hardFailure ? 'broken' : warningCount > 0 ? 'warning' : 'healthy';
1614
+
1615
+ return {
1155
1616
  configPath: cfgPath,
1156
1617
  configParseError: parseError,
1157
1618
  unknownConfigKeys: shape.unknownKeys,
1158
- typeErrors: shape.typeErrors,
1159
- phase,
1160
- activeSession,
1161
- scanDate,
1619
+ typeErrors: shape.typeErrors,
1620
+ phase,
1621
+ activeSession,
1622
+ scanDate,
1162
1623
  stale,
1163
1624
  compactDate,
1164
1625
  staleCompact,
1165
1626
  retroDate,
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
- };
1627
+ staleLessons,
1628
+ phaseWarn,
1629
+ runtimeWarn,
1630
+ reviewWarn,
1631
+ capabilityWarn,
1632
+ investigationWarn,
1633
+ sessionWarn,
1634
+ installWarn,
1635
+ copilotWarn,
1636
+ contextWarn,
1637
+ semanticsWarn,
1638
+ copilot,
1639
+ summaryGapWarn: sumWarn,
1640
+ specWarn,
1641
+ planWarn,
1642
+ planSelfEvaluation,
1643
+ planReviewStatus: parsePlanReviewStatus(stateText),
1644
+ activeRun,
1645
+ eventsSummary,
1646
+ memoryLayers,
1647
+ azureActive,
1648
+ azure: azureReport
1649
+ ? {
1650
+ profile: azureReport.profile,
1651
+ authStatus: azureReport.authStatus,
1652
+ inventorySummary: azureReport.inventorySummary,
1653
+ inventoryPath: azureReport.paths.inventory,
1654
+ operationsPath: azureReport.paths.operationsDir,
1655
+ inventorySyncedAt: azureInventorySyncedAt,
1656
+ inventoryStale: azureInventoryStale,
1657
+ pendingOperations: azure.listAzureOperations(target).filter((operation) => operation.phase === 'waiting_approval').length,
1658
+ lastOperation: azure.listAzureOperations(target)[0] || null,
1659
+ warnings: azureReport.warnings,
1660
+ }
1661
+ : null,
1662
+ contextPacks,
1663
+ contextQuality,
1664
+ semanticsDrift,
1665
+ packFreshness,
1666
+ activeSummaryRefs,
1667
+ healthStatus,
1668
+ next,
1669
+ scanFocusGlobs: config.scan_focus_globs,
1670
+ scanIgnoreGlobs: config.scan_ignore_globs,
1671
+ };
1202
1672
  }
1203
1673
 
1204
1674
  module.exports = {
@@ -1213,28 +1683,31 @@ module.exports = {
1213
1683
  loadOxeConfigMerged,
1214
1684
  validateConfigShape,
1215
1685
  parseStatePhase,
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,
1686
+ parseLastScanDate,
1687
+ parseLastCompactDate,
1688
+ parseLastRetroDate,
1689
+ parseActiveSession,
1690
+ parsePlanReviewStatus,
1691
+ isStaleScan,
1692
+ isStaleLessons,
1693
+ copilotWorkspacePaths,
1694
+ copilotLegacyPaths,
1695
+ copilotIntegrationReport,
1696
+ planAgentsWarnings,
1697
+ installationCompletenessWarnings,
1698
+ parsePlanSelfEvaluation,
1699
+ planSelfEvaluationWarnings,
1700
+ runtimeWarnings,
1701
+ planReviewWarnings,
1702
+ capabilityWarnings,
1703
+ investigationWarnings,
1704
+ phaseCoherenceWarnings,
1232
1705
  verifyGapsWithoutSummaryWarning,
1233
1706
  specSectionWarnings,
1234
1707
  planWaveWarningsFixed,
1235
1708
  planTaskAceiteWarnings,
1236
- suggestNextStep,
1237
- buildHealthReport,
1238
- oxePaths,
1239
- scopedOxePaths,
1240
- };
1709
+ suggestNextStep,
1710
+ buildHealthReport,
1711
+ oxePaths,
1712
+ scopedOxePaths,
1713
+ };