@fontdata/ai-harness 2.0.0 → 2.1.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 (131) 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 +174 -140
  5. package/{src → dist}/catalog.js +5 -12
  6. package/dist/cli.js +174 -0
  7. package/dist/comentarios/command.js +74 -0
  8. package/dist/comentarios/instalar.js +33 -0
  9. package/dist/config/config.js +98 -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 +108 -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/types.js +1 -0
  26. package/dist/update/legado.js +74 -0
  27. package/dist/update/update.js +284 -0
  28. package/package.json +17 -8
  29. package/templates/CLAUDE.md +59 -30
  30. package/templates/agents/auditor-plano.md +42 -0
  31. package/templates/agents/investigador.md +32 -0
  32. package/templates/agents/qa.md +44 -0
  33. package/templates/agents/revisor-testes.md +40 -0
  34. package/templates/catalog.json +24 -61
  35. package/templates/commands/atualizar-deps.md +43 -9
  36. package/templates/commands/diagrama.md +35 -10
  37. package/templates/commands/gerar-env.md +39 -10
  38. package/templates/commands/gerar-testes.md +42 -10
  39. package/templates/commands/git-commit-push.md +0 -0
  40. package/templates/commands/onboarding.md +44 -15
  41. package/templates/commands/refatorar.md +50 -10
  42. package/templates/commands/revisar-pr.md +56 -12
  43. package/templates/commands/sincronizar-claude-md.md +177 -9
  44. package/templates/commands/versao.md +0 -0
  45. package/templates/hooks/comentarios/check.mjs +210 -0
  46. package/templates/hooks/hooks.json +34 -0
  47. package/templates/hooks/lembrete.mjs +29 -0
  48. package/templates/hooks/lib/config.mjs +40 -0
  49. package/templates/hooks/lib/hook.mjs +85 -0
  50. package/templates/hooks/lib/rastro.mjs +130 -0
  51. package/templates/hooks/lib/segredos.mjs +91 -0
  52. package/templates/hooks/lib/temas.mjs +111 -0
  53. package/templates/hooks/lib/trabalho.mjs +94 -0
  54. package/templates/hooks/proteger-contratos.mjs +56 -0
  55. package/templates/hooks/segredo-no-commit.mjs +76 -0
  56. package/templates/hooks/sem-jargao.mjs +58 -0
  57. package/templates/hooks/task-so-fecha-verde.mjs +33 -0
  58. package/templates/legado/manifesto.json +1267 -0
  59. package/templates/rules/comentarios.md +166 -0
  60. package/templates/rules/harness-projeto.md +41 -0
  61. package/templates/skills/auditar-performance/SKILL.md +53 -0
  62. package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
  63. package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
  64. package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
  65. package/templates/skills/auditar-seguranca/SKILL.md +67 -0
  66. package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
  67. package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
  68. package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
  69. package/templates/skills/auditar-ui/SKILL.md +64 -0
  70. package/templates/skills/auditar-ui/references/lentes.md +33 -0
  71. package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
  72. package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
  73. package/templates/skills/evoluir-harness/SKILL.md +81 -0
  74. package/templates/skills/evoluir-harness/references/agent.md +83 -0
  75. package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
  76. package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
  77. package/templates/skills/evoluir-harness/references/design-system.md +56 -0
  78. package/templates/skills/evoluir-harness/references/hook.md +95 -0
  79. package/templates/skills/evoluir-harness/references/rule.md +67 -0
  80. package/templates/skills/evoluir-harness/references/skill.md +63 -0
  81. package/templates/skills/executar-plan/SKILL.md +114 -0
  82. package/templates/skills/executar-plan/references/delegacao.md +43 -0
  83. package/templates/skills/executar-plan/references/retomada.md +33 -0
  84. package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
  85. package/templates/skills/harness/SKILL.md +75 -0
  86. package/templates/skills/harness/references/auto-tag.md +118 -0
  87. package/templates/skills/harness/references/ignores.md +40 -0
  88. package/templates/skills/harness/references/scaffold.md +29 -0
  89. package/templates/skills/harness/references/validar-ideia.md +42 -0
  90. package/templates/skills/memoria/SKILL.md +52 -0
  91. package/templates/skills/nova-tarefa/SKILL.md +97 -0
  92. package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
  93. package/templates/skills/nova-tarefa/references/regras-do-plano.md +33 -0
  94. package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
  95. package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
  96. package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
  97. package/templates/skills/resolver-bug/SKILL.md +104 -0
  98. package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
  99. package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
  100. package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
  101. package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
  102. package/bin/cli.js +0 -122
  103. package/src/init.js +0 -317
  104. package/src/installer.js +0 -92
  105. package/src/run.js +0 -99
  106. package/templates/commands/auditar-performance.md +0 -27
  107. package/templates/commands/auditar-seguranca.md +0 -64
  108. package/templates/commands/auditar-ui.md +0 -38
  109. package/templates/commands/codigo-morto.md +0 -22
  110. package/templates/commands/configurar-auto-tag.md +0 -185
  111. package/templates/commands/configurar-comentarios.md +0 -543
  112. package/templates/commands/configurar-orquestrador.md +0 -13
  113. package/templates/commands/corrigir-review.md +0 -11
  114. package/templates/commands/criar-agents.md +0 -42
  115. package/templates/commands/criar-code-reviewer.md +0 -30
  116. package/templates/commands/criar-hooks.md +0 -32
  117. package/templates/commands/criar-rag.md +0 -25
  118. package/templates/commands/criar-rules.md +0 -31
  119. package/templates/commands/criar-skill.md +0 -22
  120. package/templates/commands/design-system.md +0 -27
  121. package/templates/commands/executar-plan.md +0 -36
  122. package/templates/commands/gerar-claude-md.md +0 -66
  123. package/templates/commands/gerar-ignore.md +0 -37
  124. package/templates/commands/gerar-spec.md +0 -33
  125. package/templates/commands/harness.md +0 -34
  126. package/templates/commands/nova-tarefa.md +0 -60
  127. package/templates/commands/resolver-bug.md +0 -25
  128. package/templates/commands/review-por-task.md +0 -16
  129. package/templates/commands/scaffold.md +0 -25
  130. package/templates/commands/validar-commands.md +0 -24
  131. package/templates/commands/validar-ideia.md +0 -36
@@ -0,0 +1,130 @@
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
+ * Trabalho ativo: o `trabalho_ativo` do .ai-harness/estado.json ou, sem ele,
64
+ * o único PLAN.md com `status: em-execucao`. Ambíguo ou ausente, retorna null.
65
+ * @param {string} root
66
+ * @returns {string | null}
67
+ */
68
+ export function findActiveWork(root) {
69
+ const statePath = join(root, '.ai-harness', 'estado.json');
70
+ if (existsSync(statePath)) {
71
+ try {
72
+ const active = JSON.parse(readFileSync(statePath, 'utf8')).trabalho_ativo;
73
+ if (typeof active === 'string' && active) return active;
74
+ } catch {
75
+ // estado ilegível: segue para os planos.
76
+ }
77
+ }
78
+
79
+ const worksDir = join(root, 'docs', 'trabalhos');
80
+ if (!existsSync(worksDir)) return null;
81
+
82
+ const running = readdirSync(worksDir).filter((id) => {
83
+ const plan = join(worksDir, id, 'PLAN.md');
84
+ if (!existsSync(plan)) return false;
85
+ const frontmatter = /^\s*---\r?\n([\s\S]*?)\r?\n---/.exec(readFileSync(plan, 'utf8'))?.[1] ?? '';
86
+ // Só o status do plano, na coluna zero; os status das tasks ficam indentados.
87
+ return /^status:\s*['"]?em-execucao['"]?\s*$/m.test(frontmatter);
88
+ });
89
+ return running.length === 1 ? (running[0] ?? null) : null;
90
+ }
91
+
92
+ /**
93
+ * Renomeia o arquivo para `.1.jsonl` (empurrando os anteriores) quando a próxima linha passaria do limite.
94
+ * @param {string} file
95
+ * @param {number} incoming
96
+ * @param {number} maxBytes
97
+ */
98
+ function rotateIfNeeded(file, incoming, maxBytes) {
99
+ if (!existsSync(file) || statSync(file).size + incoming <= maxBytes) return;
100
+ const base = file.slice(0, -'.jsonl'.length);
101
+ let last = 1;
102
+ while (existsSync(`${base}.${last}.jsonl`)) last++;
103
+ for (let i = last - 1; i >= 1; i--) renameSync(`${base}.${i}.jsonl`, `${base}.${i + 1}.jsonl`);
104
+ renameSync(file, `${base}.1.jsonl`);
105
+ }
106
+
107
+ /**
108
+ * Grava um evento no rastro do trabalho (ou em _geral.jsonl, sem trabalho ativo).
109
+ * @param {string} root Raiz do projeto.
110
+ * @param {EventFields} fields
111
+ * @param {{ maxBytes?: number }} [options]
112
+ */
113
+ export function recordEvent(root, fields, options = {}) {
114
+ const trabalhoId = fields.trabalhoId === undefined ? findActiveWork(root) : fields.trabalhoId;
115
+ const event = buildEvent({ ...fields, trabalhoId });
116
+ const line = `${JSON.stringify(event)}\n`;
117
+ const size = Buffer.byteLength(line);
118
+
119
+ // Linhas de até 4 KB mantêm atômicos os appends concorrentes (O_APPEND) em sistemas POSIX.
120
+ if (size > MAX_LINE_BYTES) {
121
+ throw new Error(`Evento acima de 4 KB (${size} bytes): grave o texto longo num artefato e referencie o caminho em dados.`);
122
+ }
123
+
124
+ const dir = join(root, 'docs', 'eventos');
125
+ mkdirSync(dir, { recursive: true });
126
+ const file = join(dir, `${trabalhoId ?? '_geral'}.jsonl`);
127
+ rotateIfNeeded(file, size, options.maxBytes ?? MAX_FILE_BYTES);
128
+ appendFileSync(file, line);
129
+ return event;
130
+ }
@@ -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,56 @@
1
+ // @ts-check
2
+ // Hook PreToolUse (Edit/Write): durante a execução de um trabalho, SPEC, PLAN e CLAUDE.md não são reescritos.
3
+ // Atualizar só o status das tasks (e a data de atualização) continua permitido.
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { runHook } from './lib/hook.mjs';
7
+ import { findActiveWork } from './lib/rastro.mjs';
8
+ import { relativeToRoot, workEvents } from './lib/trabalho.mjs';
9
+
10
+ const PROTECTED = [/^CLAUDE\.md$/, /^SPEC\.md$/, /^docs\/trabalhos\/[^/]+\/(SPEC|PLAN)\.md$/];
11
+ const ALLOWED_LINE = /^\s*(-\s+)?(status|atualizado_em):/;
12
+
13
+ /** @param {unknown} text */
14
+ function withoutAllowedLines(text) {
15
+ return String(text ?? '')
16
+ .split(/\r?\n/)
17
+ .filter((line) => !ALLOWED_LINE.test(line))
18
+ .join('\n');
19
+ }
20
+
21
+ /**
22
+ * A edição só troca linhas de status e data?
23
+ * @param {string} root
24
+ * @param {string} rel
25
+ * @param {Record<string, any>} input
26
+ */
27
+ function onlyStatusChange(root, rel, input) {
28
+ const toolInput = input.tool_input ?? {};
29
+ const sameEdit = (/** @type {Record<string, unknown>} */ edit) => withoutAllowedLines(edit.old_string) === withoutAllowedLines(edit.new_string);
30
+
31
+ if (input.tool_name === 'Edit') return sameEdit(toolInput);
32
+ if (input.tool_name === 'MultiEdit' && Array.isArray(toolInput.edits)) return toolInput.edits.every(sameEdit);
33
+ if (input.tool_name === 'Write') {
34
+ const file = join(root, rel);
35
+ return existsSync(file) && withoutAllowedLines(readFileSync(file, 'utf8')) === withoutAllowedLines(toolInput.content);
36
+ }
37
+ return false;
38
+ }
39
+
40
+ await runHook('proteger-contratos', (input, { root }) => {
41
+ const rel = relativeToRoot(root, String(input.tool_input?.file_path ?? ''));
42
+ if (!rel || !PROTECTED.some((pattern) => pattern.test(rel))) return null;
43
+
44
+ const active = findActiveWork(root);
45
+ const events = active ? workEvents(root, active) : null;
46
+ const running =
47
+ events !== null && events.some((event) => event.evento === 'task_iniciada') && !events.some((event) => event.evento === 'trabalho_concluido');
48
+ if (!active || !running || onlyStatusChange(root, rel, input)) return null;
49
+
50
+ return {
51
+ motivo:
52
+ `${rel} é contrato do trabalho ${active}, que está em execução, e não deve ser reescrito agora. ` +
53
+ `Registre a mudança de escopo em docs/trabalhos/${active}/DECISOES.md (ou abra um bloqueio em BLOQUEIOS.md); ` +
54
+ 'no PLAN.md, só o status das tasks pode ser atualizado.',
55
+ };
56
+ });
@@ -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
+ });
@@ -0,0 +1,58 @@
1
+ // @ts-check
2
+ // Hook PostToolUse (Edit/Write): o relatório para o cliente é escrito na linguagem de quem usa o sistema.
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { runHook } from './lib/hook.mjs';
6
+ import { normalize } from './lib/temas.mjs';
7
+ import { relativeToRoot } from './lib/trabalho.mjs';
8
+
9
+ /** Termos já normalizados (minúsculas, sem acento). */
10
+ const TECHNICAL_TERMS = [
11
+ 'endpoint',
12
+ 'deploy',
13
+ 'commit',
14
+ 'merge',
15
+ 'branch',
16
+ 'pull request',
17
+ 'refactor',
18
+ 'refatoracao',
19
+ 'stack trace',
20
+ 'exception',
21
+ 'excecao',
22
+ 'null',
23
+ 'undefined',
24
+ 'query',
25
+ 'sql',
26
+ 'json',
27
+ 'api',
28
+ 'frontend',
29
+ 'backend',
30
+ 'hotfix',
31
+ 'regex',
32
+ 'cache',
33
+ 'payload',
34
+ 'schema',
35
+ 'timeout',
36
+ 'runtime',
37
+ 'script',
38
+ 'bug',
39
+ ];
40
+
41
+ await runHook('sem-jargao', (input, { root }) => {
42
+ const rel = relativeToRoot(root, String(input.tool_input?.file_path ?? ''));
43
+ if (!rel || !/^docs\/trabalhos\/[^/]+\/relatorios\/CLIENTE\.md$/.test(rel)) return null;
44
+
45
+ const file = join(root, rel);
46
+ if (!existsSync(file)) return null;
47
+
48
+ const body = readFileSync(file, 'utf8').replace(/^\s*---\r?\n[\s\S]*?\r?\n---/, '');
49
+ const words = ` ${normalize(body).replace(/[^a-z0-9]+/g, ' ')} `;
50
+ const found = TECHNICAL_TERMS.filter((term) => words.includes(` ${term} `));
51
+ if (!found.length) return null;
52
+
53
+ return {
54
+ motivo:
55
+ `o relatório para o cliente usa termos técnicos: ${found.slice(0, 8).join(', ')}. ` +
56
+ 'Reescreva na linguagem de quem usa o sistema (o que acontecia e o que muda para ele); o detalhe técnico fica no TECNICO.md.',
57
+ };
58
+ });
@@ -0,0 +1,33 @@
1
+ // @ts-check
2
+ // Hook PostToolUse (Edit/Write): avisa quando o PLAN.md tem task concluída sem suíte verde no rastro.
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { runHook } from './lib/hook.mjs';
6
+ import { planTasks, relativeToRoot, workEvents } from './lib/trabalho.mjs';
7
+
8
+ await runHook('task-so-fecha-verde', (input, { root }) => {
9
+ const rel = relativeToRoot(root, String(input.tool_input?.file_path ?? ''));
10
+ const match = rel ? /^docs\/trabalhos\/([^/]+)\/PLAN\.md$/.exec(rel) : null;
11
+ if (!rel || !match) return null;
12
+
13
+ const workId = match[1] ?? '';
14
+ const events = workEvents(root, workId);
15
+ if (!events || !existsSync(join(root, rel))) return null;
16
+
17
+ const green = new Set(events.filter((event) => event.evento === 'suite_verde').map((event) => event.task_id));
18
+ // Task concluída antes de o rastro existir (sem task_iniciada nem task_concluida) não é cobrada.
19
+ const tracked = new Set(
20
+ events.filter((event) => event.evento === 'task_iniciada' || event.evento === 'task_concluida').map((event) => event.task_id)
21
+ );
22
+ const missing = planTasks(readFileSync(join(root, rel), 'utf8'))
23
+ .filter((task) => task.status === 'concluida' && tracked.has(task.id) && !green.has(task.id))
24
+ .map((task) => task.id);
25
+ if (!missing.length) return null;
26
+
27
+ return {
28
+ motivo:
29
+ `${missing.join(', ')} está marcada como concluída em ${rel}, mas o rastro não registra suíte verde para ela. ` +
30
+ `Rode a suíte e registre o resultado (npx @fontdata/ai-harness evento suite_verde --trabalho ${workId} --task <id> ` +
31
+ `--dados '{"comando":"<comando>","total":<n>,"falhas":0}') ou volte a task para em-andamento.`,
32
+ };
33
+ });