@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,103 @@
1
+ /** Validação do rastro de eventos (docs/contratos/EVENTOS.md). */
2
+ export const EVENT_VERSION = 1;
3
+ export const EVENTS = [
4
+ 'trabalho_iniciado',
5
+ 'trabalho_concluido',
6
+ 'task_iniciada',
7
+ 'task_concluida',
8
+ 'suite_verde',
9
+ 'suite_vermelha',
10
+ 'hook_aviso',
11
+ 'hook_bloqueio',
12
+ 'agent_veredito',
13
+ 'bloqueio_aberto',
14
+ 'bloqueio_fechado',
15
+ 'decisao_registrada',
16
+ 'relatorio_gerado',
17
+ ];
18
+ export const EVENT_KEYS = ['v', 'ts', 'trabalho_id', 'evento', 'origem', 'nome', 'task_id', 'modo', 'resultado', 'dados'];
19
+ const ORIGINS = ['hook', 'comando', 'agent', 'cli'];
20
+ const MODES = ['aviso', 'bloqueio', 'desligado'];
21
+ const RESULTS = ['ok', 'aviso', 'bloqueio', 'falha'];
22
+ const TASK_EVENTS = new Set(['task_iniciada', 'task_concluida', 'suite_verde', 'suite_vermelha']);
23
+ const HOOK_EVENTS = new Set(['hook_aviso', 'hook_bloqueio']);
24
+ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
25
+ const TASK_ID = /^T-\d{2}\.\d{2}$/;
26
+ function validateLine(event, line) {
27
+ const problems = [];
28
+ const push = (regra, mensagem) => problems.push({ linha: line, regra, mensagem });
29
+ const keys = Object.keys(event);
30
+ const missing = EVENT_KEYS.filter((key) => !(key in event));
31
+ const extra = keys.filter((key) => !EVENT_KEYS.includes(key));
32
+ for (const key of missing)
33
+ push('chave-ausente', `Chave obrigatória ausente: ${key}.`);
34
+ for (const key of extra)
35
+ push('chave-extra', `Chave fora do contrato: ${key}.`);
36
+ if (!missing.length && !extra.length && keys.join() !== EVENT_KEYS.join()) {
37
+ push('ordem-das-chaves', `As chaves precisam seguir a ordem ${EVENT_KEYS.join(', ')}.`);
38
+ }
39
+ if (typeof event.v === 'number' && event.v > EVENT_VERSION) {
40
+ push('versao-futura', `Versão ${event.v} do formato é mais nova que a suportada (${EVENT_VERSION}).`);
41
+ }
42
+ else if ('v' in event && event.v !== EVENT_VERSION) {
43
+ push('formato-invalido', `v deve ser ${EVENT_VERSION}.`);
44
+ }
45
+ if ('evento' in event && !EVENTS.includes(String(event.evento))) {
46
+ push('evento-desconhecido', `Evento fora do vocabulário: ${String(event.evento)}.`);
47
+ }
48
+ if ('ts' in event && (typeof event.ts !== 'string' || !TIMESTAMP.test(event.ts))) {
49
+ push('formato-invalido', 'ts deve estar em ISO 8601 UTC com milissegundos (AAAA-MM-DDTHH:MM:SS.mmmZ).');
50
+ }
51
+ if ('origem' in event && !ORIGINS.includes(String(event.origem))) {
52
+ push('formato-invalido', `origem inválida: ${String(event.origem)}. Use: ${ORIGINS.join(', ')}.`);
53
+ }
54
+ if ('trabalho_id' in event && event.trabalho_id !== null && typeof event.trabalho_id !== 'string') {
55
+ push('formato-invalido', 'trabalho_id deve ser texto ou null.');
56
+ }
57
+ const eventName = String(event.evento);
58
+ if ('task_id' in event) {
59
+ if (event.task_id === null) {
60
+ if (TASK_EVENTS.has(eventName))
61
+ push('task-ausente', `O evento ${eventName} exige task_id.`);
62
+ }
63
+ else if (typeof event.task_id !== 'string' || !TASK_ID.test(event.task_id)) {
64
+ push('formato-invalido', 'task_id deve seguir o padrão T-SS.NN.');
65
+ }
66
+ }
67
+ if ('modo' in event) {
68
+ if (event.modo === null) {
69
+ if (HOOK_EVENTS.has(eventName))
70
+ push('modo-ausente', `O evento ${eventName} exige modo.`);
71
+ }
72
+ else if (!MODES.includes(String(event.modo))) {
73
+ push('formato-invalido', `modo inválido: ${String(event.modo)}. Use: ${MODES.join(', ')}.`);
74
+ }
75
+ }
76
+ if ('resultado' in event && event.resultado !== null && !RESULTS.includes(String(event.resultado))) {
77
+ push('formato-invalido', `resultado inválido: ${String(event.resultado)}. Use: ${RESULTS.join(', ')}.`);
78
+ }
79
+ if ('dados' in event && event.dados !== null && (typeof event.dados !== 'object' || Array.isArray(event.dados))) {
80
+ push('formato-invalido', 'dados deve ser um objeto ou null.');
81
+ }
82
+ return problems;
83
+ }
84
+ /** Valida o conteúdo de um arquivo de rastro; a última linha sem `\n` é ignorada (escrita em andamento). */
85
+ export function validateEventLog(content) {
86
+ const complete = content.split('\n').slice(0, -1);
87
+ return complete.flatMap((text, index) => {
88
+ const line = index + 1;
89
+ if (!text.trim())
90
+ return [];
91
+ let event;
92
+ try {
93
+ event = JSON.parse(text);
94
+ }
95
+ catch {
96
+ return [{ linha: line, regra: 'json-invalido', mensagem: 'A linha não é um JSON válido.' }];
97
+ }
98
+ if (typeof event !== 'object' || event === null || Array.isArray(event)) {
99
+ return [{ linha: line, regra: 'json-invalido', mensagem: 'A linha precisa ser um objeto JSON.' }];
100
+ }
101
+ return validateLine(event, line);
102
+ });
103
+ }
@@ -0,0 +1,424 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { allCommands, loadCatalog } from '../catalog.js';
5
+ import { CONFIG_PATH, readConfig } from '../config/config.js';
6
+ import { readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
7
+ import { validateEventLog } from '../contratos/eventos.js';
8
+ import { hashContent, LOCK_PATH, LOCK_VERSION } from '../install/lock.js';
9
+ import { SETTINGS_PATH } from '../install/settings.js';
10
+ import { adoptLegacyInstall } from '../update/legado.js';
11
+ const MIN_NODE_MAJOR = 22;
12
+ const PLUGIN_ID = 'fontdata@ai-harness';
13
+ const HARNESS_HOOKS_DIR = '.claude/hooks/ai-harness/';
14
+ const REINSTALL = 'Restaure com `npx @fontdata/ai-harness update --yes`.';
15
+ const CONTRACT_URL = 'https://github.com/fontdata-tecnologia/ai-harness/blob/master/docs/contratos/ARTEFATOS.md';
16
+ const EVENTS_CONTRACT_URL = 'https://github.com/fontdata-tecnologia/ai-harness/blob/master/docs/contratos/EVENTOS.md';
17
+ const EVENTS_DIR = join('docs', 'eventos');
18
+ /** Regras cuja violação impede ler o artefato ou seguir o plano; as demais são avisos. */
19
+ const CONTRACT_ERRORS = new Set(['yaml-invalido', 'schema-futuro', 'ciclo-dependencia']);
20
+ function checkNode(version) {
21
+ const major = Number(version.split('.')[0]);
22
+ if (major >= MIN_NODE_MAJOR)
23
+ return [];
24
+ return [
25
+ {
26
+ id: 'node-antigo',
27
+ severidade: 'erro',
28
+ mensagem: `O Node ${version} está abaixo do mínimo exigido (${MIN_NODE_MAJOR}).`,
29
+ correcao: `Atualize o Node.js para a versão ${MIN_NODE_MAJOR} ou superior.`,
30
+ },
31
+ ];
32
+ }
33
+ function checkLock(root) {
34
+ const file = join(root, LOCK_PATH);
35
+ if (!existsSync(file)) {
36
+ const legacy = adoptLegacyInstall(root);
37
+ if (legacy) {
38
+ return {
39
+ lock: null,
40
+ findings: [
41
+ {
42
+ id: 'lock-ausente-legado',
43
+ severidade: 'aviso',
44
+ mensagem: `Instalação ${legacy.pacote.versao} sem ${LOCK_PATH}: os arquivos em .claude/ não podem ser conferidos nem atualizados.`,
45
+ correcao: 'Rode `npx @fontdata/ai-harness update` para ver as mudanças e `update --yes` para migrar para a versão atual.',
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ return {
51
+ lock: null,
52
+ findings: [
53
+ {
54
+ id: 'lock-ausente',
55
+ severidade: 'aviso',
56
+ mensagem: `Nenhuma instalação do ai-harness registrada neste projeto (${LOCK_PATH} não existe).`,
57
+ correcao: 'Rode `npx @fontdata/ai-harness init` para instalar. Se você usa só o modo npx ou o plugin global, ignore este aviso.',
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ let lock;
63
+ try {
64
+ lock = JSON.parse(readFileSync(file, 'utf8'));
65
+ }
66
+ catch {
67
+ return {
68
+ lock: null,
69
+ findings: [
70
+ {
71
+ id: 'lock-invalido',
72
+ severidade: 'erro',
73
+ mensagem: `O ${LOCK_PATH} está com JSON inválido.`,
74
+ correcao: `Restaure o arquivo pelo git (\`git checkout -- ${LOCK_PATH}\`) ou reinstale com \`npx @fontdata/ai-harness init --yes\`.`,
75
+ },
76
+ ],
77
+ };
78
+ }
79
+ if (lock.lock_versao > LOCK_VERSION) {
80
+ return {
81
+ lock: null,
82
+ findings: [
83
+ {
84
+ id: 'lock-futuro',
85
+ severidade: 'erro',
86
+ mensagem: `O ${LOCK_PATH} foi gerado por uma versão mais nova do ai-harness (lock_versao ${lock.lock_versao}).`,
87
+ correcao: 'Atualize o pacote: `npx @fontdata/ai-harness@latest doctor`.',
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ return { findings: [], lock };
93
+ }
94
+ function checkFiles(root, lock) {
95
+ const findings = [];
96
+ for (const item of lock.itens) {
97
+ for (const entry of item.arquivos) {
98
+ const file = join(root, entry.caminho);
99
+ if (!existsSync(file)) {
100
+ findings.push({
101
+ id: 'arquivo-faltando',
102
+ severidade: 'erro',
103
+ mensagem: `Arquivo instalado não encontrado: ${entry.caminho} (${item.tipo} ${item.id}).`,
104
+ correcao: REINSTALL,
105
+ });
106
+ }
107
+ else if (hashContent(readFileSync(file)) !== entry.sha256) {
108
+ findings.push({
109
+ id: 'arquivo-modificado',
110
+ severidade: 'aviso',
111
+ mensagem: `Arquivo alterado localmente desde a instalação: ${entry.caminho}.`,
112
+ correcao: 'Se a alteração é intencional, mantenha: atualizações não sobrescrevem arquivos alterados sem confirmação.',
113
+ });
114
+ }
115
+ }
116
+ }
117
+ return findings;
118
+ }
119
+ function checkRequiredSkills(root, lock) {
120
+ const installed = new Set(lock.itens.filter((item) => item.tipo === 'comando' || item.tipo === 'skill').map((item) => item.id));
121
+ const missing = new Map();
122
+ for (const cmd of allCommands(loadCatalog())) {
123
+ if (!installed.has(cmd.id))
124
+ continue;
125
+ for (const skill of cmd.requiresSkills ?? []) {
126
+ if (!existsSync(join(root, '.claude', 'skills', skill, 'SKILL.md')) && !missing.has(skill))
127
+ missing.set(skill, cmd.id);
128
+ }
129
+ }
130
+ return [...missing].map(([skill, cmd]) => ({
131
+ id: 'skill-exigida-ausente',
132
+ severidade: 'erro',
133
+ mensagem: `O comando /${cmd} exige a skill ${skill}, que não está instalada.`,
134
+ correcao: `Reinstale com \`npx @fontdata/ai-harness init --comandos ${cmd} --sem-harness --conflito sobrescrever --yes\`.`,
135
+ }));
136
+ }
137
+ function checkAgents(root) {
138
+ const dir = join(root, '.claude', 'agents');
139
+ if (!existsSync(dir))
140
+ return [];
141
+ return readdirSync(dir)
142
+ .filter((name) => name.endsWith('.md'))
143
+ .filter((name) => {
144
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(readFileSync(join(dir, name), 'utf8'))?.[1] ?? '';
145
+ return !/^tools:\s*\S/m.test(frontmatter);
146
+ })
147
+ .map((name) => ({
148
+ id: 'agent-sem-tools',
149
+ severidade: 'aviso',
150
+ mensagem: `O agent .claude/agents/${name} não declara tools: e herda todas as ferramentas da sessão.`,
151
+ correcao: 'Declare `tools:` só com as ferramentas de que ele precisa (ex.: `tools: Read, Grep, Glob`).',
152
+ }));
153
+ }
154
+ function hookCommands(hooks) {
155
+ return Object.values(hooks).flatMap((groups) => (Array.isArray(groups) ? groups : []).flatMap((group) => (group.hooks ?? []).map((hook) => hook.command ?? '')));
156
+ }
157
+ function checkSettings(root) {
158
+ const file = join(root, SETTINGS_PATH);
159
+ if (!existsSync(file))
160
+ return [];
161
+ let settings;
162
+ try {
163
+ settings = JSON.parse(readFileSync(file, 'utf8'));
164
+ if (typeof settings !== 'object' || settings === null || Array.isArray(settings))
165
+ throw new Error('não é objeto');
166
+ }
167
+ catch {
168
+ return [
169
+ {
170
+ id: 'settings-invalido',
171
+ severidade: 'erro',
172
+ mensagem: `O ${SETTINGS_PATH} está com JSON inválido; o Claude Code ignora hooks e permissões dele.`,
173
+ correcao: 'Corrija o JSON do arquivo (ou restaure pelo git) antes de instalar ou atualizar hooks.',
174
+ },
175
+ ];
176
+ }
177
+ const findings = [];
178
+ const commands = hookCommands(settings.hooks ?? {});
179
+ for (const command of commands) {
180
+ for (const match of command.matchAll(/[^\s"']*\.claude\/hooks\/[^\s"']+/g)) {
181
+ const path = match[0].slice(match[0].indexOf('.claude/hooks/'));
182
+ if (existsSync(join(root, path)))
183
+ continue;
184
+ findings.push({
185
+ id: 'hook-script-ausente',
186
+ severidade: 'erro',
187
+ mensagem: `O hook "${command}" aponta para ${path}, que não existe.`,
188
+ correcao: 'Reinstale os hooks com `npx @fontdata/ai-harness init` ou remova a entrada do settings.json.',
189
+ });
190
+ }
191
+ }
192
+ const plugins = settings.enabledPlugins;
193
+ const pluginEnabled = Array.isArray(plugins) ? plugins.includes(PLUGIN_ID) : plugins?.[PLUGIN_ID] === true;
194
+ if (pluginEnabled && commands.some((command) => command.includes(HARNESS_HOOKS_DIR))) {
195
+ findings.push({
196
+ id: 'hooks-duplicados',
197
+ severidade: 'aviso',
198
+ mensagem: `O plugin ${PLUGIN_ID} está habilitado e o projeto também registra os hooks do harness no ${SETTINGS_PATH}; cada hook roda duas vezes.`,
199
+ correcao: `Remova do settings.json os hooks de ${HARNESS_HOOKS_DIR} ou desabilite o plugin neste projeto.`,
200
+ });
201
+ }
202
+ return findings;
203
+ }
204
+ function checkGitIgnore(root) {
205
+ if (!existsSync(join(root, '.git')))
206
+ return [];
207
+ try {
208
+ // `check-ignore -q` sai com 0 quando o caminho está ignorado e com 1 quando não está.
209
+ execFileSync('git', ['check-ignore', '-q', LOCK_PATH], { cwd: root, stdio: 'ignore' });
210
+ }
211
+ catch {
212
+ return [];
213
+ }
214
+ return [
215
+ {
216
+ id: 'ai-harness-ignorado',
217
+ severidade: 'erro',
218
+ mensagem: 'A pasta .ai-harness/ está ignorada pelo git, mas o lock e a configuração do harness precisam ser versionados.',
219
+ correcao: 'Remova .ai-harness/ do .gitignore; só a subpasta .ai-harness/memoria/ pode continuar ignorada.',
220
+ },
221
+ ];
222
+ }
223
+ function checkWorks(root) {
224
+ return validateAllWorks(root).map((problem) => ({
225
+ id: `contrato-${problem.regra}`,
226
+ severidade: CONTRACT_ERRORS.has(problem.regra) ? 'erro' : 'aviso',
227
+ mensagem: `${problem.arquivo}${problem.linha ? ` (linha ${problem.linha})` : ''}: ${problem.mensagem}`,
228
+ correcao: `Ajuste o artefato conforme o contrato: ${CONTRACT_URL}`,
229
+ }));
230
+ }
231
+ function checkStaleBlocks(root, now, maxDays) {
232
+ const dir = join(root, WORKS_DIR);
233
+ if (!existsSync(dir))
234
+ return [];
235
+ const findings = [];
236
+ for (const work of readdirSync(dir)) {
237
+ const rel = `docs/trabalhos/${work}/BLOQUEIOS.md`;
238
+ if (!existsSync(join(root, rel)))
239
+ continue;
240
+ const { data } = readArtifact(rel, readFileSync(join(root, rel), 'utf8'));
241
+ const entries = Array.isArray(data?.bloqueios) ? data.bloqueios : [];
242
+ for (const entry of entries) {
243
+ if (typeof entry !== 'object' || entry === null)
244
+ continue;
245
+ const { id, status, aberto_em: openedAt } = entry;
246
+ if (status !== 'aberto' || typeof openedAt !== 'string')
247
+ continue;
248
+ const opened = Date.parse(`${openedAt}T00:00:00Z`);
249
+ if (Number.isNaN(opened))
250
+ continue;
251
+ const age = Math.floor((now.getTime() - opened) / 86_400_000);
252
+ if (age <= maxDays)
253
+ continue;
254
+ findings.push({
255
+ id: 'bloqueio-antigo',
256
+ severidade: 'aviso',
257
+ mensagem: `O bloqueio ${String(id)} em ${rel} está aberto há ${age} dias.`,
258
+ correcao: 'Resolva o bloqueio registrando a decisão em DECISOES.md, ou replaneje a task que ele parou.',
259
+ });
260
+ }
261
+ }
262
+ return findings;
263
+ }
264
+ function eventLogFiles(root) {
265
+ const dir = join(root, EVENTS_DIR);
266
+ if (!existsSync(dir))
267
+ return [];
268
+ return readdirSync(dir)
269
+ .filter((name) => name.endsWith('.jsonl'))
270
+ .sort();
271
+ }
272
+ function checkEventLogs(root) {
273
+ return eventLogFiles(root).flatMap((name) => validateEventLog(readFileSync(join(root, EVENTS_DIR, name), 'utf8')).map((problem) => ({
274
+ id: `rastro-${problem.regra}`,
275
+ severidade: problem.regra === 'versao-futura' ? 'erro' : 'aviso',
276
+ mensagem: `docs/eventos/${name} (linha ${problem.linha}): ${problem.mensagem}`,
277
+ correcao: `O rastro é append-only: corrija o hook ou comando que gravou a linha. Contrato: ${EVENTS_CONTRACT_URL}`,
278
+ })));
279
+ }
280
+ /** Arquivos de rastro de um trabalho, incluindo os rotacionados (`<id>.N.jsonl`). */
281
+ function workEventLogs(root, workId) {
282
+ return eventLogFiles(root).filter((name) => {
283
+ if (name === `${workId}.jsonl`)
284
+ return true;
285
+ const middle = name.slice(workId.length + 1, -'.jsonl'.length);
286
+ return name.startsWith(`${workId}.`) && /^\d+$/.test(middle);
287
+ });
288
+ }
289
+ /** R-09: uma task só fecha com suíte verde registrada. Sem rastro nesta máquina, não há como conferir. */
290
+ function checkCompletedTasks(root) {
291
+ const worksDir = join(root, WORKS_DIR);
292
+ if (!existsSync(worksDir))
293
+ return [];
294
+ const findings = [];
295
+ for (const workId of readdirSync(worksDir)) {
296
+ const planRel = `docs/trabalhos/${workId}/PLAN.md`;
297
+ if (!existsSync(join(root, planRel)))
298
+ continue;
299
+ const logs = workEventLogs(root, workId);
300
+ if (!logs.length)
301
+ continue;
302
+ const { data } = readArtifact(planRel, readFileSync(join(root, planRel), 'utf8'));
303
+ const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
304
+ const completed = tasks
305
+ .filter((task) => typeof task === 'object' && task !== null)
306
+ .filter((task) => task.status === 'concluida' && typeof task.id === 'string')
307
+ .map((task) => String(task.id));
308
+ if (!completed.length)
309
+ continue;
310
+ const green = new Set();
311
+ const tracked = new Set();
312
+ for (const log of logs) {
313
+ for (const line of readFileSync(join(root, EVENTS_DIR, log), 'utf8').split('\n')) {
314
+ try {
315
+ const event = JSON.parse(line);
316
+ if (typeof event.task_id !== 'string')
317
+ continue;
318
+ if (event.evento === 'suite_verde')
319
+ green.add(event.task_id);
320
+ if (event.evento === 'task_iniciada' || event.evento === 'task_concluida')
321
+ tracked.add(event.task_id);
322
+ }
323
+ catch {
324
+ // linha vazia ou fora do contrato: já apontada pela validação do rastro.
325
+ }
326
+ }
327
+ }
328
+ // Task concluída antes de o rastro existir (sem task_iniciada nem task_concluida) não é cobrada.
329
+ for (const taskId of completed.filter((id) => tracked.has(id) && !green.has(id))) {
330
+ findings.push({
331
+ id: 'task-concluida-sem-suite-verde',
332
+ severidade: 'aviso',
333
+ mensagem: `A task ${taskId} de ${planRel} está concluída, mas o rastro não registra suite_verde para ela.`,
334
+ correcao: 'Rode a suíte e registre o resultado (`npx @fontdata/ai-harness evento suite_verde --task <id> ...`) ou reabra a task.',
335
+ });
336
+ }
337
+ }
338
+ return findings;
339
+ }
340
+ /** Comando e skill com o mesmo nome: o comando vence (D-23) e o fluxo da skill nunca roda. */
341
+ function checkShadowedCommands(root) {
342
+ const commandsDir = join(root, '.claude', 'commands');
343
+ if (!existsSync(commandsDir))
344
+ return [];
345
+ const findings = [];
346
+ for (const name of readdirSync(commandsDir).filter((file) => file.endsWith('.md')).sort()) {
347
+ const id = name.slice(0, -'.md'.length);
348
+ if (existsSync(join(root, '.claude', 'skills', id, 'SKILL.md'))) {
349
+ findings.push({
350
+ id: 'comando-sombreia-skill',
351
+ severidade: 'erro',
352
+ mensagem: `.claude/commands/${name} tem o mesmo nome da skill ${id}: o comando vence e o fluxo da skill nunca roda.`,
353
+ correcao: 'Renomeie ou apague um dos dois. Se o comando veio do harness e foi editado, `npx @fontdata/ai-harness update --yes` o renomeia para <id>-legado.md.',
354
+ });
355
+ }
356
+ if (/-legado(-\d+)?$/.test(id)) {
357
+ findings.push({
358
+ id: 'comando-legado',
359
+ severidade: 'aviso',
360
+ mensagem: `.claude/commands/${name} é a versão personalizada de um comando que saiu do harness.`,
361
+ correcao: 'Leve o que for específico do projeto para uma rule em .claude/rules/ e apague o arquivo.',
362
+ });
363
+ }
364
+ }
365
+ return findings;
366
+ }
367
+ /** O padrão de comentários agora é só a rule, carregada sozinha; o bloco antigo no CLAUDE.md repete o texto e cita as pastas antigas. */
368
+ function checkOldCommentsBlock(root) {
369
+ const file = join(root, 'CLAUDE.md');
370
+ if (!existsSync(file))
371
+ return [];
372
+ const content = readFileSync(file, 'utf8');
373
+ const heading = /^##\s+Coment[aá]rios em c[oó]digo\s*$/m.exec(content);
374
+ if (!heading)
375
+ return [];
376
+ const rest = content.slice(heading.index + heading[0].length);
377
+ const next = rest.search(/^##\s/m);
378
+ const block = next < 0 ? rest : rest.slice(0, next);
379
+ if (!/@\.claude\/rules\/comentarios\.md|docs\/(specs|plans|bugs|investigacoes)/.test(block))
380
+ return [];
381
+ return [
382
+ {
383
+ id: 'bloco-comentarios-antigo',
384
+ severidade: 'aviso',
385
+ mensagem: 'O CLAUDE.md tem o bloco "Comentários em código" do /configurar-comentarios antigo, que importa a rule ou cita as pastas antigas.',
386
+ correcao: 'Apague o bloco: a rule .claude/rules/comentarios.md é carregada sozinha em toda sessão, e o update a mantém atualizada.',
387
+ },
388
+ ];
389
+ }
390
+ function checkConfig(problems) {
391
+ return problems.map((mensagem) => ({
392
+ id: 'config-invalida',
393
+ severidade: 'aviso',
394
+ mensagem,
395
+ correcao: `Corrija o ${CONFIG_PATH}; enquanto isso, valem os padrões (segurança em bloqueio, demais hooks em aviso).`,
396
+ }));
397
+ }
398
+ /** Diagnostica a instalação do harness no projeto; nunca altera arquivos. */
399
+ export function runDoctor(root, options = {}) {
400
+ const { findings: lockFindings, lock } = checkLock(root);
401
+ const { config, problems: configProblems } = readConfig(root);
402
+ return [
403
+ ...checkNode(options.nodeVersion ?? process.versions.node),
404
+ ...lockFindings,
405
+ ...(lock ? [...checkFiles(root, lock), ...checkRequiredSkills(root, lock)] : []),
406
+ ...checkShadowedCommands(root),
407
+ ...checkOldCommentsBlock(root),
408
+ ...checkAgents(root),
409
+ ...checkSettings(root),
410
+ ...checkGitIgnore(root),
411
+ ...checkConfig(configProblems),
412
+ ...checkWorks(root),
413
+ ...checkStaleBlocks(root, options.now ?? new Date(), options.staleBlockDays ?? config.bloqueios.dias_para_aviso),
414
+ ...checkEventLogs(root),
415
+ ...checkCompletedTasks(root),
416
+ ];
417
+ }
418
+ export function formatFindings(findings) {
419
+ if (!findings.length)
420
+ return 'Nenhum problema encontrado.';
421
+ const errors = findings.filter((finding) => finding.severidade === 'erro').length;
422
+ const lines = findings.map((finding) => `${finding.severidade === 'erro' ? '✖ erro ' : '⚠ aviso'} [${finding.id}] ${finding.mensagem}\n → ${finding.correcao}`);
423
+ return `${lines.join('\n')}\n\n${errors} erro(s), ${findings.length - errors} aviso(s).`;
424
+ }