oxe-cc 0.3.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/commands/oxe-execute.md +2 -2
- package/.cursor/commands/oxe-loop.md +11 -0
- package/.cursor/commands/oxe-milestone.md +11 -0
- package/.cursor/commands/oxe-obs.md +11 -0
- package/.cursor/commands/oxe-project.md +11 -0
- package/.cursor/commands/oxe-quick.md +2 -2
- package/.cursor/commands/oxe-security.md +11 -0
- package/.cursor/commands/oxe-spec.md +2 -2
- package/.cursor/commands/oxe-workstream.md +11 -0
- package/.cursor/commands/oxe.md +9 -0
- package/.github/prompts/oxe-execute.prompt.md +12 -12
- package/.github/prompts/oxe-loop.prompt.md +12 -0
- package/.github/prompts/oxe-milestone.prompt.md +12 -0
- package/.github/prompts/oxe-obs.prompt.md +12 -0
- package/.github/prompts/oxe-project.prompt.md +12 -0
- package/.github/prompts/oxe-quick.prompt.md +12 -12
- package/.github/prompts/oxe-security.prompt.md +12 -0
- package/.github/prompts/oxe-spec.prompt.md +2 -2
- package/.github/prompts/oxe-workstream.prompt.md +12 -0
- package/.github/prompts/oxe.prompt.md +12 -0
- package/README.md +287 -544
- package/bin/banner.txt +1 -1
- package/bin/lib/oxe-plugins.cjs +226 -0
- package/bin/lib/oxe-project-health.cjs +97 -1
- package/bin/lib/oxe-security.cjs +225 -0
- package/bin/oxe-cc.js +30 -1
- package/commands/oxe/execute.md +16 -16
- package/commands/oxe/loop.md +17 -0
- package/commands/oxe/milestone.md +16 -0
- package/commands/oxe/obs.md +16 -0
- package/commands/oxe/oxe.md +16 -0
- package/commands/oxe/project.md +16 -0
- package/commands/oxe/quick.md +16 -16
- package/commands/oxe/security.md +16 -0
- package/commands/oxe/spec.md +2 -2
- package/commands/oxe/workstream.md +16 -0
- package/lib/sdk/index.cjs +284 -0
- package/lib/sdk/index.d.ts +148 -1
- package/oxe/personas/README.md +39 -0
- package/oxe/personas/architect.md +37 -0
- package/oxe/personas/db-specialist.md +36 -0
- package/oxe/personas/debugger.md +38 -0
- package/oxe/personas/executor.md +38 -0
- package/oxe/personas/planner.md +36 -0
- package/oxe/personas/researcher.md +35 -0
- package/oxe/personas/ui-specialist.md +36 -0
- package/oxe/personas/verifier.md +39 -0
- package/oxe/templates/CONFIG.md +54 -4
- package/oxe/templates/DISCUSS.template.md +44 -0
- package/oxe/templates/MEMORY.template.md +49 -0
- package/oxe/templates/MILESTONES.template.md +17 -0
- package/oxe/templates/OBSERVATIONS.template.md +18 -0
- package/oxe/templates/PLUGINS.md +101 -0
- package/oxe/templates/ROADMAP.template.md +44 -0
- package/oxe/templates/SECURITY.template.md +72 -0
- package/oxe/templates/STATE.md +29 -2
- package/oxe/templates/config.template.json +5 -0
- package/oxe/templates/plan-agents.template.json +3 -2
- package/oxe/templates/quick-agents.template.json +24 -0
- package/oxe/workflows/discuss.md +48 -5
- package/oxe/workflows/execute.md +133 -28
- package/oxe/workflows/help.md +105 -24
- package/oxe/workflows/loop.md +57 -0
- package/oxe/workflows/milestone.md +96 -0
- package/oxe/workflows/obs.md +95 -0
- package/oxe/workflows/oxe.md +115 -0
- package/oxe/workflows/plan-agent.md +50 -3
- package/oxe/workflows/plan.md +7 -2
- package/oxe/workflows/project.md +50 -0
- package/oxe/workflows/quick.md +120 -10
- package/oxe/workflows/research.md +16 -0
- package/oxe/workflows/scan.md +23 -1
- package/oxe/workflows/security.md +61 -0
- package/oxe/workflows/spec.md +172 -23
- package/oxe/workflows/verify.md +80 -18
- package/oxe/workflows/workstream.md +96 -0
- package/package.json +3 -2
package/bin/banner.txt
CHANGED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OXE Plugin System — carrega e executa plugins de `.oxe/plugins/`.
|
|
5
|
+
*
|
|
6
|
+
* Plugins são módulos CJS em `.oxe/plugins/*.cjs` que exportam hooks do ciclo de vida OXE.
|
|
7
|
+
* Cada hook é uma função assíncrona que recebe o contexto do evento.
|
|
8
|
+
*
|
|
9
|
+
* Hooks disponíveis:
|
|
10
|
+
* - onBeforeScan(ctx) — antes do workflow scan iniciar
|
|
11
|
+
* - onAfterScan(ctx) — após o scan produzir os 7 mapas
|
|
12
|
+
* - onBeforeSpec(ctx) — antes do workflow spec
|
|
13
|
+
* - onAfterSpec(ctx) — após SPEC.md ser gerado
|
|
14
|
+
* - onBeforePlan(ctx) — antes do workflow plan
|
|
15
|
+
* - onAfterPlan(ctx) — após PLAN.md ser gerado
|
|
16
|
+
* - onPlanGenerated(ctx) — alias de onAfterPlan
|
|
17
|
+
* - onBeforeExecute(ctx) — antes de iniciar uma onda
|
|
18
|
+
* - onAfterExecute(ctx) — após onda concluída
|
|
19
|
+
* - onBeforeVerify(ctx) — antes do workflow verify
|
|
20
|
+
* - onAfterVerify(ctx) — após VERIFY.md ser gerado
|
|
21
|
+
* - onVerifyComplete(ctx) — quando verify_complete
|
|
22
|
+
* - onVerifyFailed(ctx) — quando verify_failed
|
|
23
|
+
* - onMilestoneNew(ctx) — novo milestone criado
|
|
24
|
+
* - onMilestoneComplete(ctx)— milestone encerrado
|
|
25
|
+
* - onWorkstreamNew(ctx) — novo workstream criado
|
|
26
|
+
*
|
|
27
|
+
* Exemplo de plugin (`.oxe/plugins/notify.cjs`):
|
|
28
|
+
* ```js
|
|
29
|
+
* module.exports = {
|
|
30
|
+
* name: 'notify',
|
|
31
|
+
* version: '1.0.0',
|
|
32
|
+
* hooks: {
|
|
33
|
+
* async onAfterVerify({ projectRoot, result }) {
|
|
34
|
+
* if (result === 'verify_complete') {
|
|
35
|
+
* console.log('[notify] Verificação concluída! 🎉');
|
|
36
|
+
* }
|
|
37
|
+
* },
|
|
38
|
+
* },
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const path = require('path');
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {{
|
|
48
|
+
* name: string,
|
|
49
|
+
* version?: string,
|
|
50
|
+
* hooks: Record<string, (ctx: Record<string, unknown>) => Promise<void> | void>,
|
|
51
|
+
* }} OxePlugin
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Carrega todos os plugins de `.oxe/plugins/`.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} projectRoot
|
|
58
|
+
* @returns {{ plugins: OxePlugin[], errors: Array<{ file: string, error: string }> }}
|
|
59
|
+
*/
|
|
60
|
+
function loadPlugins(projectRoot) {
|
|
61
|
+
const pluginsDir = path.join(projectRoot, '.oxe', 'plugins');
|
|
62
|
+
/** @type {OxePlugin[]} */
|
|
63
|
+
const plugins = [];
|
|
64
|
+
/** @type {Array<{ file: string, error: string }>} */
|
|
65
|
+
const errors = [];
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
68
|
+
return { plugins, errors };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let files;
|
|
72
|
+
try {
|
|
73
|
+
files = fs.readdirSync(pluginsDir).filter((f) => f.endsWith('.cjs')).sort();
|
|
74
|
+
} catch (e) {
|
|
75
|
+
errors.push({ file: pluginsDir, error: String(e) });
|
|
76
|
+
return { plugins, errors };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const fullPath = path.join(pluginsDir, file);
|
|
81
|
+
try {
|
|
82
|
+
// eslint-disable-next-line no-undef
|
|
83
|
+
const mod = require(fullPath);
|
|
84
|
+
if (!mod || typeof mod !== 'object') {
|
|
85
|
+
errors.push({ file, error: 'Plugin deve exportar um objeto' });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (typeof mod.name !== 'string') {
|
|
89
|
+
errors.push({ file, error: 'Plugin deve exportar `name` (string)' });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!mod.hooks || typeof mod.hooks !== 'object') {
|
|
93
|
+
errors.push({ file, error: 'Plugin deve exportar `hooks` (objeto com funções)' });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
plugins.push(mod);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
errors.push({ file, error: String(e) });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { plugins, errors };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Executa um hook específico em todos os plugins carregados.
|
|
107
|
+
* Erros em hooks individuais são capturados e não propagam.
|
|
108
|
+
*
|
|
109
|
+
* @param {OxePlugin[]} plugins
|
|
110
|
+
* @param {string} hookName
|
|
111
|
+
* @param {Record<string, unknown>} ctx
|
|
112
|
+
* @returns {Promise<Array<{ plugin: string, error: string }>>} erros de execução (não fatais)
|
|
113
|
+
*/
|
|
114
|
+
async function runHook(plugins, hookName, ctx) {
|
|
115
|
+
/** @type {Array<{ plugin: string, error: string }>} */
|
|
116
|
+
const hookErrors = [];
|
|
117
|
+
for (const plugin of plugins) {
|
|
118
|
+
const hook = plugin.hooks[hookName];
|
|
119
|
+
if (typeof hook !== 'function') continue;
|
|
120
|
+
try {
|
|
121
|
+
await hook(ctx);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
hookErrors.push({ plugin: plugin.name, error: String(e) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return hookErrors;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Valida a estrutura de todos os plugins em `.oxe/plugins/`.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} projectRoot
|
|
133
|
+
* @returns {{ valid: boolean, issues: Array<{ file: string, issue: string }> }}
|
|
134
|
+
*/
|
|
135
|
+
function validatePlugins(projectRoot) {
|
|
136
|
+
const pluginsDir = path.join(projectRoot, '.oxe', 'plugins');
|
|
137
|
+
/** @type {Array<{ file: string, issue: string }>} */
|
|
138
|
+
const issues = [];
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
141
|
+
return { valid: true, issues };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let files;
|
|
145
|
+
try {
|
|
146
|
+
files = fs.readdirSync(pluginsDir);
|
|
147
|
+
} catch {
|
|
148
|
+
return { valid: false, issues: [{ file: pluginsDir, issue: 'Não foi possível ler o diretório de plugins' }] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cjsFiles = files.filter((f) => f.endsWith('.cjs'));
|
|
152
|
+
const nonCjs = files.filter((f) => !f.endsWith('.cjs') && !f.startsWith('.') && f !== 'README.md');
|
|
153
|
+
|
|
154
|
+
for (const f of nonCjs) {
|
|
155
|
+
issues.push({ file: f, issue: 'Plugins devem ter extensão .cjs' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { errors } = loadPlugins(projectRoot);
|
|
159
|
+
for (const e of errors) {
|
|
160
|
+
issues.push({ file: e.file, issue: e.error });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (cjsFiles.length > 20) {
|
|
164
|
+
issues.push({ file: pluginsDir, issue: `Muitos plugins (${cjsFiles.length}) — considere consolidar` });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { valid: issues.length === 0, issues };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Cria o diretório de plugins com um README se não existir.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} projectRoot
|
|
174
|
+
*/
|
|
175
|
+
function initPluginsDir(projectRoot) {
|
|
176
|
+
const pluginsDir = path.join(projectRoot, '.oxe', 'plugins');
|
|
177
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
178
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
const readme = path.join(pluginsDir, 'README.md');
|
|
181
|
+
if (!fs.existsSync(readme)) {
|
|
182
|
+
fs.writeFileSync(
|
|
183
|
+
readme,
|
|
184
|
+
`# .oxe/plugins/
|
|
185
|
+
|
|
186
|
+
Coloque aqui plugins OXE em formato \`.cjs\`.
|
|
187
|
+
|
|
188
|
+
## Estrutura mínima
|
|
189
|
+
|
|
190
|
+
\`\`\`js
|
|
191
|
+
// .oxe/plugins/meu-plugin.cjs
|
|
192
|
+
module.exports = {
|
|
193
|
+
name: 'meu-plugin',
|
|
194
|
+
version: '1.0.0',
|
|
195
|
+
hooks: {
|
|
196
|
+
async onAfterVerify({ projectRoot, result }) {
|
|
197
|
+
// result: 'verify_complete' | 'verify_failed'
|
|
198
|
+
console.log('[meu-plugin] verify:', result);
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
## Hooks disponíveis
|
|
205
|
+
|
|
206
|
+
- onBeforeScan, onAfterScan
|
|
207
|
+
- onBeforeSpec, onAfterSpec
|
|
208
|
+
- onBeforePlan, onAfterPlan, onPlanGenerated
|
|
209
|
+
- onBeforeExecute, onAfterExecute
|
|
210
|
+
- onBeforeVerify, onAfterVerify, onVerifyComplete, onVerifyFailed
|
|
211
|
+
- onMilestoneNew, onMilestoneComplete
|
|
212
|
+
- onWorkstreamNew
|
|
213
|
+
|
|
214
|
+
Ver documentação completa: \`oxe/templates/PLUGINS.md\` (no pacote npm).
|
|
215
|
+
`,
|
|
216
|
+
'utf8'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
loadPlugins,
|
|
223
|
+
runHook,
|
|
224
|
+
validatePlugins,
|
|
225
|
+
initPluginsDir,
|
|
226
|
+
};
|
|
@@ -8,6 +8,7 @@ const ALLOWED_CONFIG_KEYS = [
|
|
|
8
8
|
'discuss_before_plan',
|
|
9
9
|
'after_verify_suggest_pr',
|
|
10
10
|
'after_verify_draft_commit',
|
|
11
|
+
'after_verify_suggest_uat',
|
|
11
12
|
'default_verify_command',
|
|
12
13
|
'scan_max_age_days',
|
|
13
14
|
'compact_max_age_days',
|
|
@@ -15,9 +16,26 @@ const ALLOWED_CONFIG_KEYS = [
|
|
|
15
16
|
'scan_ignore_globs',
|
|
16
17
|
'spec_required_sections',
|
|
17
18
|
'plan_max_tasks_per_wave',
|
|
19
|
+
'profile',
|
|
20
|
+
'verification_depth',
|
|
18
21
|
'install',
|
|
22
|
+
'plugins',
|
|
23
|
+
'workstreams',
|
|
24
|
+
'milestones',
|
|
25
|
+
'scale_adaptive',
|
|
19
26
|
];
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Profiles de execução OXE que controlam rigor do workflow.
|
|
30
|
+
* Expansão de keys: profile 'strict' liga discuss_before_plan, verification_depth thorough, etc.
|
|
31
|
+
*/
|
|
32
|
+
const EXECUTION_PROFILES = ['strict', 'balanced', 'fast', 'legacy'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Profundidade de verificação.
|
|
36
|
+
*/
|
|
37
|
+
const VERIFICATION_DEPTHS = ['standard', 'thorough', 'quick'];
|
|
38
|
+
|
|
21
39
|
/** Perfis de integração lidos de `.oxe/config.json` → `install.profile` (CLI explícita prevalece). */
|
|
22
40
|
const INSTALL_PROFILES = ['recommended', 'cursor', 'copilot', 'core', 'cli', 'all_agents'];
|
|
23
41
|
|
|
@@ -37,6 +55,57 @@ const EXPECTED_CODEBASE_MAPS = [
|
|
|
37
55
|
'CONCERNS.md',
|
|
38
56
|
];
|
|
39
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} targetProject
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Expande um profile de execução nas suas keys individuais.
|
|
63
|
+
* Keys explícitas no config prevalecem sobre o profile.
|
|
64
|
+
* @param {string} profile
|
|
65
|
+
* @returns {Record<string, unknown>}
|
|
66
|
+
*/
|
|
67
|
+
function expandExecutionProfile(profile) {
|
|
68
|
+
const profiles = {
|
|
69
|
+
strict: {
|
|
70
|
+
discuss_before_plan: true,
|
|
71
|
+
verification_depth: 'thorough',
|
|
72
|
+
after_verify_suggest_uat: true,
|
|
73
|
+
after_verify_suggest_pr: true,
|
|
74
|
+
after_verify_draft_commit: true,
|
|
75
|
+
scan_max_age_days: 14,
|
|
76
|
+
compact_max_age_days: 30,
|
|
77
|
+
},
|
|
78
|
+
balanced: {
|
|
79
|
+
discuss_before_plan: false,
|
|
80
|
+
verification_depth: 'standard',
|
|
81
|
+
after_verify_suggest_uat: false,
|
|
82
|
+
after_verify_suggest_pr: true,
|
|
83
|
+
after_verify_draft_commit: true,
|
|
84
|
+
scan_max_age_days: 0,
|
|
85
|
+
compact_max_age_days: 0,
|
|
86
|
+
},
|
|
87
|
+
fast: {
|
|
88
|
+
discuss_before_plan: false,
|
|
89
|
+
verification_depth: 'quick',
|
|
90
|
+
after_verify_suggest_uat: false,
|
|
91
|
+
after_verify_suggest_pr: false,
|
|
92
|
+
after_verify_draft_commit: true,
|
|
93
|
+
scan_max_age_days: 0,
|
|
94
|
+
compact_max_age_days: 0,
|
|
95
|
+
},
|
|
96
|
+
legacy: {
|
|
97
|
+
discuss_before_plan: true,
|
|
98
|
+
verification_depth: 'thorough',
|
|
99
|
+
after_verify_suggest_uat: true,
|
|
100
|
+
after_verify_suggest_pr: false,
|
|
101
|
+
after_verify_draft_commit: false,
|
|
102
|
+
scan_max_age_days: 0,
|
|
103
|
+
compact_max_age_days: 0,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
return profiles[profile] || {};
|
|
107
|
+
}
|
|
108
|
+
|
|
40
109
|
/**
|
|
41
110
|
* @param {string} targetProject
|
|
42
111
|
*/
|
|
@@ -45,6 +114,8 @@ function loadOxeConfigMerged(targetProject) {
|
|
|
45
114
|
discuss_before_plan: false,
|
|
46
115
|
after_verify_suggest_pr: true,
|
|
47
116
|
after_verify_draft_commit: true,
|
|
117
|
+
after_verify_suggest_uat: false,
|
|
118
|
+
verification_depth: 'standard',
|
|
48
119
|
default_verify_command: '',
|
|
49
120
|
scan_max_age_days: 0,
|
|
50
121
|
compact_max_age_days: 0,
|
|
@@ -59,7 +130,9 @@ function loadOxeConfigMerged(targetProject) {
|
|
|
59
130
|
const raw = fs.readFileSync(p, 'utf8');
|
|
60
131
|
const j = JSON.parse(raw);
|
|
61
132
|
if (!j || typeof j !== 'object' || Array.isArray(j)) return { config: defaults, path: p, parseError: 'não é um objeto' };
|
|
62
|
-
|
|
133
|
+
// Expandir profile antes de mesclar com o config explícito (keys explícitas prevalecem)
|
|
134
|
+
const profileExpansion = (typeof j.profile === 'string') ? expandExecutionProfile(j.profile) : {};
|
|
135
|
+
return { config: { ...defaults, ...profileExpansion, ...j }, path: p, parseError: null };
|
|
63
136
|
} catch (e) {
|
|
64
137
|
return { config: defaults, path: p, parseError: e.message };
|
|
65
138
|
}
|
|
@@ -129,6 +202,26 @@ function validateConfigShape(cfg) {
|
|
|
129
202
|
if (cfg.spec_required_sections != null && !Array.isArray(cfg.spec_required_sections)) {
|
|
130
203
|
typeErrors.push('spec_required_sections deve ser array de strings (cabeçalhos ## …)');
|
|
131
204
|
}
|
|
205
|
+
if (cfg.profile != null) {
|
|
206
|
+
if (typeof cfg.profile !== 'string') {
|
|
207
|
+
typeErrors.push('profile deve ser string');
|
|
208
|
+
} else if (!EXECUTION_PROFILES.includes(cfg.profile)) {
|
|
209
|
+
typeErrors.push(`profile deve ser um de: ${EXECUTION_PROFILES.join(', ')}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (cfg.verification_depth != null) {
|
|
213
|
+
if (typeof cfg.verification_depth !== 'string') {
|
|
214
|
+
typeErrors.push('verification_depth deve ser string');
|
|
215
|
+
} else if (!VERIFICATION_DEPTHS.includes(cfg.verification_depth)) {
|
|
216
|
+
typeErrors.push(`verification_depth deve ser um de: ${VERIFICATION_DEPTHS.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (cfg.after_verify_suggest_uat != null && typeof cfg.after_verify_suggest_uat !== 'boolean') {
|
|
220
|
+
typeErrors.push('after_verify_suggest_uat deve ser boolean');
|
|
221
|
+
}
|
|
222
|
+
if (cfg.scale_adaptive != null && typeof cfg.scale_adaptive !== 'boolean') {
|
|
223
|
+
typeErrors.push('scale_adaptive deve ser boolean');
|
|
224
|
+
}
|
|
132
225
|
return { unknownKeys, typeErrors };
|
|
133
226
|
}
|
|
134
227
|
|
|
@@ -504,10 +597,13 @@ function buildHealthReport(target) {
|
|
|
504
597
|
|
|
505
598
|
module.exports = {
|
|
506
599
|
ALLOWED_CONFIG_KEYS,
|
|
600
|
+
EXECUTION_PROFILES,
|
|
601
|
+
VERIFICATION_DEPTHS,
|
|
507
602
|
INSTALL_PROFILES,
|
|
508
603
|
INSTALL_REPO_LAYOUTS,
|
|
509
604
|
INSTALL_OBJECT_KEYS,
|
|
510
605
|
EXPECTED_CODEBASE_MAPS,
|
|
606
|
+
expandExecutionProfile,
|
|
511
607
|
loadOxeConfigMerged,
|
|
512
608
|
validateConfigShape,
|
|
513
609
|
parseStatePhase,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OXE Security — validação de caminhos e detecção de padrões sensíveis.
|
|
5
|
+
* Usado pelo SDK e opcionalmente pelos workflows via `oxe-cc doctor`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Padrões de arquivos sensíveis que não devem aparecer em commits ou outputs.
|
|
13
|
+
* @type {RegExp[]}
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_SECRET_PATTERNS = [
|
|
16
|
+
/\.env(\.\w+)?$/i,
|
|
17
|
+
/secrets?\.(json|yaml|yml|toml|ini)$/i,
|
|
18
|
+
/credentials?\.(json|yaml|yml)$/i,
|
|
19
|
+
/\.pem$/i,
|
|
20
|
+
/\.key$/i,
|
|
21
|
+
/id_(rsa|ecdsa|ed25519)(\.pub)?$/i,
|
|
22
|
+
/\.pfx$/i,
|
|
23
|
+
/\.p12$/i,
|
|
24
|
+
/serviceAccountKey\.json$/i,
|
|
25
|
+
/firebase-adminsdk.*\.json$/i,
|
|
26
|
+
/aws-credentials/i,
|
|
27
|
+
/\.npmrc$/i,
|
|
28
|
+
/\.netrc$/i,
|
|
29
|
+
/htpasswd/i,
|
|
30
|
+
/vault[_-]token/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Padrões de conteúdo que indicam segredos inline.
|
|
35
|
+
* @type {RegExp[]}
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_SECRET_CONTENT_PATTERNS = [
|
|
38
|
+
/(?:password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key)\s*[:=]\s*["']?[^\s"']{8,}/i,
|
|
39
|
+
/(?:BEGIN\s+(?:RSA|EC|OPENSSH|PGP)\s+PRIVATE\s+KEY)/i,
|
|
40
|
+
/(?:AKIA|ASIA)[A-Z0-9]{16}/, // AWS access key
|
|
41
|
+
/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/, // JWT
|
|
42
|
+
/ghp_[a-zA-Z0-9]{36}/, // GitHub Personal Access Token
|
|
43
|
+
/ghs_[a-zA-Z0-9]{36}/, // GitHub App Token
|
|
44
|
+
/glpat-[a-zA-Z0-9\-_]{20}/, // GitLab PAT
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Caminhos sempre negados (nunca devem ser lidos/escritos por workflows).
|
|
49
|
+
* @type {RegExp[]}
|
|
50
|
+
*/
|
|
51
|
+
const DEFAULT_DENIED_PATH_PATTERNS = [
|
|
52
|
+
/node_modules[/\\]/,
|
|
53
|
+
/\.git[/\\]/,
|
|
54
|
+
/dist[/\\]/,
|
|
55
|
+
/build[/\\]/,
|
|
56
|
+
/coverage[/\\]/,
|
|
57
|
+
/\.cache[/\\]/,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Verifica se um caminho é seguro para uso em workflows.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} filePath - Caminho a verificar (absoluto ou relativo ao projeto).
|
|
64
|
+
* @param {string} projectRoot - Raiz do projeto.
|
|
65
|
+
* @param {{
|
|
66
|
+
* allowedRoots?: string[],
|
|
67
|
+
* deniedPatterns?: RegExp[],
|
|
68
|
+
* secretPatterns?: RegExp[],
|
|
69
|
+
* }} [options]
|
|
70
|
+
* @returns {{ safe: boolean, reason: string | null }}
|
|
71
|
+
*/
|
|
72
|
+
function checkPathSafety(filePath, projectRoot, options = {}) {
|
|
73
|
+
const deniedPatterns = options.deniedPatterns || DEFAULT_DENIED_PATH_PATTERNS;
|
|
74
|
+
const secretPatterns = options.secretPatterns || DEFAULT_SECRET_PATTERNS;
|
|
75
|
+
|
|
76
|
+
const resolved = path.isAbsolute(filePath)
|
|
77
|
+
? path.normalize(filePath)
|
|
78
|
+
: path.normalize(path.join(projectRoot, filePath));
|
|
79
|
+
|
|
80
|
+
// Verifica path traversal
|
|
81
|
+
const normalizedRoot = path.normalize(projectRoot);
|
|
82
|
+
if (!resolved.startsWith(normalizedRoot)) {
|
|
83
|
+
return {
|
|
84
|
+
safe: false,
|
|
85
|
+
reason: `Path traversal detectado: "${filePath}" sai da raiz do projeto`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Verifica padrões negados
|
|
90
|
+
const relative = path.relative(normalizedRoot, resolved);
|
|
91
|
+
for (const pattern of deniedPatterns) {
|
|
92
|
+
if (pattern.test(relative)) {
|
|
93
|
+
return {
|
|
94
|
+
safe: false,
|
|
95
|
+
reason: `Caminho em área negada (${pattern}): "${relative}"`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Verifica padrões de segredos por nome de arquivo
|
|
101
|
+
const basename = path.basename(resolved);
|
|
102
|
+
for (const pattern of secretPatterns) {
|
|
103
|
+
if (pattern.test(basename)) {
|
|
104
|
+
return {
|
|
105
|
+
safe: false,
|
|
106
|
+
reason: `Nome de arquivo indica segredo (${pattern}): "${basename}"`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { safe: true, reason: null };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Escaneia um arquivo em busca de padrões de segredos inline.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} filePath - Caminho absoluto ao arquivo.
|
|
118
|
+
* @param {{ contentPatterns?: RegExp[] }} [options]
|
|
119
|
+
* @returns {{ hasSecrets: boolean, matches: Array<{ line: number, pattern: string, preview: string }> }}
|
|
120
|
+
*/
|
|
121
|
+
function scanFileForSecrets(filePath, options = {}) {
|
|
122
|
+
const contentPatterns = options.contentPatterns || DEFAULT_SECRET_CONTENT_PATTERNS;
|
|
123
|
+
|
|
124
|
+
if (!fs.existsSync(filePath)) {
|
|
125
|
+
return { hasSecrets: false, matches: [] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let content;
|
|
129
|
+
try {
|
|
130
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
131
|
+
} catch {
|
|
132
|
+
return { hasSecrets: false, matches: [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @type {Array<{ line: number, pattern: string, preview: string }>} */
|
|
136
|
+
const matches = [];
|
|
137
|
+
const lines = content.split('\n');
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
const line = lines[i];
|
|
141
|
+
for (const pattern of contentPatterns) {
|
|
142
|
+
if (pattern.test(line)) {
|
|
143
|
+
matches.push({
|
|
144
|
+
line: i + 1,
|
|
145
|
+
pattern: pattern.toString(),
|
|
146
|
+
preview: line.trim().slice(0, 80) + (line.trim().length > 80 ? '…' : ''),
|
|
147
|
+
});
|
|
148
|
+
break; // Um match por linha é suficiente
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { hasSecrets: matches.length > 0, matches };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Escaneia um diretório em busca de arquivos com nomes de segredos.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} dir - Diretório a escanear.
|
|
160
|
+
* @param {{ secretPatterns?: RegExp[], maxDepth?: number }} [options]
|
|
161
|
+
* @returns {string[]} Lista de caminhos relativos com problemas.
|
|
162
|
+
*/
|
|
163
|
+
function scanDirForSecretFiles(dir, options = {}) {
|
|
164
|
+
const secretPatterns = options.secretPatterns || DEFAULT_SECRET_PATTERNS;
|
|
165
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
166
|
+
/** @type {string[]} */
|
|
167
|
+
const found = [];
|
|
168
|
+
|
|
169
|
+
function walk(current, depth) {
|
|
170
|
+
if (depth > maxDepth) return;
|
|
171
|
+
let entries;
|
|
172
|
+
try {
|
|
173
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
174
|
+
} catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
if (entry.name.startsWith('.') && depth > 0) continue;
|
|
179
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
180
|
+
const full = path.join(current, entry.name);
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
walk(full, depth + 1);
|
|
183
|
+
} else {
|
|
184
|
+
for (const pattern of secretPatterns) {
|
|
185
|
+
if (pattern.test(entry.name)) {
|
|
186
|
+
found.push(path.relative(dir, full));
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
walk(dir, 0);
|
|
195
|
+
return found;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Valida que um conjunto de caminhos (do PLAN.md) são seguros.
|
|
200
|
+
*
|
|
201
|
+
* @param {string[]} filePaths
|
|
202
|
+
* @param {string} projectRoot
|
|
203
|
+
* @returns {{ ok: boolean, issues: Array<{ path: string, reason: string }> }}
|
|
204
|
+
*/
|
|
205
|
+
function validatePlanPaths(filePaths, projectRoot) {
|
|
206
|
+
/** @type {Array<{ path: string, reason: string }>} */
|
|
207
|
+
const issues = [];
|
|
208
|
+
for (const fp of filePaths) {
|
|
209
|
+
const check = checkPathSafety(fp, projectRoot);
|
|
210
|
+
if (!check.safe) {
|
|
211
|
+
issues.push({ path: fp, reason: check.reason || 'caminho inseguro' });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { ok: issues.length === 0, issues };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
checkPathSafety,
|
|
219
|
+
scanFileForSecrets,
|
|
220
|
+
scanDirForSecretFiles,
|
|
221
|
+
validatePlanPaths,
|
|
222
|
+
DEFAULT_SECRET_PATTERNS,
|
|
223
|
+
DEFAULT_SECRET_CONTENT_PATTERNS,
|
|
224
|
+
DEFAULT_DENIED_PATH_PATTERNS,
|
|
225
|
+
};
|
package/bin/oxe-cc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* OXE — CLI em pt-BR: instala workflows no projeto, bootstrap `.oxe/`, doctor, uninstall, update.
|
|
4
4
|
* Uso: npx oxe-cc, doctor, status, init-oxe, uninstall, update (ver --help).
|
|
@@ -1048,6 +1048,30 @@ function bootstrapOxe(target, opts) {
|
|
|
1048
1048
|
}
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
+
// Criar estruturas opcionais: plugins/, workstreams/, memory/
|
|
1052
|
+
const pluginsDir = path.join(oxeDir, 'plugins');
|
|
1053
|
+
if (!fs.existsSync(pluginsDir)) {
|
|
1054
|
+
ensureDir(pluginsDir);
|
|
1055
|
+
const pluginsReadme = path.join(PKG_ROOT, 'oxe', 'templates', 'PLUGINS.md');
|
|
1056
|
+
if (fs.existsSync(pluginsReadme)) {
|
|
1057
|
+
const destPluginsReadme = path.join(pluginsDir, 'README.md');
|
|
1058
|
+
if (!fs.existsSync(destPluginsReadme)) {
|
|
1059
|
+
copyFile(pluginsReadme, destPluginsReadme, { dryRun: false });
|
|
1060
|
+
console.log(`${green}init${reset} ${destPluginsReadme}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const workstreamsDir = path.join(oxeDir, 'workstreams');
|
|
1066
|
+
if (!fs.existsSync(workstreamsDir)) {
|
|
1067
|
+
ensureDir(workstreamsDir);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const memoryDir = path.join(oxeDir, 'memory');
|
|
1071
|
+
if (!fs.existsSync(memoryDir)) {
|
|
1072
|
+
ensureDir(memoryDir);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1051
1075
|
ensureGitignoreIgnoresOxeDir(target, { dryRun: false });
|
|
1052
1076
|
}
|
|
1053
1077
|
|
|
@@ -1596,6 +1620,11 @@ function runInstall(opts) {
|
|
|
1596
1620
|
const nested = path.join(target, '.oxe');
|
|
1597
1621
|
copyDir(path.join(PKG_ROOT, 'oxe', 'workflows'), path.join(nested, 'workflows'), copyOpts, true);
|
|
1598
1622
|
copyDir(path.join(PKG_ROOT, 'oxe', 'templates'), path.join(nested, 'templates'), copyOpts, true);
|
|
1623
|
+
// Personas: copiar para .oxe/personas/ (não sobrescreve personalizações do projeto)
|
|
1624
|
+
const personasSrc = path.join(PKG_ROOT, 'oxe', 'personas');
|
|
1625
|
+
if (fs.existsSync(personasSrc)) {
|
|
1626
|
+
copyDir(personasSrc, path.join(nested, 'personas'), copyOpts, false);
|
|
1627
|
+
}
|
|
1599
1628
|
}
|
|
1600
1629
|
|
|
1601
1630
|
const cursorBase = installCursorBase(opts);
|
package/commands/oxe/execute.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: oxe:execute
|
|
3
|
-
description: Executar onda
|
|
4
|
-
argument-hint: "[
|
|
5
|
-
allowed-tools:
|
|
6
|
-
- Read
|
|
7
|
-
- Bash
|
|
8
|
-
- Glob
|
|
9
|
-
- Grep
|
|
10
|
-
- Write
|
|
11
|
-
- Task
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
**Workflow canónico:** `oxe/workflows/execute.md`
|
|
15
|
-
|
|
16
|
-
Execute integralmente esse ficheiro na raiz do repositório em que estás a trabalhar. Usa `$ARGUMENTS` como foco (onda, tarefa, confirmação).
|
|
1
|
+
---
|
|
2
|
+
name: oxe:execute
|
|
3
|
+
description: "Executar plano (solo ou com agentes): escolha Completo (1 sessão) | Por onda | Por tarefa — controle explícito de requisições"
|
|
4
|
+
argument-hint: "[A=completo | B=por-onda | C=por-tarefa | onda N | Tn]"
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Bash
|
|
8
|
+
- Glob
|
|
9
|
+
- Grep
|
|
10
|
+
- Write
|
|
11
|
+
- Task
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
**Workflow canónico:** `oxe/workflows/execute.md`
|
|
15
|
+
|
|
16
|
+
Execute integralmente esse ficheiro na raiz do repositório em que estás a trabalhar. Usa `$ARGUMENTS` como foco (onda, tarefa, confirmação).
|