oxe-cc 1.11.0 → 1.14.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.
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ /**
8
+ * OXE Event Bus
9
+ *
10
+ * Wrapper sobre o runtime event bus para emitir eventos em OXE-EVENTS.ndjson.
11
+ * Compatível com o schema OxeEvent definido em packages/runtime/src/events/envelope.ts.
12
+ *
13
+ * Uso:
14
+ * const bus = require('./oxe-event-bus.cjs');
15
+ * bus.emit(projectRoot, 'RunStarted', { mode: 'agent', objective: '...' }, { session_id, run_id });
16
+ */
17
+
18
+ const EVENTS_FILE = 'OXE-EVENTS.ndjson';
19
+
20
+ function emit(projectRoot, eventType, payload = {}, context = {}) {
21
+ const event = {
22
+ id: crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex'),
23
+ type: eventType,
24
+ timestamp: new Date().toISOString(),
25
+ session_id: context.session_id || null,
26
+ run_id: context.run_id || null,
27
+ work_item_id: context.work_item_id || null,
28
+ attempt_id: context.attempt_id || null,
29
+ causation_id: context.causation_id || null,
30
+ correlation_id: context.correlation_id || null,
31
+ payload,
32
+ };
33
+
34
+ const eventsPath = path.join(projectRoot, '.oxe', EVENTS_FILE);
35
+
36
+ try {
37
+ fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
38
+ fs.appendFileSync(eventsPath, JSON.stringify(event) + '\n', 'utf8');
39
+ } catch {
40
+ // non-fatal: event emission should never break the main flow
41
+ }
42
+
43
+ return event;
44
+ }
45
+
46
+ function emitRunStarted(projectRoot, runId, sessionId, mode, objective, persona) {
47
+ return emit(projectRoot, 'RunStarted', { mode, objective, persona }, { run_id: runId, session_id: sessionId });
48
+ }
49
+
50
+ function emitWorkItemCompleted(projectRoot, runId, sessionId, taskId, filesChanged) {
51
+ return emit(projectRoot, 'WorkItemCompleted', { task_id: taskId, files_changed: filesChanged || [] }, { run_id: runId, session_id: sessionId, work_item_id: taskId });
52
+ }
53
+
54
+ function emitWorkItemBlocked(projectRoot, runId, sessionId, taskId, reason) {
55
+ return emit(projectRoot, 'WorkItemBlocked', { task_id: taskId, reason }, { run_id: runId, session_id: sessionId, work_item_id: taskId });
56
+ }
57
+
58
+ function emitRunCompleted(projectRoot, runId, sessionId, status, summary) {
59
+ return emit(projectRoot, 'RunCompleted', { status, ...summary }, { run_id: runId, session_id: sessionId });
60
+ }
61
+
62
+ function emitGateRequested(projectRoot, runId, sessionId, gateId, condition, type) {
63
+ return emit(projectRoot, 'GateRequested', { gate_id: gateId, condition, type }, { run_id: runId, session_id: sessionId });
64
+ }
65
+
66
+ function emitGateResolved(projectRoot, runId, sessionId, gateId, resolution, actor) {
67
+ return emit(projectRoot, 'GateResolved', { gate_id: gateId, resolution, actor }, { run_id: runId, session_id: sessionId });
68
+ }
69
+
70
+ function emitLessonPromoted(projectRoot, runId, lessonId, frequency, impact) {
71
+ return emit(projectRoot, 'LessonPromoted', { lesson_id: lessonId, frequency, impact }, { run_id: runId });
72
+ }
73
+
74
+ function emitRetroPublished(projectRoot, runId, lessonsAdded, lessonsUpdated) {
75
+ return emit(projectRoot, 'RetroPublished', { run_id: runId, lessons_added: lessonsAdded, lessons_updated: lessonsUpdated }, { run_id: runId });
76
+ }
77
+
78
+ function readEvents(projectRoot, filters = {}) {
79
+ const eventsPath = path.join(projectRoot, '.oxe', EVENTS_FILE);
80
+
81
+ if (!fs.existsSync(eventsPath)) {
82
+ return [];
83
+ }
84
+
85
+ try {
86
+ const lines = fs.readFileSync(eventsPath, 'utf8').split('\n').filter(Boolean);
87
+ let events = lines.map(line => {
88
+ try { return JSON.parse(line); } catch { return null; }
89
+ }).filter(Boolean);
90
+
91
+ if (filters.run_id) {
92
+ events = events.filter(e => e.run_id === filters.run_id);
93
+ }
94
+ if (filters.type) {
95
+ events = events.filter(e => e.type === filters.type);
96
+ }
97
+ if (filters.since) {
98
+ events = events.filter(e => e.timestamp >= filters.since);
99
+ }
100
+
101
+ return events;
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ module.exports = {
108
+ emit,
109
+ emitRunStarted,
110
+ emitWorkItemCompleted,
111
+ emitWorkItemBlocked,
112
+ emitRunCompleted,
113
+ emitGateRequested,
114
+ emitGateResolved,
115
+ emitLessonPromoted,
116
+ emitRetroPublished,
117
+ readEvents,
118
+ };
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * OXE Memory Kernel
8
+ *
9
+ * Recupera memória relevante ao objetivo atual lendo as 4 camadas de memória do OXE
10
+ * na ordem definida por buildMemoryLayers() (oxe-operational.cjs:2364):
11
+ * 1. runtime_state → .oxe/STATE.md
12
+ * 2. session_memory → .oxe/<session>/SESSION.md
13
+ * 3. project_memory → .oxe/memory/REPO-MEMORY.md
14
+ * 4. lessons → .oxe/global/LESSONS.md
15
+ * 5. observations → .oxe/OBSERVATIONS.md
16
+ *
17
+ * Retorna um context pack filtrado por intent_tags e phase.
18
+ */
19
+
20
+ function readFileSafe(filePath) {
21
+ try {
22
+ if (fs.existsSync(filePath)) {
23
+ return fs.readFileSync(filePath, 'utf8');
24
+ }
25
+ } catch {
26
+ // non-fatal
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function extractRelevantFragments(content, intentTags, phase) {
32
+ if (!content) return [];
33
+
34
+ const lines = content.split('\n');
35
+ const fragments = [];
36
+ let currentSection = null;
37
+ let currentLines = [];
38
+ let currentScore = 0;
39
+
40
+ const scoreFragment = (text) => {
41
+ let score = 0;
42
+ for (const tag of intentTags) {
43
+ if (text.toLowerCase().includes(tag.toLowerCase())) score += 3;
44
+ }
45
+ if (text.includes('Impacto: alto') || text.includes('impact: high')) score += 2;
46
+ if (text.includes('Frequência: 3') || text.includes('Frequência: 4') || text.includes('Frequência: 5')) score += 2;
47
+ if (text.includes(phase)) score += 2;
48
+ if (text.includes('Frequência: 2')) score += 1;
49
+ if (text.includes('Status: ativo') || text.includes('status: active')) score += 1;
50
+ return score;
51
+ };
52
+
53
+ for (const line of lines) {
54
+ if (line.startsWith('## ') || line.startsWith('### ')) {
55
+ if (currentSection && currentLines.length > 0 && currentScore > 0) {
56
+ fragments.push({ section: currentSection, content: currentLines.join('\n'), score: currentScore });
57
+ }
58
+ currentSection = line.trim();
59
+ currentLines = [line];
60
+ currentScore = 0;
61
+ } else {
62
+ currentLines.push(line);
63
+ currentScore = Math.max(currentScore, scoreFragment(line));
64
+ }
65
+ }
66
+
67
+ if (currentSection && currentLines.length > 0 && currentScore > 0) {
68
+ fragments.push({ section: currentSection, content: currentLines.join('\n'), score: currentScore });
69
+ }
70
+
71
+ return fragments.sort((a, b) => b.score - a.score).slice(0, 10);
72
+ }
73
+
74
+ function retrieveMemory(projectRoot, intentTags, phase, objective) {
75
+ const layers = {
76
+ runtime_state: null,
77
+ session_memory: null,
78
+ project_memory: null,
79
+ lessons: null,
80
+ observations: null,
81
+ };
82
+
83
+ // Layer 1: runtime_state
84
+ layers.runtime_state = readFileSafe(path.join(projectRoot, '.oxe', 'STATE.md'));
85
+
86
+ // Layer 2: session_memory (read active session from STATE.md)
87
+ if (layers.runtime_state) {
88
+ const sessionMatch = layers.runtime_state.match(/active_session:\s*(.+)/);
89
+ if (sessionMatch && sessionMatch[1].trim() !== '—') {
90
+ const sessionPath = sessionMatch[1].trim();
91
+ layers.session_memory = readFileSafe(path.join(projectRoot, sessionPath, 'SESSION.md'))
92
+ || readFileSafe(path.join(projectRoot, '.oxe', sessionPath, 'SESSION.md'));
93
+ }
94
+ }
95
+
96
+ // Layer 3: project_memory
97
+ layers.project_memory = readFileSafe(path.join(projectRoot, '.oxe', 'memory', 'REPO-MEMORY.md'));
98
+
99
+ // Layer 4: lessons
100
+ layers.lessons = readFileSafe(path.join(projectRoot, '.oxe', 'global', 'LESSONS.md'));
101
+
102
+ // Layer 5: observations
103
+ layers.observations = readFileSafe(path.join(projectRoot, '.oxe', 'OBSERVATIONS.md'));
104
+
105
+ // Extract relevant fragments from each layer
106
+ const contextParts = [];
107
+
108
+ if (layers.runtime_state) {
109
+ contextParts.push(`### Estado Atual\n${layers.runtime_state.split('\n').slice(0, 20).join('\n')}`);
110
+ }
111
+
112
+ if (layers.project_memory) {
113
+ const frags = extractRelevantFragments(layers.project_memory, intentTags, phase);
114
+ if (frags.length > 0) {
115
+ contextParts.push(`### Memória do Projeto\n${frags.map(f => f.content).join('\n\n')}`);
116
+ }
117
+ }
118
+
119
+ if (layers.lessons) {
120
+ const frags = extractRelevantFragments(layers.lessons, intentTags, phase);
121
+ const activeFrags = frags.filter(f => f.content.includes('Status: ativo'));
122
+ if (activeFrags.length > 0) {
123
+ contextParts.push(`### Lições Aplicáveis\n${activeFrags.map(f => f.content).join('\n\n')}`);
124
+ }
125
+ }
126
+
127
+ if (layers.session_memory) {
128
+ contextParts.push(`### Contexto da Sessão\n${layers.session_memory.split('\n').slice(0, 30).join('\n')}`);
129
+ }
130
+
131
+ if (layers.observations) {
132
+ const pendingObs = layers.observations.split('\n\n').filter(block =>
133
+ block.includes('Status: pendente') &&
134
+ (block.includes('all') || intentTags.some(tag => block.toLowerCase().includes(tag)))
135
+ );
136
+ if (pendingObs.length > 0) {
137
+ contextParts.push(`### Observações Pendentes\n${pendingObs.join('\n\n')}`);
138
+ }
139
+ }
140
+
141
+ const hasMemory = contextParts.length > 1; // more than just state
142
+ const contextPack = [
143
+ `## Contexto de Memória — ${phase} / ${new Date().toISOString()}`,
144
+ `**Objetivo:** ${objective}`,
145
+ `**Intent Tags:** ${intentTags.join(', ')}`,
146
+ `**Fontes:** ${Object.entries(layers).filter(([, v]) => v).map(([k]) => k).join(', ')}`,
147
+ '',
148
+ ...contextParts,
149
+ ].join('\n');
150
+
151
+ return { contextPack, hasMemory, layers: Object.keys(layers).filter(k => layers[k]) };
152
+ }
153
+
154
+ function saveContextPack(projectRoot, contextPack, mode, phase) {
155
+ let outPath;
156
+
157
+ if (mode === 'agent') {
158
+ outPath = path.join(projectRoot, '.oxe', 'agent', 'MEMORY-INJECTIONS.md');
159
+ } else {
160
+ outPath = path.join(projectRoot, '.oxe', 'swarm', 'DECISIONS.md');
161
+ }
162
+
163
+ try {
164
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
165
+
166
+ // For DECISIONS.md (swarm), prepend a section header
167
+ if (mode === 'swarm' && fs.existsSync(outPath)) {
168
+ const existing = fs.readFileSync(outPath, 'utf8');
169
+ if (!existing.includes('## Contexto de Memória')) {
170
+ fs.writeFileSync(outPath, existing + '\n\n' + contextPack, 'utf8');
171
+ }
172
+ } else {
173
+ fs.writeFileSync(outPath, contextPack, 'utf8');
174
+ }
175
+
176
+ // Also save snapshot in retrieved/
177
+ const retrievedDir = path.join(projectRoot, '.oxe', 'memory', 'retrieved');
178
+ fs.mkdirSync(retrievedDir, { recursive: true });
179
+ fs.writeFileSync(path.join(retrievedDir, `${phase}.md`), contextPack, 'utf8');
180
+ } catch {
181
+ // non-fatal
182
+ }
183
+ }
184
+
185
+ module.exports = {
186
+ retrieveMemory,
187
+ saveContextPack,
188
+ };
@@ -2730,6 +2730,79 @@ function buildHealthReport(target) {
2730
2730
  };
2731
2731
  }
2732
2732
 
2733
+ /**
2734
+ * Compact, stable projection of `buildHealthReport` for host integrations
2735
+ * (IDEs, OXESpace). Versioned independently via `oxeSummarySchema` so a host
2736
+ * can depend on a small, cheap payload instead of parsing the full ~100KB
2737
+ * status. Pure — pass in a report from `buildHealthReport`.
2738
+ * @param {ReturnType<typeof buildHealthReport>} report
2739
+ */
2740
+ function buildStatusSummary(report) {
2741
+ const next = (report && report.next) || {};
2742
+ const gaps = Array.isArray(report && report.criticalExecutionGaps) ? report.criticalExecutionGaps.length : 0;
2743
+ const planWarn = report && report.planSelfEvaluation && Array.isArray(report.planSelfEvaluation.warnings)
2744
+ ? report.planSelfEvaluation.warnings.length
2745
+ : 0;
2746
+ return {
2747
+ oxeSummarySchema: 1,
2748
+ workspaceMode: (report && report.workspaceMode) || 'oxe_project',
2749
+ phase: (report && report.phase) || null,
2750
+ healthStatus: (report && report.healthStatus) || null,
2751
+ activeSession: (report && report.activeSession) || null,
2752
+ nextStep: next.step || null,
2753
+ cursorCmd: next.cursorCmd || null,
2754
+ reason: next.reason || null,
2755
+ eventsCount: report && report.eventsSummary && typeof report.eventsSummary.total === 'number' ? report.eventsSummary.total : 0,
2756
+ warningsCount: gaps + planWarn,
2757
+ };
2758
+ }
2759
+
2760
+ /**
2761
+ * Per-agent OXE skills/integration status for a workspace. Lets a host detect
2762
+ * when an agent lacks the `/oxe-*` skills BEFORE launching it — directly
2763
+ * addresses the "Failed to load N skills" failure seen in agent CLIs. Reuses
2764
+ * the tested copilot/codex integration reports plus a filesystem check of the
2765
+ * Copilot CLI skills home (~/.copilot/skills).
2766
+ * @param {string} target
2767
+ */
2768
+ function agentSkillsReport(target) {
2769
+ const agents = [];
2770
+
2771
+ const cp = copilotIntegrationReport(target);
2772
+ agents.push({
2773
+ agent: 'copilot-vscode',
2774
+ detected: cp.detected,
2775
+ skillsInstalled: cp.promptSource === 'workspace',
2776
+ skillsPath: cp.workspace.promptsDir,
2777
+ status: cp.status,
2778
+ issues: cp.warnings || [],
2779
+ });
2780
+
2781
+ const cx = codexIntegrationReport(target);
2782
+ agents.push({
2783
+ agent: 'codex',
2784
+ detected: cx.detected,
2785
+ skillsInstalled: Boolean(cx.skillsReady),
2786
+ skillsPath: cx.skillsRoot,
2787
+ status: cx.status,
2788
+ issues: cx.warnings || [],
2789
+ });
2790
+
2791
+ const cliSkillsRoot = path.join(copilotLegacyHome(), 'skills');
2792
+ const cliSkillDirs = listOxeSkillDirs(cliSkillsRoot);
2793
+ const cliInstalled = cliSkillDirs.length > 0;
2794
+ agents.push({
2795
+ agent: 'copilot-cli',
2796
+ detected: cliInstalled,
2797
+ skillsInstalled: cliInstalled,
2798
+ skillsPath: cliSkillsRoot,
2799
+ status: cliInstalled ? 'healthy' : 'not_installed',
2800
+ issues: cliInstalled ? [] : ['Skills OXE ausentes em ~/.copilot/skills — rode `oxe install --copilot-cli` e depois `/skills reload`'],
2801
+ });
2802
+
2803
+ return agents;
2804
+ }
2805
+
2733
2806
  module.exports = {
2734
2807
  ALLOWED_CONFIG_KEYS,
2735
2808
  EXECUTION_PROFILES,
@@ -2775,6 +2848,8 @@ module.exports = {
2775
2848
  shouldSuppressExecutionWorkspaceGates,
2776
2849
  suggestNextStep,
2777
2850
  buildHealthReport,
2851
+ buildStatusSummary,
2852
+ agentSkillsReport,
2778
2853
  buildExecutionRationality: rationality.buildExecutionRationality,
2779
2854
  oxePaths,
2780
2855
  scopedOxePaths,
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * OXE Skill Loader
8
+ *
9
+ * Resolve skills/personas por ID seguindo a ordem de precedência:
10
+ * 1. projeto: .oxe/skills/active/<id>.md
11
+ * 2. capabilities: .oxe/capabilities/<id>/SKILL.md
12
+ * 3. global: oxe/personas/<id>.md
13
+ *
14
+ * Retorna o conteúdo do skill/persona ou null se não encontrado.
15
+ */
16
+
17
+ function resolveSkillPath(skillId, projectRoot) {
18
+ const candidates = [
19
+ path.join(projectRoot, '.oxe', 'skills', 'active', `${skillId}.md`),
20
+ path.join(projectRoot, '.oxe', 'capabilities', skillId, 'SKILL.md'),
21
+ path.join(projectRoot, 'oxe', 'personas', `${skillId}.md`),
22
+ ];
23
+
24
+ for (const candidate of candidates) {
25
+ if (fs.existsSync(candidate)) {
26
+ return candidate;
27
+ }
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ function loadSkill(skillId, projectRoot) {
34
+ const skillPath = resolveSkillPath(skillId, projectRoot);
35
+ if (!skillPath) {
36
+ return null;
37
+ }
38
+
39
+ try {
40
+ const content = fs.readFileSync(skillPath, 'utf8');
41
+ return { id: skillId, path: skillPath, content };
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function listSkills(projectRoot) {
48
+ const skills = { active: [], proposed: [], archived: [], global: [] };
49
+
50
+ const activeDir = path.join(projectRoot, '.oxe', 'skills', 'active');
51
+ const proposedDir = path.join(projectRoot, '.oxe', 'skills', 'proposed');
52
+ const archivedDir = path.join(projectRoot, '.oxe', 'skills', 'archived');
53
+ const globalDir = path.join(projectRoot, 'oxe', 'personas');
54
+
55
+ for (const [key, dir] of Object.entries({ active: activeDir, proposed: proposedDir, archived: archivedDir, global: globalDir })) {
56
+ if (fs.existsSync(dir)) {
57
+ skills[key] = fs.readdirSync(dir)
58
+ .filter(f => f.endsWith('.md'))
59
+ .map(f => f.replace('.md', ''));
60
+ }
61
+ }
62
+
63
+ return skills;
64
+ }
65
+
66
+ /**
67
+ * Seleciona personas aplicáveis a partir de intent_tags.
68
+ * Retorna lista de {id, persona_path, content} ordenada por prioridade.
69
+ */
70
+ function selectPersonasForIntent(intentTags, projectRoot) {
71
+ const tagToPersona = {
72
+ backend: { primary: 'executor', secondary: 'architect' },
73
+ frontend: { primary: 'ui-specialist', secondary: 'executor' },
74
+ storage: { primary: 'db-specialist', secondary: 'architect' },
75
+ auth: { primary: 'architect', secondary: 'executor' },
76
+ infra: { primary: 'architect', secondary: null },
77
+ test: { primary: 'executor', secondary: 'verifier' },
78
+ docs: { primary: 'executor', secondary: null },
79
+ config: { primary: 'executor', secondary: null },
80
+ research: { primary: 'researcher', secondary: null },
81
+ debug: { primary: 'debugger', secondary: null },
82
+ };
83
+
84
+ const selected = new Map();
85
+
86
+ for (const tag of intentTags) {
87
+ const mapping = tagToPersona[tag];
88
+ if (!mapping) continue;
89
+ if (mapping.primary && !selected.has(mapping.primary)) {
90
+ selected.set(mapping.primary, 'primary');
91
+ }
92
+ if (mapping.secondary && !selected.has(mapping.secondary)) {
93
+ selected.set(mapping.secondary, 'secondary');
94
+ }
95
+ }
96
+
97
+ const result = [];
98
+ for (const [id, priority] of selected) {
99
+ const skill = loadSkill(id, projectRoot);
100
+ if (skill) {
101
+ result.push({ ...skill, priority });
102
+ }
103
+ }
104
+
105
+ return result;
106
+ }
107
+
108
+ function recordSkillsLoaded(skills, sessionDir) {
109
+ const record = {
110
+ loaded_at: new Date().toISOString(),
111
+ skills: skills.map(s => ({ id: s.id, path: s.path, priority: s.priority || 'explicit' })),
112
+ };
113
+
114
+ const outPath = path.join(sessionDir, 'SKILLS-LOADED.json');
115
+ try {
116
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
117
+ fs.writeFileSync(outPath, JSON.stringify(record, null, 2), 'utf8');
118
+ } catch {
119
+ // non-fatal
120
+ }
121
+
122
+ return record;
123
+ }
124
+
125
+ module.exports = {
126
+ loadSkill,
127
+ listSkills,
128
+ resolveSkillPath,
129
+ selectPersonasForIntent,
130
+ recordSkillsLoaded,
131
+ };