@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.
Files changed (132) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +9 -2
  3. package/LICENSE +21 -0
  4. package/README.md +175 -140
  5. package/{src → dist}/catalog.js +5 -12
  6. package/dist/cli.js +180 -0
  7. package/dist/comentarios/command.js +74 -0
  8. package/dist/comentarios/instalar.js +33 -0
  9. package/dist/config/config.js +99 -0
  10. package/dist/contratos/artefatos.js +284 -0
  11. package/dist/contratos/eventos.js +103 -0
  12. package/dist/doctor/checks.js +424 -0
  13. package/dist/init.js +240 -0
  14. package/dist/install/atomic.js +51 -0
  15. package/dist/install/flags.js +99 -0
  16. package/dist/install/hooks.js +47 -0
  17. package/dist/install/lock.js +39 -0
  18. package/dist/install/plan.js +209 -0
  19. package/dist/install/selection.js +77 -0
  20. package/dist/install/settings.js +112 -0
  21. package/dist/memoria/indice.js +195 -0
  22. package/dist/migrar/migrar.js +426 -0
  23. package/dist/rastro/command.js +46 -0
  24. package/dist/run.js +86 -0
  25. package/dist/trabalhos/conflitos.js +115 -0
  26. package/dist/types.js +1 -0
  27. package/dist/update/legado.js +74 -0
  28. package/dist/update/update.js +284 -0
  29. package/package.json +17 -8
  30. package/templates/CLAUDE.md +59 -30
  31. package/templates/agents/auditor-plano.md +44 -0
  32. package/templates/agents/investigador.md +32 -0
  33. package/templates/agents/qa.md +44 -0
  34. package/templates/agents/revisor-testes.md +40 -0
  35. package/templates/catalog.json +24 -61
  36. package/templates/commands/atualizar-deps.md +43 -9
  37. package/templates/commands/diagrama.md +35 -10
  38. package/templates/commands/gerar-env.md +39 -10
  39. package/templates/commands/gerar-testes.md +42 -10
  40. package/templates/commands/git-commit-push.md +0 -0
  41. package/templates/commands/onboarding.md +44 -15
  42. package/templates/commands/refatorar.md +50 -10
  43. package/templates/commands/revisar-pr.md +56 -12
  44. package/templates/commands/sincronizar-claude-md.md +177 -9
  45. package/templates/commands/versao.md +0 -0
  46. package/templates/hooks/comentarios/check.mjs +210 -0
  47. package/templates/hooks/hooks.json +34 -0
  48. package/templates/hooks/lembrete.mjs +29 -0
  49. package/templates/hooks/lib/config.mjs +40 -0
  50. package/templates/hooks/lib/hook.mjs +85 -0
  51. package/templates/hooks/lib/rastro.mjs +144 -0
  52. package/templates/hooks/lib/segredos.mjs +91 -0
  53. package/templates/hooks/lib/temas.mjs +111 -0
  54. package/templates/hooks/lib/trabalho.mjs +94 -0
  55. package/templates/hooks/proteger-contratos.mjs +80 -0
  56. package/templates/hooks/segredo-no-commit.mjs +76 -0
  57. package/templates/hooks/sem-jargao.mjs +58 -0
  58. package/templates/hooks/task-so-fecha-verde.mjs +33 -0
  59. package/templates/legado/manifesto.json +1267 -0
  60. package/templates/rules/comentarios.md +166 -0
  61. package/templates/rules/harness-projeto.md +41 -0
  62. package/templates/skills/auditar-performance/SKILL.md +53 -0
  63. package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
  64. package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
  65. package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
  66. package/templates/skills/auditar-seguranca/SKILL.md +67 -0
  67. package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
  68. package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
  69. package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
  70. package/templates/skills/auditar-ui/SKILL.md +64 -0
  71. package/templates/skills/auditar-ui/references/lentes.md +33 -0
  72. package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
  73. package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
  74. package/templates/skills/evoluir-harness/SKILL.md +81 -0
  75. package/templates/skills/evoluir-harness/references/agent.md +83 -0
  76. package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
  77. package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
  78. package/templates/skills/evoluir-harness/references/design-system.md +56 -0
  79. package/templates/skills/evoluir-harness/references/hook.md +95 -0
  80. package/templates/skills/evoluir-harness/references/rule.md +67 -0
  81. package/templates/skills/evoluir-harness/references/skill.md +63 -0
  82. package/templates/skills/executar-plan/SKILL.md +130 -0
  83. package/templates/skills/executar-plan/references/delegacao.md +43 -0
  84. package/templates/skills/executar-plan/references/retomada.md +33 -0
  85. package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
  86. package/templates/skills/harness/SKILL.md +75 -0
  87. package/templates/skills/harness/references/auto-tag.md +118 -0
  88. package/templates/skills/harness/references/ignores.md +40 -0
  89. package/templates/skills/harness/references/scaffold.md +29 -0
  90. package/templates/skills/harness/references/validar-ideia.md +42 -0
  91. package/templates/skills/memoria/SKILL.md +52 -0
  92. package/templates/skills/nova-tarefa/SKILL.md +99 -0
  93. package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
  94. package/templates/skills/nova-tarefa/references/regras-do-plano.md +35 -0
  95. package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
  96. package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
  97. package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
  98. package/templates/skills/resolver-bug/SKILL.md +104 -0
  99. package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
  100. package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
  101. package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
  102. package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
  103. package/bin/cli.js +0 -122
  104. package/src/init.js +0 -317
  105. package/src/installer.js +0 -92
  106. package/src/run.js +0 -99
  107. package/templates/commands/auditar-performance.md +0 -27
  108. package/templates/commands/auditar-seguranca.md +0 -64
  109. package/templates/commands/auditar-ui.md +0 -38
  110. package/templates/commands/codigo-morto.md +0 -22
  111. package/templates/commands/configurar-auto-tag.md +0 -185
  112. package/templates/commands/configurar-comentarios.md +0 -543
  113. package/templates/commands/configurar-orquestrador.md +0 -13
  114. package/templates/commands/corrigir-review.md +0 -11
  115. package/templates/commands/criar-agents.md +0 -42
  116. package/templates/commands/criar-code-reviewer.md +0 -30
  117. package/templates/commands/criar-hooks.md +0 -32
  118. package/templates/commands/criar-rag.md +0 -25
  119. package/templates/commands/criar-rules.md +0 -31
  120. package/templates/commands/criar-skill.md +0 -22
  121. package/templates/commands/design-system.md +0 -27
  122. package/templates/commands/executar-plan.md +0 -36
  123. package/templates/commands/gerar-claude-md.md +0 -66
  124. package/templates/commands/gerar-ignore.md +0 -37
  125. package/templates/commands/gerar-spec.md +0 -33
  126. package/templates/commands/harness.md +0 -34
  127. package/templates/commands/nova-tarefa.md +0 -60
  128. package/templates/commands/resolver-bug.md +0 -25
  129. package/templates/commands/review-por-task.md +0 -16
  130. package/templates/commands/scaffold.md +0 -25
  131. package/templates/commands/validar-commands.md +0 -24
  132. 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
+ });