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.
- package/CHANGELOG.md +106 -0
- package/README.md +736 -681
- package/bin/lib/oxe-event-bus.cjs +118 -0
- package/bin/lib/oxe-memory-kernel.cjs +188 -0
- package/bin/lib/oxe-project-health.cjs +75 -0
- package/bin/lib/oxe-skill-loader.cjs +131 -0
- package/bin/oxe-cc.js +202 -40
- package/docs/INTEGRATION.md +152 -0
- package/docs/oxe-artifact-map.html +1172 -0
- package/lib/sdk/index.cjs +2 -0
- package/lib/sdk/index.d.ts +185 -159
- package/oxe/schemas/swarm-run.schema.json +130 -0
- package/oxe/workflows/agent-mode.md +150 -0
- package/oxe/workflows/conduct.md +149 -0
- package/oxe/workflows/distill.md +164 -0
- package/oxe/workflows/help.md +1 -0
- package/oxe/workflows/memory.md +163 -0
- package/oxe/workflows/oxe.md +2 -1
- package/oxe/workflows/references/workflow-runtime-contracts.json +1113 -960
- package/oxe/workflows/route.md +7 -5
- package/oxe/workflows/swarm/board.md +119 -0
- package/oxe/workflows/swarm/scout.md +170 -0
- package/oxe/workflows/swarm-mode.md +280 -0
- package/package.json +2 -2
- package/packages/runtime/package.json +1 -1
- package/vscode-extension/package.json +1 -1
|
@@ -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
|
+
};
|