aiox-core 5.0.0 → 5.0.2
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/.aiox-core/data/entity-registry.yaml +5297 -1814
- package/.aiox-core/data/registry-update-log.jsonl +2 -0
- package/.aiox-core/development/templates/service-template/README.md.hbs +158 -158
- package/.aiox-core/development/templates/service-template/__tests__/index.test.ts.hbs +237 -237
- package/.aiox-core/development/templates/service-template/client.ts.hbs +403 -403
- package/.aiox-core/development/templates/service-template/errors.ts.hbs +182 -182
- package/.aiox-core/development/templates/service-template/index.ts.hbs +120 -120
- package/.aiox-core/development/templates/service-template/package.json.hbs +87 -87
- package/.aiox-core/development/templates/service-template/types.ts.hbs +145 -145
- package/.aiox-core/development/templates/squad-template/LICENSE +21 -21
- package/.aiox-core/infrastructure/scripts/tool-resolver.js +4 -4
- package/.aiox-core/infrastructure/templates/aiox-sync.yaml.template +182 -182
- package/.aiox-core/infrastructure/templates/coderabbit.yaml.template +279 -279
- package/.aiox-core/infrastructure/templates/github-workflows/ci.yml.template +169 -169
- package/.aiox-core/infrastructure/templates/github-workflows/pr-automation.yml.template +330 -330
- package/.aiox-core/infrastructure/templates/github-workflows/release.yml.template +196 -196
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-aiox-base.tmpl +63 -63
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-brownfield-merge.tmpl +18 -18
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-node.tmpl +85 -85
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-python.tmpl +145 -145
- package/.aiox-core/install-manifest.yaml +58 -58
- package/.aiox-core/local-config.yaml.template +71 -71
- package/.aiox-core/monitor/hooks/lib/__init__.py +1 -1
- package/.aiox-core/monitor/hooks/lib/enrich.py +58 -58
- package/.aiox-core/monitor/hooks/lib/send_event.py +47 -47
- package/.aiox-core/monitor/hooks/notification.py +29 -29
- package/.aiox-core/monitor/hooks/post_tool_use.py +45 -45
- package/.aiox-core/monitor/hooks/pre_compact.py +29 -29
- package/.aiox-core/monitor/hooks/pre_tool_use.py +40 -40
- package/.aiox-core/monitor/hooks/stop.py +29 -29
- package/.aiox-core/monitor/hooks/subagent_stop.py +29 -29
- package/.aiox-core/monitor/hooks/user_prompt_submit.py +38 -38
- package/.aiox-core/product/templates/adr.hbs +125 -125
- package/.aiox-core/product/templates/dbdr.hbs +241 -241
- package/.aiox-core/product/templates/engine/elicitation.js +2 -3
- package/.aiox-core/product/templates/epic.hbs +212 -212
- package/.aiox-core/product/templates/pmdr.hbs +186 -186
- package/.aiox-core/product/templates/prd-v2.0.hbs +216 -216
- package/.aiox-core/product/templates/prd.hbs +201 -201
- package/.aiox-core/product/templates/story.hbs +263 -263
- package/.aiox-core/product/templates/task.hbs +170 -170
- package/.aiox-core/product/templates/tmpl-comment-on-examples.sql +158 -158
- package/.aiox-core/product/templates/tmpl-migration-script.sql +91 -91
- package/.aiox-core/product/templates/tmpl-rls-granular-policies.sql +104 -104
- package/.aiox-core/product/templates/tmpl-rls-kiss-policy.sql +10 -10
- package/.aiox-core/product/templates/tmpl-rls-roles.sql +135 -135
- package/.aiox-core/product/templates/tmpl-rls-simple.sql +77 -77
- package/.aiox-core/product/templates/tmpl-rls-tenant.sql +152 -152
- package/.aiox-core/product/templates/tmpl-rollback-script.sql +77 -77
- package/.aiox-core/product/templates/tmpl-seed-data.sql +140 -140
- package/.aiox-core/product/templates/tmpl-smoke-test.sql +16 -16
- package/.aiox-core/product/templates/tmpl-staging-copy-merge.sql +139 -139
- package/.aiox-core/product/templates/tmpl-stored-proc.sql +140 -140
- package/.aiox-core/product/templates/tmpl-trigger.sql +152 -152
- package/.aiox-core/product/templates/tmpl-view-materialized.sql +133 -133
- package/.aiox-core/product/templates/tmpl-view.sql +177 -177
- package/.aiox-core/scripts/pm.sh +0 -0
- package/.claude/hooks/code-intel-pretool.cjs +107 -0
- package/.claude/hooks/enforce-architecture-first.py +196 -196
- package/.claude/hooks/mind-clone-governance.py +192 -192
- package/.claude/hooks/read-protection.py +151 -151
- package/.claude/hooks/slug-validation.py +176 -176
- package/.claude/hooks/sql-governance.py +182 -182
- package/.claude/hooks/write-path-validation.py +194 -194
- package/LICENSE +33 -33
- package/bin/aiox-graph.js +0 -0
- package/bin/aiox-minimal.js +0 -0
- package/bin/aiox.js +0 -0
- package/docs/guides/aios-workflows/README.md +247 -0
- package/docs/guides/aios-workflows/bob-orchestrator-workflow.md +1536 -0
- package/package.json +1 -1
- package/packages/aiox-install/bin/aiox-install.js +0 -0
- package/packages/aiox-install/bin/edmcp.js +0 -0
- package/packages/aiox-pro-cli/bin/aiox-pro.js +0 -0
- package/packages/installer/src/wizard/pro-setup.js +210 -123
- package/pro/README.md +66 -0
- package/pro/license/degradation.js +220 -0
- package/pro/license/errors.js +450 -0
- package/pro/license/feature-gate.js +354 -0
- package/pro/license/index.js +181 -0
- package/pro/license/license-api.js +679 -0
- package/pro/license/license-cache.js +523 -0
- package/pro/license/license-crypto.js +303 -0
- package/scripts/check-markdown-links.py +352 -352
- package/scripts/dashboard-parallel-dev.sh +0 -0
- package/scripts/dashboard-parallel-phase3.sh +0 -0
- package/scripts/dashboard-parallel-phase4.sh +0 -0
- package/scripts/glue/README.md +355 -0
- package/scripts/glue/compose-agent-prompt.cjs +362 -0
- package/scripts/install-monitor-hooks.sh +0 -0
- package/.aiox-core/lib/build.json +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Code-Intel PreToolUse Hook — Auto-injects code intelligence context
|
|
6
|
+
* when an agent is about to Write or Edit a file.
|
|
7
|
+
*
|
|
8
|
+
* Protocol:
|
|
9
|
+
* - Reads JSON from stdin (Claude Code PreToolUse event)
|
|
10
|
+
* - Filters: only acts on Write and Edit tools
|
|
11
|
+
* - Queries RegistryProvider for entity, references, dependencies
|
|
12
|
+
* - Outputs JSON with additionalContext containing <code-intel-context> XML
|
|
13
|
+
* - Silent exit on any error (never blocks the tool call)
|
|
14
|
+
*
|
|
15
|
+
* @module code-intel-pretool-hook
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
/** Tools that trigger code-intel injection. */
|
|
21
|
+
const TARGET_TOOLS = new Set(['Write', 'Edit']);
|
|
22
|
+
|
|
23
|
+
/** Safety timeout (ms) — defense-in-depth. */
|
|
24
|
+
const HOOK_TIMEOUT_MS = 4000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read all data from stdin as a JSON object.
|
|
28
|
+
* @returns {Promise<object>}
|
|
29
|
+
*/
|
|
30
|
+
function readStdin() {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let data = '';
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
process.stdin.on('error', (e) => reject(e));
|
|
35
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
36
|
+
process.stdin.on('end', () => {
|
|
37
|
+
try { resolve(JSON.parse(data)); }
|
|
38
|
+
catch (e) { reject(e); }
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Main hook pipeline.
|
|
45
|
+
*/
|
|
46
|
+
async function main() {
|
|
47
|
+
const input = await readStdin();
|
|
48
|
+
|
|
49
|
+
// Filter: only act on Write/Edit tools
|
|
50
|
+
const toolName = input && input.tool_name;
|
|
51
|
+
if (!toolName || !TARGET_TOOLS.has(toolName)) return;
|
|
52
|
+
|
|
53
|
+
// Extract file_path from tool input
|
|
54
|
+
const toolInput = input.tool_input;
|
|
55
|
+
if (!toolInput) return;
|
|
56
|
+
const filePath = toolInput.file_path;
|
|
57
|
+
if (!filePath) return;
|
|
58
|
+
|
|
59
|
+
// Resolve project root from hook cwd or process.cwd()
|
|
60
|
+
const cwd = input.cwd || process.cwd();
|
|
61
|
+
|
|
62
|
+
// Load hook-runtime (lazy — only when we actually need it)
|
|
63
|
+
const { resolveCodeIntel, formatAsXml } = require(
|
|
64
|
+
path.join(__dirname, '..', '..', '.aios-core', 'core', 'code-intel', 'hook-runtime.js'),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const intel = await resolveCodeIntel(filePath, cwd);
|
|
68
|
+
const xml = formatAsXml(intel, filePath);
|
|
69
|
+
if (!xml) return;
|
|
70
|
+
|
|
71
|
+
// Output in Claude Code hook format
|
|
72
|
+
const output = JSON.stringify({
|
|
73
|
+
hookSpecificOutput: {
|
|
74
|
+
additionalContext: xml,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const flushed = process.stdout.write(output);
|
|
79
|
+
if (!flushed) {
|
|
80
|
+
await new Promise((resolve) => process.stdout.once('drain', resolve));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Safely exit — no-op inside Jest workers.
|
|
86
|
+
* @param {number} code
|
|
87
|
+
*/
|
|
88
|
+
function safeExit(code) {
|
|
89
|
+
if (process.env.JEST_WORKER_ID) return;
|
|
90
|
+
process.exit(code);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Entry point runner. */
|
|
94
|
+
function run() {
|
|
95
|
+
const timer = setTimeout(() => safeExit(0), HOOK_TIMEOUT_MS);
|
|
96
|
+
timer.unref();
|
|
97
|
+
main()
|
|
98
|
+
.then(() => safeExit(0))
|
|
99
|
+
.catch(() => {
|
|
100
|
+
// Silent exit — stderr output triggers "hook error" in Claude Code UI
|
|
101
|
+
safeExit(0);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (require.main === module) run();
|
|
106
|
+
|
|
107
|
+
module.exports = { readStdin, main, run, HOOK_TIMEOUT_MS, TARGET_TOOLS };
|
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Hook: Enforce Architecture-First Development
|
|
4
|
-
|
|
5
|
-
REGRA: Código só pode ser criado/editado se existir documentação prévia.
|
|
6
|
-
|
|
7
|
-
Este hook intercepta Write/Edit em paths de código e verifica se existe
|
|
8
|
-
documentação aprovada antes de permitir a operação.
|
|
9
|
-
|
|
10
|
-
Exit Codes:
|
|
11
|
-
- 0: Permitido (doc existe ou path não requer doc)
|
|
12
|
-
- 2: Bloqueado (doc não existe para path protegido)
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
import sys
|
|
17
|
-
import os
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
# =============================================================================
|
|
21
|
-
# CONFIGURAÇÃO: Paths que EXIGEM documentação prévia
|
|
22
|
-
# =============================================================================
|
|
23
|
-
|
|
24
|
-
PROTECTED_PATHS = [
|
|
25
|
-
# Edge Functions - exigem docs/architecture/{function-name}.md
|
|
26
|
-
{
|
|
27
|
-
"pattern": "supabase/functions/",
|
|
28
|
-
"doc_patterns": [
|
|
29
|
-
"docs/architecture/{name}.md",
|
|
30
|
-
"docs/architecture/{name}-architecture.md",
|
|
31
|
-
"docs/approved-plans/{name}.md",
|
|
32
|
-
],
|
|
33
|
-
"extract_name": lambda p: p.split("supabase/functions/")[1].split("/")[0] if "supabase/functions/" in p else None,
|
|
34
|
-
},
|
|
35
|
-
# Migrations - exigem documentação de schema changes
|
|
36
|
-
{
|
|
37
|
-
"pattern": "supabase/migrations/",
|
|
38
|
-
"doc_patterns": [
|
|
39
|
-
"docs/approved-plans/migration-{name}.md",
|
|
40
|
-
"docs/architecture/database-changes.md",
|
|
41
|
-
],
|
|
42
|
-
"extract_name": lambda p: Path(p).stem if "supabase/migrations/" in p else None,
|
|
43
|
-
"allow_if_exists": True, # Permite editar migrations existentes
|
|
44
|
-
},
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
# Paths que são SEMPRE permitidos (não exigem doc)
|
|
48
|
-
ALWAYS_ALLOWED = [
|
|
49
|
-
".claude/",
|
|
50
|
-
"docs/",
|
|
51
|
-
"outputs/",
|
|
52
|
-
"squads/",
|
|
53
|
-
".aiox-core/",
|
|
54
|
-
".aiox-custom/",
|
|
55
|
-
"node_modules/",
|
|
56
|
-
".git/",
|
|
57
|
-
"package.json",
|
|
58
|
-
"package-lock.json",
|
|
59
|
-
"tsconfig.json",
|
|
60
|
-
".env",
|
|
61
|
-
"README.md",
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
# =============================================================================
|
|
65
|
-
# LÓGICA DO HOOK
|
|
66
|
-
# =============================================================================
|
|
67
|
-
|
|
68
|
-
def get_project_root():
|
|
69
|
-
"""Obtém o root do projeto via variável de ambiente ou cwd."""
|
|
70
|
-
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
71
|
-
|
|
72
|
-
def is_always_allowed(file_path: str) -> bool:
|
|
73
|
-
"""Verifica se o path está na lista de sempre permitidos."""
|
|
74
|
-
for allowed in ALWAYS_ALLOWED:
|
|
75
|
-
if allowed in file_path:
|
|
76
|
-
return True
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
def find_matching_protection(file_path: str) -> dict | None:
|
|
80
|
-
"""Encontra a regra de proteção que corresponde ao path."""
|
|
81
|
-
for protection in PROTECTED_PATHS:
|
|
82
|
-
if protection["pattern"] in file_path:
|
|
83
|
-
return protection
|
|
84
|
-
return None
|
|
85
|
-
|
|
86
|
-
def check_documentation_exists(file_path: str, protection: dict, project_root: str) -> tuple[bool, str]:
|
|
87
|
-
"""
|
|
88
|
-
Verifica se existe documentação para o path protegido.
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
(exists: bool, doc_path: str | None)
|
|
92
|
-
"""
|
|
93
|
-
extract_fn = protection.get("extract_name")
|
|
94
|
-
if not extract_fn:
|
|
95
|
-
return True, None
|
|
96
|
-
|
|
97
|
-
name = extract_fn(file_path)
|
|
98
|
-
if not name:
|
|
99
|
-
return True, None
|
|
100
|
-
|
|
101
|
-
# Verificar cada padrão de documentação
|
|
102
|
-
for doc_pattern in protection["doc_patterns"]:
|
|
103
|
-
doc_path = doc_pattern.format(name=name)
|
|
104
|
-
full_doc_path = os.path.join(project_root, doc_path)
|
|
105
|
-
|
|
106
|
-
if os.path.exists(full_doc_path):
|
|
107
|
-
return True, doc_path
|
|
108
|
-
|
|
109
|
-
# Se allow_if_exists e o arquivo já existe, permitir edição
|
|
110
|
-
if protection.get("allow_if_exists"):
|
|
111
|
-
full_file_path = os.path.join(project_root, file_path) if not file_path.startswith("/") else file_path
|
|
112
|
-
if os.path.exists(full_file_path):
|
|
113
|
-
return True, "(arquivo existente)"
|
|
114
|
-
|
|
115
|
-
return False, None
|
|
116
|
-
|
|
117
|
-
def format_required_docs(protection: dict, name: str) -> str:
|
|
118
|
-
"""Formata a lista de documentos aceitos."""
|
|
119
|
-
docs = []
|
|
120
|
-
for pattern in protection["doc_patterns"]:
|
|
121
|
-
docs.append(f" - {pattern.format(name=name)}")
|
|
122
|
-
return "\n".join(docs)
|
|
123
|
-
|
|
124
|
-
def main():
|
|
125
|
-
# Ler input do stdin
|
|
126
|
-
try:
|
|
127
|
-
input_data = json.load(sys.stdin)
|
|
128
|
-
except json.JSONDecodeError:
|
|
129
|
-
# Se não conseguir parsear, permitir (fail-open)
|
|
130
|
-
sys.exit(0)
|
|
131
|
-
|
|
132
|
-
tool_name = input_data.get("tool_name", "")
|
|
133
|
-
tool_input = input_data.get("tool_input", {})
|
|
134
|
-
file_path = tool_input.get("file_path", "")
|
|
135
|
-
|
|
136
|
-
# Só processar Write e Edit
|
|
137
|
-
if tool_name not in ["Write", "Edit"]:
|
|
138
|
-
sys.exit(0)
|
|
139
|
-
|
|
140
|
-
# Normalizar path (remover prefixo absoluto se presente)
|
|
141
|
-
project_root = get_project_root()
|
|
142
|
-
relative_path = file_path
|
|
143
|
-
if file_path.startswith(project_root):
|
|
144
|
-
relative_path = file_path[len(project_root):].lstrip("/")
|
|
145
|
-
|
|
146
|
-
# Verificar se é sempre permitido
|
|
147
|
-
if is_always_allowed(relative_path):
|
|
148
|
-
sys.exit(0)
|
|
149
|
-
|
|
150
|
-
# Verificar se path está protegido
|
|
151
|
-
protection = find_matching_protection(relative_path)
|
|
152
|
-
if not protection:
|
|
153
|
-
# Path não protegido, permitir
|
|
154
|
-
sys.exit(0)
|
|
155
|
-
|
|
156
|
-
# Verificar se documentação existe
|
|
157
|
-
doc_exists, doc_path = check_documentation_exists(relative_path, protection, project_root)
|
|
158
|
-
|
|
159
|
-
if doc_exists:
|
|
160
|
-
# Documentação existe, permitir
|
|
161
|
-
sys.exit(0)
|
|
162
|
-
|
|
163
|
-
# BLOQUEAR: Documentação não existe
|
|
164
|
-
name = protection["extract_name"](relative_path) or "unknown"
|
|
165
|
-
required_docs = format_required_docs(protection, name)
|
|
166
|
-
|
|
167
|
-
error_message = f"""
|
|
168
|
-
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
169
|
-
║ 🛑 ARCHITECTURE-FIRST: Documentação obrigatória antes de código ║
|
|
170
|
-
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
171
|
-
║ ║
|
|
172
|
-
║ Arquivo bloqueado: {relative_path[:50]:<50} ║
|
|
173
|
-
║ ║
|
|
174
|
-
║ REGRA: Antes de criar/editar código em paths protegidos, você DEVE: ║
|
|
175
|
-
║ ║
|
|
176
|
-
║ 1. Documentar o plano de arquitetura ║
|
|
177
|
-
║ 2. Obter aprovação do usuário ║
|
|
178
|
-
║ 3. Criar o arquivo de documentação ║
|
|
179
|
-
║ ║
|
|
180
|
-
║ Documentos aceitos para '{name}': ║
|
|
181
|
-
{required_docs}
|
|
182
|
-
║ ║
|
|
183
|
-
║ AÇÃO: Crie um dos documentos acima com o plano aprovado, depois tente ║
|
|
184
|
-
║ novamente a operação de código. ║
|
|
185
|
-
║ ║
|
|
186
|
-
║ DICA: Use `*create-doc architecture` para criar doc de arquitetura ║
|
|
187
|
-
║ Ou crie docs/approved-plans/{name}.md com o plano resumido ║
|
|
188
|
-
║ ║
|
|
189
|
-
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
190
|
-
"""
|
|
191
|
-
|
|
192
|
-
print(error_message, file=sys.stderr)
|
|
193
|
-
sys.exit(2) # Exit code 2 = bloqueia o tool
|
|
194
|
-
|
|
195
|
-
if __name__ == "__main__":
|
|
196
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: Enforce Architecture-First Development
|
|
4
|
+
|
|
5
|
+
REGRA: Código só pode ser criado/editado se existir documentação prévia.
|
|
6
|
+
|
|
7
|
+
Este hook intercepta Write/Edit em paths de código e verifica se existe
|
|
8
|
+
documentação aprovada antes de permitir a operação.
|
|
9
|
+
|
|
10
|
+
Exit Codes:
|
|
11
|
+
- 0: Permitido (doc existe ou path não requer doc)
|
|
12
|
+
- 2: Bloqueado (doc não existe para path protegido)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# CONFIGURAÇÃO: Paths que EXIGEM documentação prévia
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
PROTECTED_PATHS = [
|
|
25
|
+
# Edge Functions - exigem docs/architecture/{function-name}.md
|
|
26
|
+
{
|
|
27
|
+
"pattern": "supabase/functions/",
|
|
28
|
+
"doc_patterns": [
|
|
29
|
+
"docs/architecture/{name}.md",
|
|
30
|
+
"docs/architecture/{name}-architecture.md",
|
|
31
|
+
"docs/approved-plans/{name}.md",
|
|
32
|
+
],
|
|
33
|
+
"extract_name": lambda p: p.split("supabase/functions/")[1].split("/")[0] if "supabase/functions/" in p else None,
|
|
34
|
+
},
|
|
35
|
+
# Migrations - exigem documentação de schema changes
|
|
36
|
+
{
|
|
37
|
+
"pattern": "supabase/migrations/",
|
|
38
|
+
"doc_patterns": [
|
|
39
|
+
"docs/approved-plans/migration-{name}.md",
|
|
40
|
+
"docs/architecture/database-changes.md",
|
|
41
|
+
],
|
|
42
|
+
"extract_name": lambda p: Path(p).stem if "supabase/migrations/" in p else None,
|
|
43
|
+
"allow_if_exists": True, # Permite editar migrations existentes
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Paths que são SEMPRE permitidos (não exigem doc)
|
|
48
|
+
ALWAYS_ALLOWED = [
|
|
49
|
+
".claude/",
|
|
50
|
+
"docs/",
|
|
51
|
+
"outputs/",
|
|
52
|
+
"squads/",
|
|
53
|
+
".aiox-core/",
|
|
54
|
+
".aiox-custom/",
|
|
55
|
+
"node_modules/",
|
|
56
|
+
".git/",
|
|
57
|
+
"package.json",
|
|
58
|
+
"package-lock.json",
|
|
59
|
+
"tsconfig.json",
|
|
60
|
+
".env",
|
|
61
|
+
"README.md",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# LÓGICA DO HOOK
|
|
66
|
+
# =============================================================================
|
|
67
|
+
|
|
68
|
+
def get_project_root():
|
|
69
|
+
"""Obtém o root do projeto via variável de ambiente ou cwd."""
|
|
70
|
+
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
|
71
|
+
|
|
72
|
+
def is_always_allowed(file_path: str) -> bool:
|
|
73
|
+
"""Verifica se o path está na lista de sempre permitidos."""
|
|
74
|
+
for allowed in ALWAYS_ALLOWED:
|
|
75
|
+
if allowed in file_path:
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def find_matching_protection(file_path: str) -> dict | None:
|
|
80
|
+
"""Encontra a regra de proteção que corresponde ao path."""
|
|
81
|
+
for protection in PROTECTED_PATHS:
|
|
82
|
+
if protection["pattern"] in file_path:
|
|
83
|
+
return protection
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def check_documentation_exists(file_path: str, protection: dict, project_root: str) -> tuple[bool, str]:
|
|
87
|
+
"""
|
|
88
|
+
Verifica se existe documentação para o path protegido.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
(exists: bool, doc_path: str | None)
|
|
92
|
+
"""
|
|
93
|
+
extract_fn = protection.get("extract_name")
|
|
94
|
+
if not extract_fn:
|
|
95
|
+
return True, None
|
|
96
|
+
|
|
97
|
+
name = extract_fn(file_path)
|
|
98
|
+
if not name:
|
|
99
|
+
return True, None
|
|
100
|
+
|
|
101
|
+
# Verificar cada padrão de documentação
|
|
102
|
+
for doc_pattern in protection["doc_patterns"]:
|
|
103
|
+
doc_path = doc_pattern.format(name=name)
|
|
104
|
+
full_doc_path = os.path.join(project_root, doc_path)
|
|
105
|
+
|
|
106
|
+
if os.path.exists(full_doc_path):
|
|
107
|
+
return True, doc_path
|
|
108
|
+
|
|
109
|
+
# Se allow_if_exists e o arquivo já existe, permitir edição
|
|
110
|
+
if protection.get("allow_if_exists"):
|
|
111
|
+
full_file_path = os.path.join(project_root, file_path) if not file_path.startswith("/") else file_path
|
|
112
|
+
if os.path.exists(full_file_path):
|
|
113
|
+
return True, "(arquivo existente)"
|
|
114
|
+
|
|
115
|
+
return False, None
|
|
116
|
+
|
|
117
|
+
def format_required_docs(protection: dict, name: str) -> str:
|
|
118
|
+
"""Formata a lista de documentos aceitos."""
|
|
119
|
+
docs = []
|
|
120
|
+
for pattern in protection["doc_patterns"]:
|
|
121
|
+
docs.append(f" - {pattern.format(name=name)}")
|
|
122
|
+
return "\n".join(docs)
|
|
123
|
+
|
|
124
|
+
def main():
|
|
125
|
+
# Ler input do stdin
|
|
126
|
+
try:
|
|
127
|
+
input_data = json.load(sys.stdin)
|
|
128
|
+
except json.JSONDecodeError:
|
|
129
|
+
# Se não conseguir parsear, permitir (fail-open)
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
tool_name = input_data.get("tool_name", "")
|
|
133
|
+
tool_input = input_data.get("tool_input", {})
|
|
134
|
+
file_path = tool_input.get("file_path", "")
|
|
135
|
+
|
|
136
|
+
# Só processar Write e Edit
|
|
137
|
+
if tool_name not in ["Write", "Edit"]:
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
# Normalizar path (remover prefixo absoluto se presente)
|
|
141
|
+
project_root = get_project_root()
|
|
142
|
+
relative_path = file_path
|
|
143
|
+
if file_path.startswith(project_root):
|
|
144
|
+
relative_path = file_path[len(project_root):].lstrip("/")
|
|
145
|
+
|
|
146
|
+
# Verificar se é sempre permitido
|
|
147
|
+
if is_always_allowed(relative_path):
|
|
148
|
+
sys.exit(0)
|
|
149
|
+
|
|
150
|
+
# Verificar se path está protegido
|
|
151
|
+
protection = find_matching_protection(relative_path)
|
|
152
|
+
if not protection:
|
|
153
|
+
# Path não protegido, permitir
|
|
154
|
+
sys.exit(0)
|
|
155
|
+
|
|
156
|
+
# Verificar se documentação existe
|
|
157
|
+
doc_exists, doc_path = check_documentation_exists(relative_path, protection, project_root)
|
|
158
|
+
|
|
159
|
+
if doc_exists:
|
|
160
|
+
# Documentação existe, permitir
|
|
161
|
+
sys.exit(0)
|
|
162
|
+
|
|
163
|
+
# BLOQUEAR: Documentação não existe
|
|
164
|
+
name = protection["extract_name"](relative_path) or "unknown"
|
|
165
|
+
required_docs = format_required_docs(protection, name)
|
|
166
|
+
|
|
167
|
+
error_message = f"""
|
|
168
|
+
╔══════════════════════════════════════════════════════════════════════════════╗
|
|
169
|
+
║ 🛑 ARCHITECTURE-FIRST: Documentação obrigatória antes de código ║
|
|
170
|
+
╠══════════════════════════════════════════════════════════════════════════════╣
|
|
171
|
+
║ ║
|
|
172
|
+
║ Arquivo bloqueado: {relative_path[:50]:<50} ║
|
|
173
|
+
║ ║
|
|
174
|
+
║ REGRA: Antes de criar/editar código em paths protegidos, você DEVE: ║
|
|
175
|
+
║ ║
|
|
176
|
+
║ 1. Documentar o plano de arquitetura ║
|
|
177
|
+
║ 2. Obter aprovação do usuário ║
|
|
178
|
+
║ 3. Criar o arquivo de documentação ║
|
|
179
|
+
║ ║
|
|
180
|
+
║ Documentos aceitos para '{name}': ║
|
|
181
|
+
{required_docs}
|
|
182
|
+
║ ║
|
|
183
|
+
║ AÇÃO: Crie um dos documentos acima com o plano aprovado, depois tente ║
|
|
184
|
+
║ novamente a operação de código. ║
|
|
185
|
+
║ ║
|
|
186
|
+
║ DICA: Use `*create-doc architecture` para criar doc de arquitetura ║
|
|
187
|
+
║ Ou crie docs/approved-plans/{name}.md com o plano resumido ║
|
|
188
|
+
║ ║
|
|
189
|
+
╚══════════════════════════════════════════════════════════════════════════════╝
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
print(error_message, file=sys.stderr)
|
|
193
|
+
sys.exit(2) # Exit code 2 = bloqueia o tool
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
main()
|