oxe-cc 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.cursor/commands/oxe-capabilities.md +11 -0
  2. package/.cursor/commands/oxe-dashboard.md +11 -0
  3. package/.github/prompts/oxe-capabilities.prompt.md +12 -0
  4. package/.github/prompts/oxe-dashboard.prompt.md +12 -0
  5. package/CHANGELOG.md +33 -0
  6. package/README.md +147 -11
  7. package/assets/oxe-framework-artifacts-paper.png +0 -0
  8. package/bin/banner.txt +1 -1
  9. package/bin/lib/oxe-azure.cjs +1445 -0
  10. package/bin/lib/oxe-dashboard.cjs +588 -0
  11. package/bin/lib/oxe-install-resolve.cjs +4 -1
  12. package/bin/lib/oxe-operational.cjs +670 -0
  13. package/bin/lib/oxe-project-health.cjs +372 -28
  14. package/bin/oxe-cc.js +1517 -312
  15. package/commands/oxe/capabilities.md +13 -0
  16. package/commands/oxe/dashboard.md +14 -0
  17. package/lib/sdk/README.md +9 -7
  18. package/lib/sdk/index.cjs +56 -0
  19. package/lib/sdk/index.d.ts +73 -0
  20. package/oxe/templates/ACTIVE-RUN.template.json +32 -0
  21. package/oxe/templates/CAPABILITIES.template.md +7 -0
  22. package/oxe/templates/CAPABILITY.template.md +45 -0
  23. package/oxe/templates/CHECKPOINTS.template.md +7 -0
  24. package/oxe/templates/EXECUTION-RUNTIME.template.md +68 -0
  25. package/oxe/templates/INVESTIGATION.template.md +38 -0
  26. package/oxe/templates/NOTES.template.md +16 -0
  27. package/oxe/templates/PLAN-REVIEW.template.md +31 -0
  28. package/oxe/templates/RESEARCH.template.md +11 -4
  29. package/oxe/templates/SPEC.template.md +6 -4
  30. package/oxe/templates/STATE.md +45 -7
  31. package/oxe/templates/config.template.json +11 -3
  32. package/oxe/workflows/ask.md +10 -1
  33. package/oxe/workflows/capabilities.md +23 -0
  34. package/oxe/workflows/dashboard.md +23 -0
  35. package/oxe/workflows/discuss.md +11 -9
  36. package/oxe/workflows/execute.md +57 -35
  37. package/oxe/workflows/help.md +256 -225
  38. package/oxe/workflows/obs.md +70 -20
  39. package/oxe/workflows/plan.md +83 -74
  40. package/oxe/workflows/quick.md +16 -11
  41. package/oxe/workflows/references/adaptive-discovery.md +27 -0
  42. package/oxe/workflows/research.md +12 -8
  43. package/oxe/workflows/retro.md +30 -5
  44. package/oxe/workflows/scan.md +1 -0
  45. package/oxe/workflows/spec.md +65 -48
  46. package/oxe/workflows/verify.md +52 -37
  47. package/package.json +2 -2
@@ -20,7 +20,10 @@ function resolveInstallOptionsFromConfig(projectRoot, optsIn) {
20
20
  if (!fs.existsSync(projectRoot)) return { options: opts, warnings };
21
21
 
22
22
  const { config, parseError } = health.loadOxeConfigMerged(projectRoot);
23
- if (parseError) return { options: opts, warnings };
23
+ if (parseError) {
24
+ warnings.push(`config.json ignorado (parse error): ${parseError}`);
25
+ return { options: opts, warnings };
26
+ }
24
27
 
25
28
  const inst = config.install;
26
29
  if (!inst || typeof inst !== 'object' || Array.isArray(inst)) return { options: opts, warnings };
@@ -0,0 +1,670 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ const VALID_RUN_STATUSES = new Set([
8
+ 'planned',
9
+ 'running',
10
+ 'paused',
11
+ 'waiting_approval',
12
+ 'failed',
13
+ 'completed',
14
+ 'replaying',
15
+ 'aborted',
16
+ ]);
17
+
18
+ const VALID_APPROVAL_POLICIES = new Set([
19
+ 'always_allow',
20
+ 'require_approval',
21
+ 'require_approval_if_external_side_effect',
22
+ 'deny_unless_overridden',
23
+ ]);
24
+
25
+ const VALID_CAPABILITY_TYPES = new Set(['script', 'mcp', 'automation', 'local']);
26
+
27
+ function readTextIfExists(filePath) {
28
+ try {
29
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function ensureDir(dirPath) {
36
+ fs.mkdirSync(dirPath, { recursive: true });
37
+ }
38
+
39
+ function ensureDirForFile(filePath) {
40
+ ensureDir(path.dirname(filePath));
41
+ }
42
+
43
+ function parseFrontmatter(text) {
44
+ const match = String(text || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
45
+ if (!match) return {};
46
+ const out = {};
47
+ for (const line of match[1].split(/\r?\n/)) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith('#')) continue;
50
+ const idx = trimmed.indexOf(':');
51
+ if (idx === -1) continue;
52
+ const key = trimmed.slice(0, idx).trim();
53
+ const value = trimmed.slice(idx + 1).trim();
54
+ out[key] = value;
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function parseArrayField(value) {
60
+ const raw = String(value || '').trim();
61
+ if (!raw) return [];
62
+ if (raw === '[]') return [];
63
+ if (/^\[.*\]$/.test(raw)) {
64
+ return raw
65
+ .slice(1, -1)
66
+ .split(',')
67
+ .map((item) => item.trim().replace(/^['"`]|['"`]$/g, ''))
68
+ .filter(Boolean);
69
+ }
70
+ return raw.split(',').map((item) => item.trim()).filter(Boolean);
71
+ }
72
+
73
+ function operationalPaths(projectRoot, activeSession) {
74
+ const oxeDir = path.join(projectRoot, '.oxe');
75
+ const scopeRoot = activeSession ? path.join(oxeDir, ...String(activeSession).split('/')) : oxeDir;
76
+ const executionRoot = activeSession ? path.join(scopeRoot, 'execution') : oxeDir;
77
+ const runsDir = path.join(executionRoot, 'runs');
78
+ return {
79
+ oxeDir,
80
+ scopeRoot,
81
+ executionRoot,
82
+ runsDir,
83
+ events: path.join(executionRoot, 'OXE-EVENTS.ndjson'),
84
+ activeRun: path.join(executionRoot, 'ACTIVE-RUN.json'),
85
+ projectLessons: path.join(oxeDir, 'global', 'LESSONS.md'),
86
+ sessionManifest: activeSession ? path.join(scopeRoot, 'SESSION.md') : null,
87
+ verify: activeSession ? path.join(scopeRoot, 'verification', 'VERIFY.md') : path.join(oxeDir, 'VERIFY.md'),
88
+ investigations: activeSession ? path.join(scopeRoot, 'research', 'INVESTIGATIONS.md') : path.join(oxeDir, 'INVESTIGATIONS.md'),
89
+ capabilitiesDir: path.join(oxeDir, 'capabilities'),
90
+ capabilitiesIndex: path.join(oxeDir, 'CAPABILITIES.md'),
91
+ checkpoints: activeSession ? path.join(scopeRoot, 'execution', 'CHECKPOINTS.md') : path.join(oxeDir, 'CHECKPOINTS.md'),
92
+ };
93
+ }
94
+
95
+ function makeRunId() {
96
+ return `oxe-run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
97
+ }
98
+
99
+ function dedupeNodes(nodes) {
100
+ const byId = new Map();
101
+ for (const node of nodes || []) {
102
+ if (!node || !node.id) continue;
103
+ byId.set(node.id, { ...byId.get(node.id), ...node });
104
+ }
105
+ return Array.from(byId.values());
106
+ }
107
+
108
+ function dedupeEdges(edges) {
109
+ const byKey = new Map();
110
+ for (const edge of edges || []) {
111
+ if (!edge || !edge.from || !edge.to) continue;
112
+ const key = [edge.from, edge.to, edge.type || 'link', edge.label || ''].join('::');
113
+ byKey.set(key, { ...byKey.get(key), ...edge });
114
+ }
115
+ return Array.from(byKey.values());
116
+ }
117
+
118
+ function buildOperationalGraph(runState = {}) {
119
+ const currentWave = runState.current_wave == null ? null : Number(runState.current_wave);
120
+ const activeTasks = Array.isArray(runState.active_tasks) ? runState.active_tasks.map(String) : [];
121
+ const pendingCheckpoints = Array.isArray(runState.pending_checkpoints) ? runState.pending_checkpoints.map(String) : [];
122
+ const azureContext = runState.provider_context && runState.provider_context.azure && typeof runState.provider_context.azure === 'object'
123
+ ? runState.provider_context.azure
124
+ : null;
125
+ const baseNodes = (((runState.graph || {}).nodes) || []).map((node) => ({ ...node }));
126
+ const baseEdges = (((runState.graph || {}).edges) || []).map((edge) => ({ ...edge }));
127
+ const generatedNodes = [
128
+ {
129
+ id: `run:${runState.run_id || 'active'}`,
130
+ label: runState.run_id || 'active-run',
131
+ kind: 'run',
132
+ status: runState.status || 'planned',
133
+ detail: runState.cursor && runState.cursor.mode ? `mode:${runState.cursor.mode}` : 'run ativo',
134
+ },
135
+ {
136
+ id: 'agent:main-executor',
137
+ label: 'main-executor',
138
+ kind: 'agent',
139
+ status: /running|replaying/.test(String(runState.status || '')) ? 'active' : 'planned',
140
+ detail: activeTasks.length ? activeTasks.join(', ') : 'sem tarefas ativas',
141
+ },
142
+ ];
143
+ const generatedEdges = [
144
+ {
145
+ from: `run:${runState.run_id || 'active'}`,
146
+ to: 'agent:main-executor',
147
+ type: 'handoff',
148
+ status: /running|replaying/.test(String(runState.status || '')) ? 'active' : 'planned',
149
+ reason: 'orquestração principal',
150
+ },
151
+ ];
152
+ if (currentWave != null) {
153
+ generatedNodes.push({
154
+ id: `wave:${currentWave}`,
155
+ label: `wave-${currentWave}`,
156
+ kind: 'wave',
157
+ status: runState.status || 'planned',
158
+ detail: activeTasks.length ? `${activeTasks.length} tarefa(s)` : 'sem tarefas ativas',
159
+ });
160
+ generatedEdges.push({
161
+ from: `run:${runState.run_id || 'active'}`,
162
+ to: `wave:${currentWave}`,
163
+ type: 'contains',
164
+ status: 'done',
165
+ });
166
+ }
167
+ for (const taskId of activeTasks) {
168
+ generatedNodes.push({
169
+ id: `task:${taskId}`,
170
+ label: taskId,
171
+ kind: 'task',
172
+ status: /paused|waiting_approval/.test(String(runState.status || '')) ? 'blocked' : 'active',
173
+ detail: currentWave != null ? `wave ${currentWave}` : 'task ativa',
174
+ });
175
+ generatedEdges.push({
176
+ from: currentWave != null ? `wave:${currentWave}` : 'agent:main-executor',
177
+ to: `task:${taskId}`,
178
+ type: 'executes',
179
+ status: /running|replaying/.test(String(runState.status || '')) ? 'active' : 'planned',
180
+ });
181
+ }
182
+ for (const checkpointId of pendingCheckpoints) {
183
+ generatedNodes.push({
184
+ id: `checkpoint:${checkpointId}`,
185
+ label: checkpointId,
186
+ kind: 'checkpoint',
187
+ status: 'pending_approval',
188
+ detail: 'aprovação pendente',
189
+ });
190
+ generatedEdges.push({
191
+ from: activeTasks.length ? `task:${activeTasks[activeTasks.length - 1]}` : (currentWave != null ? `wave:${currentWave}` : `run:${runState.run_id || 'active'}`),
192
+ to: `checkpoint:${checkpointId}`,
193
+ type: 'gate',
194
+ status: 'blocked',
195
+ reason: 'checkpoint pendente',
196
+ });
197
+ }
198
+ if (azureContext) {
199
+ const azureStatus = azureContext.login_active
200
+ ? (azureContext.pending_approval_count > 0 ? 'blocked' : 'active')
201
+ : 'warning';
202
+ generatedNodes.push({
203
+ id: 'provider:azure',
204
+ label: 'azure',
205
+ kind: 'provider',
206
+ status: azureStatus,
207
+ detail: azureContext.subscription_name || azureContext.subscription_id || azureContext.cloud || 'contexto azure',
208
+ });
209
+ generatedEdges.push({
210
+ from: `run:${runState.run_id || 'active'}`,
211
+ to: 'provider:azure',
212
+ type: 'provider',
213
+ status: azureStatus,
214
+ reason: 'contexto cloud ativo',
215
+ });
216
+ const lastOperation = azureContext.last_operation && typeof azureContext.last_operation === 'object'
217
+ ? azureContext.last_operation
218
+ : null;
219
+ if (lastOperation) {
220
+ const capabilityId = String(lastOperation.capability_id || lastOperation.domain || 'azure-operation');
221
+ generatedNodes.push({
222
+ id: `capability:${capabilityId}`,
223
+ label: capabilityId,
224
+ kind: 'capability',
225
+ status: String(lastOperation.phase || lastOperation.status || 'planned'),
226
+ detail: lastOperation.command_display || lastOperation.operation || 'operação Azure',
227
+ });
228
+ generatedEdges.push({
229
+ from: 'provider:azure',
230
+ to: `capability:${capabilityId}`,
231
+ type: 'tool_call',
232
+ status: String(lastOperation.phase || lastOperation.status || 'planned'),
233
+ reason: lastOperation.operation || lastOperation.domain || 'ação Azure',
234
+ });
235
+ const refs = Array.isArray(lastOperation.resource_refs) ? lastOperation.resource_refs : [];
236
+ for (let index = 0; index < refs.length; index += 1) {
237
+ const ref = refs[index];
238
+ const normalized = ref && typeof ref === 'object' ? ref : { name: String(ref || 'resource') };
239
+ const resourceId = normalized.id || normalized.name || normalized.resource_id || normalized.type || `resource-${index + 1}`;
240
+ generatedNodes.push({
241
+ id: `resource:${resourceId}`,
242
+ label: normalized.name || normalized.type || resourceId,
243
+ kind: 'resource',
244
+ status: String(lastOperation.phase || lastOperation.status || 'planned'),
245
+ detail: [normalized.type, normalized.resource_group, normalized.location].filter(Boolean).join(' · ') || azureContext.subscription_name || 'recurso Azure',
246
+ });
247
+ generatedEdges.push({
248
+ from: `capability:${capabilityId}`,
249
+ to: `resource:${resourceId}`,
250
+ type: 'targets',
251
+ status: String(lastOperation.phase || lastOperation.status || 'planned'),
252
+ reason: normalized.scope || normalized.kind || 'recurso alvo',
253
+ });
254
+ }
255
+ if (lastOperation.pending_checkpoint_id) {
256
+ generatedNodes.push({
257
+ id: `checkpoint:${lastOperation.pending_checkpoint_id}`,
258
+ label: String(lastOperation.pending_checkpoint_id),
259
+ kind: 'checkpoint',
260
+ status: 'pending_approval',
261
+ detail: 'gate Azure pendente',
262
+ });
263
+ generatedEdges.push({
264
+ from: `capability:${capabilityId}`,
265
+ to: `checkpoint:${lastOperation.pending_checkpoint_id}`,
266
+ type: 'gate',
267
+ status: 'blocked',
268
+ reason: 'approval policy do provider Azure',
269
+ });
270
+ }
271
+ }
272
+ }
273
+ return {
274
+ nodes: dedupeNodes([...baseNodes, ...generatedNodes]),
275
+ edges: dedupeEdges([...baseEdges, ...generatedEdges]),
276
+ };
277
+ }
278
+
279
+ function appendEvent(projectRoot, activeSession, event = {}) {
280
+ const p = operationalPaths(projectRoot, activeSession);
281
+ ensureDirForFile(p.events);
282
+ const entry = {
283
+ event_id: event.event_id || `evt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
284
+ type: String(event.type || 'custom'),
285
+ timestamp: event.timestamp || new Date().toISOString(),
286
+ run_id: event.run_id || null,
287
+ session_id: activeSession || null,
288
+ wave_id: event.wave_id || null,
289
+ task_id: event.task_id || null,
290
+ agent_id: event.agent_id || null,
291
+ payload: event.payload && typeof event.payload === 'object' ? event.payload : {},
292
+ };
293
+ fs.appendFileSync(p.events, `${JSON.stringify(entry)}\n`, 'utf8');
294
+ return entry;
295
+ }
296
+
297
+ function readEvents(projectRoot, activeSession) {
298
+ const p = operationalPaths(projectRoot, activeSession);
299
+ const raw = readTextIfExists(p.events) || '';
300
+ return raw
301
+ .split(/\r?\n/)
302
+ .map((line) => line.trim())
303
+ .filter(Boolean)
304
+ .map((line) => {
305
+ try {
306
+ return JSON.parse(line);
307
+ } catch {
308
+ return null;
309
+ }
310
+ })
311
+ .filter(Boolean);
312
+ }
313
+
314
+ function summarizeEvents(events) {
315
+ const byType = {};
316
+ for (const event of events) {
317
+ const key = String(event.type || 'custom');
318
+ byType[key] = (byType[key] || 0) + 1;
319
+ }
320
+ return {
321
+ total: events.length,
322
+ byType,
323
+ lastEvent: events.length ? events[events.length - 1] : null,
324
+ };
325
+ }
326
+
327
+ function writeRunState(projectRoot, activeSession, runState = {}) {
328
+ const p = operationalPaths(projectRoot, activeSession);
329
+ const runId = String(runState.run_id || makeRunId());
330
+ const payload = {
331
+ run_id: runId,
332
+ status: VALID_RUN_STATUSES.has(String(runState.status || 'planned')) ? String(runState.status || 'planned') : 'planned',
333
+ created_at: runState.created_at || new Date().toISOString(),
334
+ updated_at: runState.updated_at || new Date().toISOString(),
335
+ plan_ref: runState.plan_ref || 'PLAN.md',
336
+ session_id: activeSession || null,
337
+ current_wave: runState.current_wave == null ? null : Number(runState.current_wave),
338
+ cursor: {
339
+ wave: runState.cursor && runState.cursor.wave != null ? Number(runState.cursor.wave) : null,
340
+ task: runState.cursor && runState.cursor.task ? String(runState.cursor.task) : null,
341
+ mode: runState.cursor && runState.cursor.mode ? String(runState.cursor.mode) : null,
342
+ },
343
+ active_tasks: Array.isArray(runState.active_tasks) ? runState.active_tasks.map(String) : [],
344
+ pending_checkpoints: Array.isArray(runState.pending_checkpoints) ? runState.pending_checkpoints.map(String) : [],
345
+ retries: Array.isArray(runState.retries) ? runState.retries : [],
346
+ failures: Array.isArray(runState.failures) ? runState.failures : [],
347
+ evidence: Array.isArray(runState.evidence) ? runState.evidence : [],
348
+ provider_context: runState.provider_context && typeof runState.provider_context === 'object'
349
+ ? runState.provider_context
350
+ : {},
351
+ graph: runState.graph && typeof runState.graph === 'object'
352
+ ? {
353
+ nodes: Array.isArray(runState.graph.nodes) ? runState.graph.nodes : [],
354
+ edges: Array.isArray(runState.graph.edges) ? runState.graph.edges : [],
355
+ }
356
+ : { nodes: [], edges: [] },
357
+ metrics: runState.metrics && typeof runState.metrics === 'object' ? runState.metrics : {},
358
+ };
359
+ payload.graph = buildOperationalGraph(payload);
360
+ ensureDir(p.runsDir);
361
+ fs.writeFileSync(path.join(p.runsDir, `${runId}.json`), JSON.stringify(payload, null, 2), 'utf8');
362
+ fs.writeFileSync(
363
+ p.activeRun,
364
+ JSON.stringify(
365
+ {
366
+ run_id: runId,
367
+ status: payload.status,
368
+ updated_at: payload.updated_at,
369
+ current_wave: payload.current_wave,
370
+ cursor: payload.cursor,
371
+ provider_context: payload.provider_context,
372
+ },
373
+ null,
374
+ 2
375
+ ),
376
+ 'utf8'
377
+ );
378
+ return payload;
379
+ }
380
+
381
+ function readRunState(projectRoot, activeSession) {
382
+ const p = operationalPaths(projectRoot, activeSession);
383
+ const activeRaw = readTextIfExists(p.activeRun);
384
+ let runId = null;
385
+ if (activeRaw) {
386
+ try {
387
+ runId = JSON.parse(activeRaw).run_id || null;
388
+ } catch {
389
+ runId = null;
390
+ }
391
+ }
392
+ if (!runId && fs.existsSync(p.runsDir)) {
393
+ const files = fs.readdirSync(p.runsDir).filter((name) => name.endsWith('.json')).sort();
394
+ if (files.length) runId = files[files.length - 1].replace(/\.json$/i, '');
395
+ }
396
+ if (!runId) return null;
397
+ const filePath = path.join(p.runsDir, `${runId}.json`);
398
+ const raw = readTextIfExists(filePath);
399
+ if (!raw) return null;
400
+ try {
401
+ return JSON.parse(raw);
402
+ } catch {
403
+ return null;
404
+ }
405
+ }
406
+
407
+ function readGitActivity(projectRoot, since) {
408
+ if (!projectRoot || !since) return [];
409
+ try {
410
+ const result = spawnSync(
411
+ 'git',
412
+ ['log', `--since=${since}`, '--pretty=format:%H %aI'],
413
+ { cwd: projectRoot, encoding: 'utf8', timeout: 5000 }
414
+ );
415
+ if (result.status !== 0 || result.error || !result.stdout) return [];
416
+ return result.stdout
417
+ .split(/\r?\n/)
418
+ .map((line) => line.trim())
419
+ .filter(Boolean)
420
+ .map((line) => {
421
+ const idx = line.indexOf(' ');
422
+ return {
423
+ hash: idx !== -1 ? line.slice(0, idx) : line,
424
+ timestamp: idx !== -1 ? line.slice(idx + 1) : '',
425
+ };
426
+ });
427
+ } catch {
428
+ return [];
429
+ }
430
+ }
431
+
432
+ function verifyGitEvidence(runState, projectRoot) {
433
+ const warns = [];
434
+ if (!runState || !projectRoot || !runState.created_at) return warns;
435
+ const commits = readGitActivity(projectRoot, runState.created_at);
436
+ if (
437
+ commits.length === 0 &&
438
+ (String(runState.status || '') === 'completed' ||
439
+ (Array.isArray(runState.evidence) && runState.evidence.length > 0))
440
+ ) {
441
+ warns.push('Nenhum commit git encontrado desde o início do run — confirme se o trabalho foi commitado');
442
+ }
443
+ return warns;
444
+ }
445
+
446
+ function runtimeStateWarnings(runState, checkpoints = [], projectRoot = null) {
447
+ const warns = [];
448
+ if (!runState) return warns;
449
+ if (!VALID_RUN_STATUSES.has(String(runState.status || ''))) {
450
+ warns.push(`ACTIVE-RUN inválido: status "${runState.status}" fora do contrato`);
451
+ }
452
+ const cursorWave = runState.cursor && runState.cursor.wave != null ? Number(runState.cursor.wave) : null;
453
+ if (cursorWave != null && runState.current_wave != null && Number(cursorWave) !== Number(runState.current_wave)) {
454
+ warns.push('ACTIVE-RUN com cursor de onda divergente do current_wave');
455
+ }
456
+ const pending = new Set((runState.pending_checkpoints || []).map(String));
457
+ for (const cp of checkpoints) {
458
+ if (/pending_approval/i.test(String(cp.status || '')) && !pending.has(String(cp.id))) {
459
+ warns.push(`Checkpoint ${cp.id} pendente no índice, mas ausente do ACTIVE-RUN`);
460
+ }
461
+ }
462
+ for (const edge of (((runState.graph || {}).edges) || [])) {
463
+ if (edge.type === 'handoff' && (!edge.from || !edge.to)) {
464
+ warns.push('Grafo operacional contém handoff sem origem ou destino');
465
+ break;
466
+ }
467
+ }
468
+ if (projectRoot) {
469
+ for (const w of verifyGitEvidence(runState, projectRoot)) {
470
+ warns.push(w);
471
+ }
472
+ }
473
+ return warns;
474
+ }
475
+
476
+ function parseCapabilityManifest(text) {
477
+ const fm = parseFrontmatter(text);
478
+ const approval = String(fm.approval_policy || fm.policy || '').trim() || null;
479
+ const sideEffects = parseArrayField(fm.side_effects || '');
480
+ const envs = parseArrayField(fm.requires_env || '');
481
+ const evidence = parseArrayField(fm.evidence_outputs || '');
482
+ const sessionCompat = parseArrayField(fm.session_compatibility || fm.session_scope || '');
483
+ const objectiveMatch = String(text || '').match(/##\s*Objetivo\s*\n+([\s\S]*?)(?=\n##\s|\n#[^\#]|$)/i);
484
+ const desc = objectiveMatch ? objectiveMatch[1].split(/\r?\n/).map((line) => line.replace(/^-\s+/, '').trim()).filter(Boolean)[0] || '' : '';
485
+ return {
486
+ id: String(fm.id || '').trim(),
487
+ version: String(fm.version || '').trim() || '1',
488
+ type: String(fm.type || 'local').trim(),
489
+ status: String(fm.status || 'active').trim(),
490
+ scope: String(fm.scope || 'mixed').trim(),
491
+ entrypoint: String(fm.entrypoint || '').trim(),
492
+ approvalPolicy: approval,
493
+ sideEffects,
494
+ requiresEnv: envs,
495
+ evidenceOutputs: evidence,
496
+ sessionCompatibility: sessionCompat,
497
+ description: desc || 'Capability local do projeto',
498
+ };
499
+ }
500
+
501
+ function readCapabilityCatalog(projectRoot) {
502
+ const p = operationalPaths(projectRoot, null);
503
+ if (!fs.existsSync(p.capabilitiesDir)) return [];
504
+ return fs
505
+ .readdirSync(p.capabilitiesDir, { withFileTypes: true })
506
+ .filter((entry) => entry.isDirectory())
507
+ .map((entry) => {
508
+ const manifestPath = path.join(p.capabilitiesDir, entry.name, 'CAPABILITY.md');
509
+ const raw = readTextIfExists(manifestPath);
510
+ if (!raw) return null;
511
+ return { ...parseCapabilityManifest(raw), manifestPath };
512
+ })
513
+ .filter(Boolean)
514
+ .sort((a, b) => a.id.localeCompare(b.id));
515
+ }
516
+
517
+ function capabilityCatalogWarnings(projectRoot) {
518
+ const warns = [];
519
+ for (const cap of readCapabilityCatalog(projectRoot)) {
520
+ if (!cap.id) warns.push(`Capability em ${cap.manifestPath} sem id`);
521
+ if (!VALID_CAPABILITY_TYPES.has(cap.type)) warns.push(`Capability ${cap.id || cap.manifestPath}: type "${cap.type}" inválido`);
522
+ if (!cap.approvalPolicy || !VALID_APPROVAL_POLICIES.has(cap.approvalPolicy)) {
523
+ warns.push(`Capability ${cap.id || cap.manifestPath}: approval_policy ausente ou inválida`);
524
+ }
525
+ if (!cap.entrypoint) warns.push(`Capability ${cap.id || cap.manifestPath}: entrypoint ausente`);
526
+ if (!cap.evidenceOutputs.length) warns.push(`Capability ${cap.id || cap.manifestPath}: evidence_outputs ausente`);
527
+ }
528
+ return warns;
529
+ }
530
+
531
+ function buildMemoryLayers(projectRoot, activeSession) {
532
+ const p = operationalPaths(projectRoot, activeSession);
533
+ return {
534
+ readOrder: ['runtime_state', 'session_memory', 'project_memory', 'evidence'],
535
+ runtime_state: {
536
+ source: activeSession ? path.join('.oxe', activeSession, 'execution', 'STATE.md') : '.oxe/STATE.md + ACTIVE-RUN.json',
537
+ exists: fs.existsSync(activeSession ? path.join(projectRoot, '.oxe', activeSession, 'execution', 'STATE.md') : path.join(projectRoot, '.oxe', 'STATE.md')),
538
+ },
539
+ session_memory: {
540
+ source: activeSession ? path.join('.oxe', activeSession, 'SESSION.md') : null,
541
+ exists: Boolean(p.sessionManifest && fs.existsSync(p.sessionManifest)),
542
+ },
543
+ project_memory: {
544
+ source: '.oxe/global/LESSONS.md',
545
+ exists: fs.existsSync(p.projectLessons),
546
+ },
547
+ evidence: {
548
+ source: [
549
+ activeSession ? path.join('.oxe', activeSession, 'research', 'INVESTIGATIONS.md') : '.oxe/INVESTIGATIONS.md',
550
+ activeSession ? path.join('.oxe', activeSession, 'verification', 'VERIFY.md') : '.oxe/VERIFY.md',
551
+ ],
552
+ exists: fs.existsSync(p.investigations) || fs.existsSync(p.verify),
553
+ },
554
+ };
555
+ }
556
+
557
+ function applyRuntimeAction(projectRoot, activeSession, input = {}) {
558
+ const action = String(input.action || 'status').toLowerCase();
559
+ const now = new Date().toISOString();
560
+ const current = readRunState(projectRoot, activeSession);
561
+ const wave = input.wave == null || input.wave === '' ? null : Number(input.wave);
562
+ const task = input.task ? String(input.task) : null;
563
+ const mode = input.mode ? String(input.mode) : (task ? 'task' : wave != null ? 'wave' : 'complete');
564
+ const pendingCheckpoints = Array.isArray(input.pending_checkpoints)
565
+ ? input.pending_checkpoints.map(String)
566
+ : current && Array.isArray(current.pending_checkpoints)
567
+ ? current.pending_checkpoints
568
+ : [];
569
+
570
+ if (action === 'start') {
571
+ const next = writeRunState(projectRoot, activeSession, {
572
+ run_id: input.run_id || makeRunId(),
573
+ status: 'running',
574
+ created_at: now,
575
+ updated_at: now,
576
+ plan_ref: input.plan_ref || 'PLAN.md',
577
+ current_wave: wave,
578
+ cursor: { wave, task, mode },
579
+ active_tasks: task ? [task] : Array.isArray(input.active_tasks) ? input.active_tasks.map(String) : [],
580
+ pending_checkpoints: pendingCheckpoints,
581
+ retries: [],
582
+ failures: [],
583
+ evidence: [],
584
+ metrics: {
585
+ transitions: 1,
586
+ pause_count: 0,
587
+ replay_count: 0,
588
+ last_action: 'start',
589
+ },
590
+ });
591
+ appendEvent(projectRoot, activeSession, {
592
+ type: 'run_started',
593
+ run_id: next.run_id,
594
+ wave_id: wave != null ? `wave-${wave}` : null,
595
+ task_id: task,
596
+ payload: { reason: input.reason || 'run inicializado', mode },
597
+ });
598
+ return next;
599
+ }
600
+
601
+ if (!current) {
602
+ throw new Error('Nenhum ACTIVE-RUN disponível para esta ação.');
603
+ }
604
+
605
+ const next = {
606
+ ...current,
607
+ updated_at: now,
608
+ current_wave: wave != null ? wave : current.current_wave,
609
+ cursor: {
610
+ wave: wave != null ? wave : current.cursor && current.cursor.wave != null ? current.cursor.wave : current.current_wave,
611
+ task: task || (current.cursor ? current.cursor.task : null),
612
+ mode: input.mode ? String(input.mode) : (current.cursor && current.cursor.mode) || mode,
613
+ },
614
+ active_tasks: task ? [task] : Array.isArray(input.active_tasks) ? input.active_tasks.map(String) : current.active_tasks || [],
615
+ pending_checkpoints: pendingCheckpoints,
616
+ metrics: {
617
+ ...(current.metrics || {}),
618
+ transitions: Number((current.metrics || {}).transitions || 0) + 1,
619
+ last_action: action,
620
+ },
621
+ };
622
+
623
+ if (action === 'pause') {
624
+ next.status = 'paused';
625
+ next.metrics.pause_count = Number((current.metrics || {}).pause_count || 0) + 1;
626
+ } else if (action === 'resume') {
627
+ next.status = pendingCheckpoints.length ? 'waiting_approval' : 'running';
628
+ } else if (action === 'replay') {
629
+ next.status = 'replaying';
630
+ next.metrics.replay_count = Number((current.metrics || {}).replay_count || 0) + 1;
631
+ } else {
632
+ throw new Error(`Ação de runtime desconhecida: ${action}`);
633
+ }
634
+
635
+ const saved = writeRunState(projectRoot, activeSession, next);
636
+ appendEvent(projectRoot, activeSession, {
637
+ type: action === 'pause' ? 'run_paused' : action === 'resume' ? 'run_resumed' : 'run_replay_requested',
638
+ run_id: saved.run_id,
639
+ wave_id: saved.current_wave != null ? `wave-${saved.current_wave}` : null,
640
+ task_id: saved.cursor && saved.cursor.task ? saved.cursor.task : null,
641
+ payload: {
642
+ reason: input.reason || '',
643
+ mode: saved.cursor && saved.cursor.mode ? saved.cursor.mode : null,
644
+ pending_checkpoints: saved.pending_checkpoints || [],
645
+ },
646
+ });
647
+ return saved;
648
+ }
649
+
650
+ module.exports = {
651
+ VALID_RUN_STATUSES,
652
+ VALID_APPROVAL_POLICIES,
653
+ VALID_CAPABILITY_TYPES,
654
+ operationalPaths,
655
+ makeRunId,
656
+ appendEvent,
657
+ readEvents,
658
+ summarizeEvents,
659
+ writeRunState,
660
+ readRunState,
661
+ readGitActivity,
662
+ verifyGitEvidence,
663
+ runtimeStateWarnings,
664
+ parseCapabilityManifest,
665
+ readCapabilityCatalog,
666
+ capabilityCatalogWarnings,
667
+ buildMemoryLayers,
668
+ buildOperationalGraph,
669
+ applyRuntimeAction,
670
+ };