oxe-cc 0.3.8 → 0.5.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 (78) hide show
  1. package/.cursor/commands/oxe-checkpoint.md +2 -0
  2. package/.cursor/commands/oxe-compact.md +2 -0
  3. package/.cursor/commands/oxe-debug.md +2 -0
  4. package/.cursor/commands/oxe-discuss.md +2 -0
  5. package/.cursor/commands/oxe-execute.md +3 -1
  6. package/.cursor/commands/oxe-forensics.md +2 -0
  7. package/.cursor/commands/oxe-help.md +2 -0
  8. package/.cursor/commands/oxe-milestone.md +11 -0
  9. package/.cursor/commands/oxe-next.md +2 -0
  10. package/.cursor/commands/oxe-obs.md +11 -0
  11. package/.cursor/commands/oxe-plan-agent.md +2 -0
  12. package/.cursor/commands/oxe-plan.md +2 -0
  13. package/.cursor/commands/oxe-quick.md +3 -1
  14. package/.cursor/commands/oxe-research.md +2 -0
  15. package/.cursor/commands/oxe-review-pr.md +2 -0
  16. package/.cursor/commands/oxe-route.md +2 -0
  17. package/.cursor/commands/oxe-scan.md +2 -0
  18. package/.cursor/commands/oxe-spec.md +3 -1
  19. package/.cursor/commands/oxe-ui-review.md +2 -0
  20. package/.cursor/commands/oxe-ui-spec.md +2 -0
  21. package/.cursor/commands/oxe-update.md +2 -0
  22. package/.cursor/commands/oxe-validate-gaps.md +2 -0
  23. package/.cursor/commands/oxe-verify.md +2 -0
  24. package/.cursor/commands/oxe-workstream.md +11 -0
  25. package/.github/prompts/oxe-execute.prompt.md +12 -12
  26. package/.github/prompts/oxe-milestone.prompt.md +12 -0
  27. package/.github/prompts/oxe-obs.prompt.md +12 -0
  28. package/.github/prompts/oxe-quick.prompt.md +12 -12
  29. package/.github/prompts/oxe-spec.prompt.md +2 -2
  30. package/.github/prompts/oxe-workstream.prompt.md +12 -0
  31. package/README.md +205 -442
  32. package/bin/banner.txt +1 -1
  33. package/bin/lib/oxe-plugins.cjs +226 -0
  34. package/bin/lib/oxe-project-health.cjs +97 -1
  35. package/bin/lib/oxe-security.cjs +225 -0
  36. package/bin/oxe-cc.js +29 -0
  37. package/commands/oxe/execute.md +16 -16
  38. package/commands/oxe/milestone.md +16 -0
  39. package/commands/oxe/obs.md +16 -0
  40. package/commands/oxe/quick.md +16 -16
  41. package/commands/oxe/spec.md +2 -2
  42. package/commands/oxe/workstream.md +16 -0
  43. package/lib/sdk/index.cjs +284 -0
  44. package/lib/sdk/index.d.ts +148 -1
  45. package/oxe/personas/README.md +39 -0
  46. package/oxe/personas/architect.md +37 -0
  47. package/oxe/personas/db-specialist.md +36 -0
  48. package/oxe/personas/debugger.md +38 -0
  49. package/oxe/personas/executor.md +38 -0
  50. package/oxe/personas/planner.md +36 -0
  51. package/oxe/personas/researcher.md +35 -0
  52. package/oxe/personas/ui-specialist.md +36 -0
  53. package/oxe/personas/verifier.md +39 -0
  54. package/oxe/templates/CONFIG.md +58 -4
  55. package/oxe/templates/DISCUSS.template.md +44 -0
  56. package/oxe/templates/MEMORY.template.md +49 -0
  57. package/oxe/templates/MILESTONES.template.md +17 -0
  58. package/oxe/templates/OBSERVATIONS.template.md +18 -0
  59. package/oxe/templates/PLUGINS.md +101 -0
  60. package/oxe/templates/ROADMAP.template.md +44 -0
  61. package/oxe/templates/STATE.md +29 -2
  62. package/oxe/templates/config.template.json +5 -0
  63. package/oxe/templates/plan-agent-messages-README.template.md +1 -1
  64. package/oxe/templates/quick-agents.template.json +24 -0
  65. package/oxe/workflows/discuss.md +48 -5
  66. package/oxe/workflows/execute.md +111 -28
  67. package/oxe/workflows/help.md +80 -15
  68. package/oxe/workflows/milestone.md +96 -0
  69. package/oxe/workflows/obs.md +95 -0
  70. package/oxe/workflows/plan-agent.md +31 -1
  71. package/oxe/workflows/plan.md +5 -1
  72. package/oxe/workflows/quick.md +120 -10
  73. package/oxe/workflows/references/plan-agent-chat-protocol.md +14 -0
  74. package/oxe/workflows/scan.md +9 -1
  75. package/oxe/workflows/spec.md +172 -23
  76. package/oxe/workflows/verify.md +77 -17
  77. package/oxe/workflows/workstream.md +96 -0
  78. package/package.json +3 -2
package/bin/banner.txt CHANGED
@@ -15,4 +15,4 @@
15
15
 
16
16
  ══════════════════════════════════════════════════════
17
17
 
18
- v0.3.8
18
+ v0.5.0
@@ -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
- return { config: { ...defaults, ...j }, path: p, parseError: null };
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
@@ -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);
@@ -1,16 +1,16 @@
1
- ---
2
- name: oxe:execute
3
- description: Executar onda do PLAN.md ou passos do QUICK.md
4
- argument-hint: "[opcional: onda ou 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).
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).
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: oxe:milestone
3
+ description: "Marcos de entrega (M-NN) — new, complete (arquiva SPEC/PLAN/VERIFY em .oxe/milestones/M-NN/), status, audit"
4
+ argument-hint: "new <nome> | complete | status | audit"
5
+ allowed-tools:
6
+ - Read
7
+ - Bash
8
+ - Glob
9
+ - Grep
10
+ - Write
11
+ - Task
12
+ ---
13
+
14
+ **Workflow canónico:** `oxe/workflows/milestone.md`
15
+
16
+ Execute integralmente esse ficheiro na raiz do repositório em que estás a trabalhar. Usa o texto em `$ARGUMENTS` como subcomando e contexto (ex.: `new sprint-1`, `complete`, `audit`).