@fontdata/ai-harness 2.0.0 → 2.2.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +9 -2
- package/LICENSE +21 -0
- package/README.md +175 -140
- package/{src → dist}/catalog.js +5 -12
- package/dist/cli.js +180 -0
- package/dist/comentarios/command.js +74 -0
- package/dist/comentarios/instalar.js +33 -0
- package/dist/config/config.js +99 -0
- package/dist/contratos/artefatos.js +284 -0
- package/dist/contratos/eventos.js +103 -0
- package/dist/doctor/checks.js +424 -0
- package/dist/init.js +240 -0
- package/dist/install/atomic.js +51 -0
- package/dist/install/flags.js +99 -0
- package/dist/install/hooks.js +47 -0
- package/dist/install/lock.js +39 -0
- package/dist/install/plan.js +209 -0
- package/dist/install/selection.js +77 -0
- package/dist/install/settings.js +112 -0
- package/dist/memoria/indice.js +195 -0
- package/dist/migrar/migrar.js +426 -0
- package/dist/rastro/command.js +46 -0
- package/dist/run.js +86 -0
- package/dist/trabalhos/conflitos.js +115 -0
- package/dist/types.js +1 -0
- package/dist/update/legado.js +74 -0
- package/dist/update/update.js +284 -0
- package/package.json +17 -8
- package/templates/CLAUDE.md +59 -30
- package/templates/agents/auditor-plano.md +44 -0
- package/templates/agents/investigador.md +32 -0
- package/templates/agents/qa.md +44 -0
- package/templates/agents/revisor-testes.md +40 -0
- package/templates/catalog.json +24 -61
- package/templates/commands/atualizar-deps.md +43 -9
- package/templates/commands/diagrama.md +35 -10
- package/templates/commands/gerar-env.md +39 -10
- package/templates/commands/gerar-testes.md +42 -10
- package/templates/commands/git-commit-push.md +0 -0
- package/templates/commands/onboarding.md +44 -15
- package/templates/commands/refatorar.md +50 -10
- package/templates/commands/revisar-pr.md +56 -12
- package/templates/commands/sincronizar-claude-md.md +177 -9
- package/templates/commands/versao.md +0 -0
- package/templates/hooks/comentarios/check.mjs +210 -0
- package/templates/hooks/hooks.json +34 -0
- package/templates/hooks/lembrete.mjs +29 -0
- package/templates/hooks/lib/config.mjs +40 -0
- package/templates/hooks/lib/hook.mjs +85 -0
- package/templates/hooks/lib/rastro.mjs +144 -0
- package/templates/hooks/lib/segredos.mjs +91 -0
- package/templates/hooks/lib/temas.mjs +111 -0
- package/templates/hooks/lib/trabalho.mjs +94 -0
- package/templates/hooks/proteger-contratos.mjs +80 -0
- package/templates/hooks/segredo-no-commit.mjs +76 -0
- package/templates/hooks/sem-jargao.mjs +58 -0
- package/templates/hooks/task-so-fecha-verde.mjs +33 -0
- package/templates/legado/manifesto.json +1267 -0
- package/templates/rules/comentarios.md +166 -0
- package/templates/rules/harness-projeto.md +41 -0
- package/templates/skills/auditar-performance/SKILL.md +53 -0
- package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
- package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
- package/templates/skills/auditar-seguranca/SKILL.md +67 -0
- package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
- package/templates/skills/auditar-ui/SKILL.md +64 -0
- package/templates/skills/auditar-ui/references/lentes.md +33 -0
- package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
- package/templates/skills/evoluir-harness/SKILL.md +81 -0
- package/templates/skills/evoluir-harness/references/agent.md +83 -0
- package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
- package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
- package/templates/skills/evoluir-harness/references/design-system.md +56 -0
- package/templates/skills/evoluir-harness/references/hook.md +95 -0
- package/templates/skills/evoluir-harness/references/rule.md +67 -0
- package/templates/skills/evoluir-harness/references/skill.md +63 -0
- package/templates/skills/executar-plan/SKILL.md +130 -0
- package/templates/skills/executar-plan/references/delegacao.md +43 -0
- package/templates/skills/executar-plan/references/retomada.md +33 -0
- package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
- package/templates/skills/harness/SKILL.md +75 -0
- package/templates/skills/harness/references/auto-tag.md +118 -0
- package/templates/skills/harness/references/ignores.md +40 -0
- package/templates/skills/harness/references/scaffold.md +29 -0
- package/templates/skills/harness/references/validar-ideia.md +42 -0
- package/templates/skills/memoria/SKILL.md +52 -0
- package/templates/skills/nova-tarefa/SKILL.md +99 -0
- package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
- package/templates/skills/nova-tarefa/references/regras-do-plano.md +35 -0
- package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
- package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
- package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
- package/templates/skills/resolver-bug/SKILL.md +104 -0
- package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
- package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
- package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
- package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
- package/bin/cli.js +0 -122
- package/src/init.js +0 -317
- package/src/installer.js +0 -92
- package/src/run.js +0 -99
- package/templates/commands/auditar-performance.md +0 -27
- package/templates/commands/auditar-seguranca.md +0 -64
- package/templates/commands/auditar-ui.md +0 -38
- package/templates/commands/codigo-morto.md +0 -22
- package/templates/commands/configurar-auto-tag.md +0 -185
- package/templates/commands/configurar-comentarios.md +0 -543
- package/templates/commands/configurar-orquestrador.md +0 -13
- package/templates/commands/corrigir-review.md +0 -11
- package/templates/commands/criar-agents.md +0 -42
- package/templates/commands/criar-code-reviewer.md +0 -30
- package/templates/commands/criar-hooks.md +0 -32
- package/templates/commands/criar-rag.md +0 -25
- package/templates/commands/criar-rules.md +0 -31
- package/templates/commands/criar-skill.md +0 -22
- package/templates/commands/design-system.md +0 -27
- package/templates/commands/executar-plan.md +0 -36
- package/templates/commands/gerar-claude-md.md +0 -66
- package/templates/commands/gerar-ignore.md +0 -37
- package/templates/commands/gerar-spec.md +0 -33
- package/templates/commands/harness.md +0 -34
- package/templates/commands/nova-tarefa.md +0 -60
- package/templates/commands/resolver-bug.md +0 -25
- package/templates/commands/review-por-task.md +0 -16
- package/templates/commands/scaffold.md +0 -25
- package/templates/commands/validar-commands.md +0 -24
- package/templates/commands/validar-ideia.md +0 -36
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Esqueleto comum dos hooks do ai-harness: lê o evento do stdin, respeita o modo da config,
|
|
3
|
+
// responde no protocolo do Claude Code e registra no rastro. Sem dependências.
|
|
4
|
+
import { DEFAULT_HOOK_MODES, hookMode } from './config.mjs';
|
|
5
|
+
import { recordEvent } from './rastro.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{ motivo: string }} Violation
|
|
9
|
+
* @typedef {{ root: string, mode: 'aviso' | 'bloqueio' }} HookContext
|
|
10
|
+
* @callback Handler
|
|
11
|
+
* @param {Record<string, any>} input
|
|
12
|
+
* @param {HookContext} context
|
|
13
|
+
* @returns {Promise<Violation | null> | Violation | null}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
async function readStdin() {
|
|
17
|
+
let data = '';
|
|
18
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resposta no formato de cada evento: em `PreToolUse` o bloqueio nega a permissão e o aviso só
|
|
24
|
+
* acrescenta contexto; nos demais eventos o bloqueio usa `decision: "block"`.
|
|
25
|
+
* @param {string} event
|
|
26
|
+
* @param {'aviso' | 'bloqueio'} mode
|
|
27
|
+
* @param {string} message
|
|
28
|
+
*/
|
|
29
|
+
function response(event, mode, message) {
|
|
30
|
+
if (event === 'PreToolUse') {
|
|
31
|
+
return mode === 'bloqueio'
|
|
32
|
+
? { hookSpecificOutput: { hookEventName: event, permissionDecision: 'deny', permissionDecisionReason: message } }
|
|
33
|
+
: { hookSpecificOutput: { hookEventName: event, additionalContext: message } };
|
|
34
|
+
}
|
|
35
|
+
if (mode === 'bloqueio') return { decision: 'block', reason: message };
|
|
36
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: message } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Executa um hook. O handler devolve `{ motivo }` quando há algo a avisar ou bloquear, ou null.
|
|
41
|
+
* Erros falham abertos (não atrapalham o usuário), exceto em hook de segurança no modo bloqueio.
|
|
42
|
+
* @param {string} name
|
|
43
|
+
* @param {Handler} handler
|
|
44
|
+
*/
|
|
45
|
+
export async function runHook(name, handler) {
|
|
46
|
+
/** @type {any} */
|
|
47
|
+
let input;
|
|
48
|
+
try {
|
|
49
|
+
input = JSON.parse(await readStdin());
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (typeof input !== 'object' || input === null) return;
|
|
54
|
+
|
|
55
|
+
const root = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd();
|
|
56
|
+
const mode = hookMode(root, name);
|
|
57
|
+
if (mode === 'desligado') return;
|
|
58
|
+
|
|
59
|
+
/** @type {Violation | null} */
|
|
60
|
+
let violation;
|
|
61
|
+
try {
|
|
62
|
+
violation = await handler(input, { root, mode });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (mode !== 'bloqueio' || DEFAULT_HOOK_MODES[name] !== 'bloqueio') return;
|
|
65
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
66
|
+
violation = { motivo: `o hook não conseguiu verificar (${reason}); por segurança, a ação foi bloqueada.` };
|
|
67
|
+
}
|
|
68
|
+
if (!violation) return;
|
|
69
|
+
|
|
70
|
+
const message = `[ai-harness · ${name}] ${violation.motivo}`;
|
|
71
|
+
process.stdout.write(JSON.stringify(response(String(input.hook_event_name ?? ''), mode, message)));
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
recordEvent(root, {
|
|
75
|
+
evento: mode === 'bloqueio' ? 'hook_bloqueio' : 'hook_aviso',
|
|
76
|
+
origem: 'hook',
|
|
77
|
+
nome: name,
|
|
78
|
+
modo: mode,
|
|
79
|
+
resultado: mode,
|
|
80
|
+
dados: { motivo: violation.motivo.slice(0, 1000) },
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
// o rastro nunca impede a resposta do hook.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Gravação do rastro de eventos (contrato em docs/contratos/EVENTOS.md do ai-harness).
|
|
3
|
+
// Sem dependências: roda também no plugin instalado a partir do GitHub, que não tem node_modules.
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const EVENT_VERSION = 1;
|
|
8
|
+
export const MAX_LINE_BYTES = 4096;
|
|
9
|
+
export const MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
export const EVENTS = [
|
|
12
|
+
'trabalho_iniciado',
|
|
13
|
+
'trabalho_concluido',
|
|
14
|
+
'task_iniciada',
|
|
15
|
+
'task_concluida',
|
|
16
|
+
'suite_verde',
|
|
17
|
+
'suite_vermelha',
|
|
18
|
+
'hook_aviso',
|
|
19
|
+
'hook_bloqueio',
|
|
20
|
+
'agent_veredito',
|
|
21
|
+
'bloqueio_aberto',
|
|
22
|
+
'bloqueio_fechado',
|
|
23
|
+
'decisao_registrada',
|
|
24
|
+
'relatorio_gerado',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} EventFields
|
|
29
|
+
* @property {string | null} [trabalhoId] Sem valor, usa o trabalho ativo do projeto.
|
|
30
|
+
* @property {string} evento
|
|
31
|
+
* @property {'hook' | 'comando' | 'agent' | 'cli'} origem
|
|
32
|
+
* @property {string} nome
|
|
33
|
+
* @property {string | null} [taskId]
|
|
34
|
+
* @property {'aviso' | 'bloqueio' | 'desligado' | null} [modo]
|
|
35
|
+
* @property {'ok' | 'aviso' | 'bloqueio' | 'falha' | null} [resultado]
|
|
36
|
+
* @property {Record<string, unknown> | null} [dados]
|
|
37
|
+
* @property {Date} [now]
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Monta o evento com as chaves na ordem do contrato.
|
|
42
|
+
* @param {EventFields} fields
|
|
43
|
+
*/
|
|
44
|
+
export function buildEvent(fields) {
|
|
45
|
+
if (!EVENTS.includes(fields.evento)) {
|
|
46
|
+
throw new Error(`Evento fora do vocabulário do rastro: ${fields.evento}. Veja docs/contratos/EVENTOS.md.`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
v: EVENT_VERSION,
|
|
50
|
+
ts: (fields.now ?? new Date()).toISOString(),
|
|
51
|
+
trabalho_id: fields.trabalhoId ?? null,
|
|
52
|
+
evento: fields.evento,
|
|
53
|
+
origem: fields.origem,
|
|
54
|
+
nome: fields.nome,
|
|
55
|
+
task_id: fields.taskId ?? null,
|
|
56
|
+
modo: fields.modo ?? null,
|
|
57
|
+
resultado: fields.resultado ?? null,
|
|
58
|
+
dados: fields.dados ?? null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Trabalhos ativos: os `trabalhos_ativos` do .ai-harness/estado.json (ou o `trabalho_ativo` do formato
|
|
64
|
+
* anterior) ou, sem eles, os PLAN.md com `status: em-execucao`. Várias sessões podem executar
|
|
65
|
+
* trabalhos diferentes na mesma pasta, então a lista pode ter mais de um id.
|
|
66
|
+
* @param {string} root
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
export function findActiveWorks(root) {
|
|
70
|
+
const statePath = join(root, '.ai-harness', 'estado.json');
|
|
71
|
+
if (existsSync(statePath)) {
|
|
72
|
+
try {
|
|
73
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
74
|
+
/** @type {unknown[]} */
|
|
75
|
+
const listed = Array.isArray(state.trabalhos_ativos) ? state.trabalhos_ativos : [state.trabalho_ativo];
|
|
76
|
+
const active = [...new Set(listed.filter(/** @returns {id is string} */ (id) => typeof id === 'string' && id !== ''))];
|
|
77
|
+
if (active.length) return active;
|
|
78
|
+
} catch {
|
|
79
|
+
// estado ilegível: segue para os planos.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const worksDir = join(root, 'docs', 'trabalhos');
|
|
84
|
+
if (!existsSync(worksDir)) return [];
|
|
85
|
+
|
|
86
|
+
return readdirSync(worksDir).filter((id) => {
|
|
87
|
+
const plan = join(worksDir, id, 'PLAN.md');
|
|
88
|
+
if (!existsSync(plan)) return false;
|
|
89
|
+
const frontmatter = /^\s*---\r?\n([\s\S]*?)\r?\n---/.exec(readFileSync(plan, 'utf8'))?.[1] ?? '';
|
|
90
|
+
// Só o status do plano, na coluna zero; os status das tasks ficam indentados.
|
|
91
|
+
return /^status:\s*['"]?em-execucao['"]?\s*$/m.test(frontmatter);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* O trabalho ao qual atribuir um evento sem `--trabalho`: o único ativo. Com nenhum ou vários, retorna
|
|
97
|
+
* null e o evento vai para _geral.jsonl, para não cair no rastro do trabalho errado.
|
|
98
|
+
* @param {string} root
|
|
99
|
+
* @returns {string | null}
|
|
100
|
+
*/
|
|
101
|
+
export function findActiveWork(root) {
|
|
102
|
+
const active = findActiveWorks(root);
|
|
103
|
+
return active.length === 1 ? (active[0] ?? null) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Renomeia o arquivo para `.1.jsonl` (empurrando os anteriores) quando a próxima linha passaria do limite.
|
|
108
|
+
* @param {string} file
|
|
109
|
+
* @param {number} incoming
|
|
110
|
+
* @param {number} maxBytes
|
|
111
|
+
*/
|
|
112
|
+
function rotateIfNeeded(file, incoming, maxBytes) {
|
|
113
|
+
if (!existsSync(file) || statSync(file).size + incoming <= maxBytes) return;
|
|
114
|
+
const base = file.slice(0, -'.jsonl'.length);
|
|
115
|
+
let last = 1;
|
|
116
|
+
while (existsSync(`${base}.${last}.jsonl`)) last++;
|
|
117
|
+
for (let i = last - 1; i >= 1; i--) renameSync(`${base}.${i}.jsonl`, `${base}.${i + 1}.jsonl`);
|
|
118
|
+
renameSync(file, `${base}.1.jsonl`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Grava um evento no rastro do trabalho (ou em _geral.jsonl, sem trabalho ativo).
|
|
123
|
+
* @param {string} root Raiz do projeto.
|
|
124
|
+
* @param {EventFields} fields
|
|
125
|
+
* @param {{ maxBytes?: number }} [options]
|
|
126
|
+
*/
|
|
127
|
+
export function recordEvent(root, fields, options = {}) {
|
|
128
|
+
const trabalhoId = fields.trabalhoId === undefined ? findActiveWork(root) : fields.trabalhoId;
|
|
129
|
+
const event = buildEvent({ ...fields, trabalhoId });
|
|
130
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
131
|
+
const size = Buffer.byteLength(line);
|
|
132
|
+
|
|
133
|
+
// Linhas de até 4 KB mantêm atômicos os appends concorrentes (O_APPEND) em sistemas POSIX.
|
|
134
|
+
if (size > MAX_LINE_BYTES) {
|
|
135
|
+
throw new Error(`Evento acima de 4 KB (${size} bytes): grave o texto longo num artefato e referencie o caminho em dados.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const dir = join(root, 'docs', 'eventos');
|
|
139
|
+
mkdirSync(dir, { recursive: true });
|
|
140
|
+
const file = join(dir, `${trabalhoId ?? '_geral'}.jsonl`);
|
|
141
|
+
rotateIfNeeded(file, size, options.maxBytes ?? MAX_FILE_BYTES);
|
|
142
|
+
appendFileSync(file, line);
|
|
143
|
+
return event;
|
|
144
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Detecção de segredos nas linhas adicionadas de um diff. Sem dependências.
|
|
3
|
+
// Os achados nunca incluem o valor encontrado, só o arquivo, a linha e o tipo.
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ arquivo: string, linha: number, tipo: string }} Finding */
|
|
7
|
+
|
|
8
|
+
const PATTERNS = [
|
|
9
|
+
{ tipo: 'chave de acesso AWS', padrao: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
10
|
+
{ tipo: 'segredo AWS', padrao: /aws_secret_access_key\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}/i },
|
|
11
|
+
{ tipo: 'token do GitHub', padrao: /\b(gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{50,})/ },
|
|
12
|
+
{ tipo: 'token do npm', padrao: /\bnpm_[A-Za-z0-9]{36}\b/ },
|
|
13
|
+
{ tipo: 'token do Slack', padrao: /\bxox[abprs]-[A-Za-z0-9-]{10,}/ },
|
|
14
|
+
{ tipo: 'chave de API do Google', padrao: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
15
|
+
{ tipo: 'chave privada', padrao: /-----BEGIN ([A-Z]+ )?PRIVATE KEY-----/ },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const ASSIGNED_SECRET = /\b(password|passwd|senha|secret|api[_-]?key|access[_-]?token|token)\b\s*[:=]\s*['"]([^'"]{8,})['"]/i;
|
|
19
|
+
const ENV_LINE = /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.*)$/;
|
|
20
|
+
const PLACEHOLDER = /^(x+|\*+|changeme|change-me|example|exemplo|placeholder|senha|password|secret|todo|<.*>|\$\{.*\}|(your|sua|seu)[-_ ].*)$/i;
|
|
21
|
+
|
|
22
|
+
/** @param {string} value */
|
|
23
|
+
function isPlaceholder(value) {
|
|
24
|
+
return PLACEHOLDER.test(value.trim().replace(/^['"]|['"]$/g, ''));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `.env`, `.env.local`, `.env.production`... mas não `.env.example`, `.env.sample` ou `.env.template`.
|
|
29
|
+
* @param {string} path
|
|
30
|
+
*/
|
|
31
|
+
function isEnvFile(path) {
|
|
32
|
+
const name = basename(path);
|
|
33
|
+
return /^\.env(\..+)?$/.test(name) && !/\.(example|sample|template|dist)$/.test(name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} path
|
|
38
|
+
* @param {string} content Linha adicionada, sem o `+`.
|
|
39
|
+
* @returns {string | null} Tipo do segredo encontrado.
|
|
40
|
+
*/
|
|
41
|
+
function secretIn(path, content) {
|
|
42
|
+
const known = PATTERNS.find(({ padrao }) => padrao.test(content));
|
|
43
|
+
if (known) return known.tipo;
|
|
44
|
+
|
|
45
|
+
if (isEnvFile(path)) {
|
|
46
|
+
const value = ENV_LINE.exec(content)?.[1]?.trim() ?? '';
|
|
47
|
+
return value && !isPlaceholder(value) ? 'valor em arquivo .env' : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const assigned = ASSIGNED_SECRET.exec(content);
|
|
51
|
+
return assigned && !isPlaceholder(assigned[2] ?? '') ? 'senha ou token atribuído no código' : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Varre as linhas adicionadas de um diff unificado (`git diff --cached`).
|
|
56
|
+
* @param {string} diff
|
|
57
|
+
* @returns {Finding[]}
|
|
58
|
+
*/
|
|
59
|
+
export function scanDiff(diff) {
|
|
60
|
+
/** @type {Finding[]} */
|
|
61
|
+
const findings = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
let file = '';
|
|
64
|
+
let line = 0;
|
|
65
|
+
|
|
66
|
+
for (const raw of diff.split('\n')) {
|
|
67
|
+
if (raw.startsWith('+++ ')) {
|
|
68
|
+
file = raw === '+++ /dev/null' ? '' : raw.slice(4).replace(/^b\//, '');
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(raw);
|
|
72
|
+
if (hunk) {
|
|
73
|
+
line = Number(hunk[1]);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!file || raw.startsWith('---')) continue;
|
|
77
|
+
|
|
78
|
+
if (raw.startsWith('+')) {
|
|
79
|
+
const tipo = secretIn(file, raw.slice(1));
|
|
80
|
+
const key = `${file}:${tipo}`;
|
|
81
|
+
if (tipo && !seen.has(key)) {
|
|
82
|
+
seen.add(key);
|
|
83
|
+
findings.push({ arquivo: file, linha: line, tipo });
|
|
84
|
+
}
|
|
85
|
+
line++;
|
|
86
|
+
} else if (raw.startsWith(' ')) {
|
|
87
|
+
line++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return findings;
|
|
91
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Classificação do prompt para o hook de lembrete: qual fluxo do harness o pedido pede.
|
|
3
|
+
// Sem dependências; roda a cada prompt, então precisa ser barata.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} Theme
|
|
7
|
+
* @property {'seguranca' | 'bug' | 'performance' | 'feature'} tema
|
|
8
|
+
* @property {string} comando Id do fluxo sugerido.
|
|
9
|
+
* @property {string} descricao Como o pedido é descrito na sugestão.
|
|
10
|
+
* @property {string} fluxo O que o fluxo garante.
|
|
11
|
+
* @property {RegExp[]} padroes Aplicados ao prompt normalizado (minúsculas, sem acento).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Em ordem de prioridade: quando um prompt casa com mais de um tema, vale o primeiro.
|
|
16
|
+
* @type {Theme[]}
|
|
17
|
+
*/
|
|
18
|
+
export const THEMES = [
|
|
19
|
+
{
|
|
20
|
+
tema: 'seguranca',
|
|
21
|
+
comando: 'auditar-seguranca',
|
|
22
|
+
descricao: 'um risco de segurança ou de dados pessoais',
|
|
23
|
+
fluxo: 'auditoria defensiva com conformidade LGPD e relatório',
|
|
24
|
+
padroes: [
|
|
25
|
+
/\bvazamento\b/,
|
|
26
|
+
/\bdados pessoais\b/,
|
|
27
|
+
/\blgpd\b/,
|
|
28
|
+
/\bsql injection\b/,
|
|
29
|
+
/\binjecao de sql\b/,
|
|
30
|
+
/\bxss\b/,
|
|
31
|
+
/\bcsrf\b/,
|
|
32
|
+
/\bvulnerabilidade/,
|
|
33
|
+
/\bpentest\b/,
|
|
34
|
+
/\b(senha|token|chave|segredo|credencial)s? [\w ]{0,25}\b(expost|vazad)/,
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
tema: 'bug',
|
|
39
|
+
comando: 'resolver-bug',
|
|
40
|
+
descricao: 'um relato de bug',
|
|
41
|
+
fluxo: 'investigação da causa raiz, plano aprovado e correção com TDD',
|
|
42
|
+
padroes: [
|
|
43
|
+
/\bnao (esta )?funciona/,
|
|
44
|
+
/\bparou de funcionar\b/,
|
|
45
|
+
/\b(dando|deu|da|com) erro\b/,
|
|
46
|
+
/\berro ao\b/,
|
|
47
|
+
/\b(valor|calculo|total|preco|frete|saldo|imposto|desconto)\b[\w ]{0,30}\berrad[oa]s?\b/,
|
|
48
|
+
/\b(esta|ficou|veio|saiu|vem) errad[oa]s?\b/,
|
|
49
|
+
/\bquebrou\b/,
|
|
50
|
+
/\bexception\b/,
|
|
51
|
+
/\bexcecao\b/,
|
|
52
|
+
/\bstack ?trace\b/,
|
|
53
|
+
/\bbug\b/,
|
|
54
|
+
/\bnao (esta )?(salvando|aparecendo|carregando|gravando|enviando)\b/,
|
|
55
|
+
/\bcrash/,
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
tema: 'performance',
|
|
60
|
+
comando: 'auditar-performance',
|
|
61
|
+
descricao: 'um problema de performance',
|
|
62
|
+
fluxo: 'medição e auditoria dos gargalos antes de otimizar',
|
|
63
|
+
padroes: [
|
|
64
|
+
/\blent[oa]s?\b/,
|
|
65
|
+
/\blentidao\b/,
|
|
66
|
+
/\bdemora/,
|
|
67
|
+
/\btravand/,
|
|
68
|
+
/\btimeout\b/,
|
|
69
|
+
/\bgargalo/,
|
|
70
|
+
/\bperformance\b/,
|
|
71
|
+
/\bconsumo (alto )?de (memoria|cpu)\b/,
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
tema: 'feature',
|
|
76
|
+
comando: 'nova-tarefa',
|
|
77
|
+
descricao: 'uma funcionalidade nova',
|
|
78
|
+
fluxo: 'spec e plano aprovados antes de implementar',
|
|
79
|
+
padroes: [
|
|
80
|
+
/\bimplementar\b/,
|
|
81
|
+
/\b(crie|criar|desenvolver|desenvolva) (uma |um )?(nova |novo )?(tela|funcionalidade|modulo|endpoint|relatorio|cadastro)/,
|
|
82
|
+
/\bnova funcionalidade\b/,
|
|
83
|
+
/\b(adicionar|adicione|incluir|inclua) (uma |um )?(nova |novo )?funcionalidade\b/,
|
|
84
|
+
/\bquero que o sistema\b/,
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Minúsculas, sem acento e com espaços simples.
|
|
91
|
+
* @param {string} text
|
|
92
|
+
*/
|
|
93
|
+
export function normalize(text) {
|
|
94
|
+
return text.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tema do prompt, ou null quando nenhum casa, quando o prompt já é um slash command ou quando é uma
|
|
99
|
+
* mensagem do próprio Claude Code (notificação de agent em segundo plano, lembrete do sistema).
|
|
100
|
+
* @param {string} prompt
|
|
101
|
+
* @returns {Omit<Theme, 'padroes'> | null}
|
|
102
|
+
*/
|
|
103
|
+
export function classifyPrompt(prompt) {
|
|
104
|
+
if (typeof prompt !== 'string' || prompt.trim().startsWith('/')) return null;
|
|
105
|
+
if (/<(task-notification|system-reminder)\b/.test(prompt)) return null;
|
|
106
|
+
const text = normalize(prompt);
|
|
107
|
+
const match = THEMES.find((theme) => theme.padroes.some((pattern) => pattern.test(text)));
|
|
108
|
+
if (!match) return null;
|
|
109
|
+
const { padroes: _padroes, ...theme } = match;
|
|
110
|
+
return theme;
|
|
111
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Leitura de trabalhos para os hooks de método: caminhos, tasks do PLAN.md e eventos do rastro.
|
|
3
|
+
// Sem dependências: o frontmatter é lido linha a linha, no formato do contrato de artefatos.
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join, relative, sep } from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Caminho relativo à raiz do projeto com `/`; null quando está fora dele.
|
|
9
|
+
* @param {string} root
|
|
10
|
+
* @param {string} file
|
|
11
|
+
*/
|
|
12
|
+
export function relativeToRoot(root, file) {
|
|
13
|
+
if (!file) return null;
|
|
14
|
+
const rel = relative(root, file).split(sep).join('/');
|
|
15
|
+
return !rel || rel.startsWith('..') ? null : rel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {string} content */
|
|
19
|
+
function frontmatter(content) {
|
|
20
|
+
return /^\s*---\r?\n([\s\S]*?)\r?\n---/.exec(content)?.[1] ?? '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tasks (id e status) do frontmatter de um PLAN.md. Só considera os itens no nível da lista
|
|
25
|
+
* `tasks`, para que textos em listas internas (como testes_criticos) não virem tasks.
|
|
26
|
+
* @param {string} content
|
|
27
|
+
* @returns {Array<{ id: string, status: string }>}
|
|
28
|
+
*/
|
|
29
|
+
export function planTasks(content) {
|
|
30
|
+
/** @type {Array<{ id?: string, status?: string }>} */
|
|
31
|
+
const tasks = [];
|
|
32
|
+
/** @type {{ id?: string, status?: string } | null} */
|
|
33
|
+
let current = null;
|
|
34
|
+
let inTasks = false;
|
|
35
|
+
let itemIndent = -1;
|
|
36
|
+
|
|
37
|
+
for (const line of frontmatter(content).split(/\r?\n/)) {
|
|
38
|
+
if (/^tasks:/.test(line)) {
|
|
39
|
+
inTasks = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!inTasks) continue;
|
|
43
|
+
if (/^\S/.test(line)) break;
|
|
44
|
+
|
|
45
|
+
const item = /^(\s*)-\s+(\w+):\s*(.*)$/.exec(line);
|
|
46
|
+
if (item && (itemIndent === -1 || (item[1] ?? '').length === itemIndent)) {
|
|
47
|
+
itemIndent = (item[1] ?? '').length;
|
|
48
|
+
current = {};
|
|
49
|
+
tasks.push(current);
|
|
50
|
+
assign(current, item[2] ?? '', item[3] ?? '');
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const field = /^(\s+)(\w+):\s*(.*)$/.exec(line);
|
|
54
|
+
if (field && current && (field[1] ?? '').length === itemIndent + 2) assign(current, field[2] ?? '', field[3] ?? '');
|
|
55
|
+
}
|
|
56
|
+
return /** @type {Array<{ id: string, status: string }>} */ (tasks.filter((task) => task.id));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{ id?: string, status?: string }} task
|
|
61
|
+
* @param {string} key
|
|
62
|
+
* @param {string} raw
|
|
63
|
+
*/
|
|
64
|
+
function assign(task, key, raw) {
|
|
65
|
+
if (key === 'id' || key === 'status') task[key] = raw.trim().replace(/^['"]|['"]$/g, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Eventos do rastro de um trabalho, incluindo arquivos rotacionados; null quando não há rastro nesta máquina.
|
|
70
|
+
* @param {string} root
|
|
71
|
+
* @param {string} workId
|
|
72
|
+
* @returns {Array<Record<string, unknown>> | null}
|
|
73
|
+
*/
|
|
74
|
+
export function workEvents(root, workId) {
|
|
75
|
+
const dir = join(root, 'docs', 'eventos');
|
|
76
|
+
if (!existsSync(dir)) return null;
|
|
77
|
+
const files = readdirSync(dir).filter(
|
|
78
|
+
(name) => name === `${workId}.jsonl` || (name.startsWith(`${workId}.`) && /^\d+\.jsonl$/.test(name.slice(workId.length + 1)))
|
|
79
|
+
);
|
|
80
|
+
if (!files.length) return null;
|
|
81
|
+
|
|
82
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
83
|
+
const events = [];
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
for (const line of readFileSync(join(dir, file), 'utf8').split('\n')) {
|
|
86
|
+
try {
|
|
87
|
+
events.push(JSON.parse(line));
|
|
88
|
+
} catch {
|
|
89
|
+
// linha vazia ou incompleta.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return events;
|
|
94
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Hook PreToolUse (Edit/Write): durante a execução de um trabalho, o SPEC e o PLAN dele não são reescritos,
|
|
3
|
+
// nem o CLAUDE.md e o SPEC.md da raiz enquanto houver algum trabalho em execução.
|
|
4
|
+
// Atualizar só o status das tasks (e a data de atualização) continua permitido.
|
|
5
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { runHook } from './lib/hook.mjs';
|
|
8
|
+
import { relativeToRoot, workEvents } from './lib/trabalho.mjs';
|
|
9
|
+
|
|
10
|
+
const WORK_CONTRACT = /^docs\/trabalhos\/([^/]+)\/(SPEC|PLAN)\.md$/;
|
|
11
|
+
const PROTECTED = [/^CLAUDE\.md$/, /^SPEC\.md$/, WORK_CONTRACT];
|
|
12
|
+
const ALLOWED_LINE = /^\s*(-\s+)?(status|atualizado_em):/;
|
|
13
|
+
|
|
14
|
+
/** @param {unknown} text */
|
|
15
|
+
function withoutAllowedLines(text) {
|
|
16
|
+
return String(text ?? '')
|
|
17
|
+
.split(/\r?\n/)
|
|
18
|
+
.filter((line) => !ALLOWED_LINE.test(line))
|
|
19
|
+
.join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* O trabalho tem task iniciada no rastro e ainda não foi concluído?
|
|
24
|
+
* @param {string} root
|
|
25
|
+
* @param {string} workId
|
|
26
|
+
*/
|
|
27
|
+
function isRunning(root, workId) {
|
|
28
|
+
const events = workEvents(root, workId);
|
|
29
|
+
return events !== null && events.some((event) => event.evento === 'task_iniciada') && !events.some((event) => event.evento === 'trabalho_concluido');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Trabalhos em execução afetados pelo arquivo: o dono do SPEC/PLAN pelo caminho ou, para os contratos
|
|
34
|
+
* da raiz, todos os que estão em execução.
|
|
35
|
+
* @param {string} root
|
|
36
|
+
* @param {string} rel
|
|
37
|
+
*/
|
|
38
|
+
function runningWorks(root, rel) {
|
|
39
|
+
const owner = WORK_CONTRACT.exec(rel)?.[1];
|
|
40
|
+
if (owner) return isRunning(root, owner) ? [owner] : [];
|
|
41
|
+
const worksDir = join(root, 'docs', 'trabalhos');
|
|
42
|
+
if (!existsSync(worksDir)) return [];
|
|
43
|
+
return readdirSync(worksDir).filter((id) => isRunning(root, id));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A edição só troca linhas de status e data?
|
|
48
|
+
* @param {string} root
|
|
49
|
+
* @param {string} rel
|
|
50
|
+
* @param {Record<string, any>} input
|
|
51
|
+
*/
|
|
52
|
+
function onlyStatusChange(root, rel, input) {
|
|
53
|
+
const toolInput = input.tool_input ?? {};
|
|
54
|
+
const sameEdit = (/** @type {Record<string, unknown>} */ edit) => withoutAllowedLines(edit.old_string) === withoutAllowedLines(edit.new_string);
|
|
55
|
+
|
|
56
|
+
if (input.tool_name === 'Edit') return sameEdit(toolInput);
|
|
57
|
+
if (input.tool_name === 'MultiEdit' && Array.isArray(toolInput.edits)) return toolInput.edits.every(sameEdit);
|
|
58
|
+
if (input.tool_name === 'Write') {
|
|
59
|
+
const file = join(root, rel);
|
|
60
|
+
return existsSync(file) && withoutAllowedLines(readFileSync(file, 'utf8')) === withoutAllowedLines(toolInput.content);
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await runHook('proteger-contratos', (input, { root }) => {
|
|
66
|
+
const rel = relativeToRoot(root, String(input.tool_input?.file_path ?? ''));
|
|
67
|
+
if (!rel || !PROTECTED.some((pattern) => pattern.test(rel))) return null;
|
|
68
|
+
|
|
69
|
+
const running = runningWorks(root, rel);
|
|
70
|
+
if (!running.length || onlyStatusChange(root, rel, input)) return null;
|
|
71
|
+
|
|
72
|
+
const works = running.join(', ');
|
|
73
|
+
const decisions = `docs/trabalhos/${running.length === 1 ? works : '<id>'}/DECISOES.md`;
|
|
74
|
+
return {
|
|
75
|
+
motivo:
|
|
76
|
+
`${rel} é contrato ${running.length === 1 ? `do trabalho ${works}, que está` : `dos trabalhos ${works}, que estão`} em execução, ` +
|
|
77
|
+
`e não deve ser reescrito agora. Registre a mudança de escopo em ${decisions} (ou abra um bloqueio em BLOQUEIOS.md); ` +
|
|
78
|
+
'no PLAN.md, só o status das tasks pode ser atualizado.',
|
|
79
|
+
};
|
|
80
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Hook PreToolUse (Bash `git commit`): impede commitar segredos. Nasce em modo bloqueio.
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { copyFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { runHook } from './lib/hook.mjs';
|
|
8
|
+
import { scanDiff } from './lib/segredos.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} root
|
|
12
|
+
* @param {string[]} args
|
|
13
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
14
|
+
*/
|
|
15
|
+
const runGit = (root, args, env) =>
|
|
16
|
+
execFileSync('git', args, { cwd: root, env, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Argumentos de um trecho do comando que chama o git, sem as aspas; `null` se o trecho não é git.
|
|
20
|
+
* @param {string} segment
|
|
21
|
+
*/
|
|
22
|
+
function gitArgs(segment) {
|
|
23
|
+
const words = segment.trim().split(/\s+/).map((word) => word.replace(/^['"]|['"]$/g, ''));
|
|
24
|
+
return words[0] === 'git' ? words.slice(1) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Diff do que o commit vai gravar. Quando o hook dispara, os `git add` do mesmo comando
|
|
29
|
+
* (`git add x && git commit`) ainda não rodaram: eles são reproduzidos numa cópia do índice,
|
|
30
|
+
* sem tocar no stage real. Com `-a`/`--all` no commit entram as mudanças rastreadas fora do stage.
|
|
31
|
+
* @param {string} root
|
|
32
|
+
* @param {string} command
|
|
33
|
+
*/
|
|
34
|
+
function diffToCommit(root, command) {
|
|
35
|
+
const realIndex = resolve(root, runGit(root, ['rev-parse', '--git-path', 'index']).trim());
|
|
36
|
+
const dir = mkdtempSync(join(tmpdir(), 'ai-harness-segredo-'));
|
|
37
|
+
const index = join(dir, 'index');
|
|
38
|
+
if (existsSync(realIndex)) copyFileSync(realIndex, index);
|
|
39
|
+
const env = { ...process.env, GIT_INDEX_FILE: index };
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
for (const segment of command.split(/&&|\|\||;|\n/)) {
|
|
43
|
+
const args = gitArgs(segment);
|
|
44
|
+
if (!args) continue;
|
|
45
|
+
try {
|
|
46
|
+
if (args[0] === 'add') runGit(root, args, env);
|
|
47
|
+
else if (args[0] === 'commit' && args.slice(1).some((arg) => arg === '--all' || /^-[A-Za-z]*a[A-Za-z]*$/.test(arg))) {
|
|
48
|
+
runGit(root, ['add', '-u'], env);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// O git vai recusar esse trecho de verdade; o que deu para simular continua valendo.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return runGit(root, ['diff', '--cached', '--no-color', '--unified=0'], env);
|
|
55
|
+
} finally {
|
|
56
|
+
rmSync(dir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await runHook('segredo-no-commit', (input, { root }) => {
|
|
61
|
+
const command = String(input.tool_input?.command ?? '');
|
|
62
|
+
if (!/\bgit\b[^;&|]*\bcommit\b/.test(command)) return null;
|
|
63
|
+
|
|
64
|
+
const findings = scanDiff(diffToCommit(root, command));
|
|
65
|
+
if (!findings.length) return null;
|
|
66
|
+
|
|
67
|
+
const list = findings
|
|
68
|
+
.slice(0, 10)
|
|
69
|
+
.map((finding) => `${finding.arquivo}:${finding.linha} (${finding.tipo})`)
|
|
70
|
+
.join('; ');
|
|
71
|
+
return {
|
|
72
|
+
motivo:
|
|
73
|
+
`o commit inclui possível segredo em ${list}. Tire o arquivo do stage (git restore --staged <arquivo>), ` +
|
|
74
|
+
'leve o valor para uma variável de ambiente e, se a credencial já saiu desta máquina, troque-a.',
|
|
75
|
+
};
|
|
76
|
+
});
|