@fontdata/ai-harness 2.2.0 → 2.3.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdata",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Harness de engenharia com IA da Fontdata para Claude Code",
5
5
  "author": { "name": "Fontdata" },
6
6
  "commands": ["./templates/commands"],
package/README.md CHANGED
@@ -108,9 +108,9 @@ duas vezes.
108
108
  | Subcomando | O que faz |
109
109
  |---|---|
110
110
  | `init [caminho]` | Instala comandos, skills, agents e hooks no projeto |
111
- | `doctor [caminho]` | Diagnostica instalação, contrato dos documentos, rastro, bloqueios antigos e versão do Node |
111
+ | `doctor [caminho]` | Diagnostica instalação, contrato dos documentos, rastro, bloqueios antigos, citações às pastas antigas de `docs/` no código e versão do Node |
112
112
  | `update [caminho]` | Atualiza o que foi instalado sem sobrescrever alterações locais (`--check`, `--yes`, `--forcar`) |
113
- | `migrar [caminho]` | Move os documentos do layout anterior para `docs/trabalhos/<id>/` (`--yes`) |
113
+ | `migrar [caminho]` | Move os documentos do layout anterior para `docs/trabalhos/<id>/` (`--yes`); `--reclassificar` corrige planos migrados que ficaram em execução |
114
114
  | `memoria [caminho]` | Índice de quem mexeu em cada arquivo (`--arquivo <caminho>`, `--regressao <id>`) |
115
115
  | `evento <evento>` | Grava no rastro do trabalho (usado por fluxos e agents) |
116
116
  | `conflitos <id>` | Aponta trabalhos em execução que mexem nos mesmos arquivos; `--aguardar` espera liberar (usado pelo `/executar-plan`) |
package/dist/cli.js CHANGED
@@ -48,6 +48,7 @@ Gerenciar:
48
48
  doctor [caminho] Diagnostica a instalação (arquivos, lock, skills, agents, hooks, Node)
49
49
  update [caminho] Atualiza o que foi instalado: --check só lista, --yes aplica, --forcar sobrescreve conflitos
50
50
  migrar [caminho] Move docs/specs, plans, bugs e investigacoes para docs/trabalhos/<id>/ (--yes aplica)
51
+ --reclassificar: planos já migrados em em-execucao viram concluido ou legado
51
52
  comentarios [caminho] Instala o hook de comentários e a catraca do legado (--yes aplica)
52
53
  memoria [caminho] Índice de quem mexeu em cada arquivo (--arquivo <caminho>, --regressao <id do bug>)
53
54
  evento <evento> Grava no rastro do trabalho (--trabalho, --task, --nome, --resultado, --dados '{json}')
@@ -19,7 +19,7 @@ export const ARTIFACT_TYPES = {
19
19
  };
20
20
  export const STATUS_BY_TYPE = {
21
21
  spec: ['rascunho', 'aguardando-aprovacao', 'aprovado', 'substituido'],
22
- plano: ['rascunho', 'aguardando-aprovacao', 'em-execucao', 'concluido', 'cancelado'],
22
+ plano: ['rascunho', 'aguardando-aprovacao', 'em-execucao', 'concluido', 'cancelado', 'legado'],
23
23
  decisoes: ['ativo'],
24
24
  bloqueios: ['aberto', 'resolvido'],
25
25
  relatorio_tecnico: ['rascunho', 'final'],
@@ -8,6 +8,7 @@ import { validateEventLog } from '../contratos/eventos.js';
8
8
  import { hashContent, LOCK_PATH, LOCK_VERSION } from '../install/lock.js';
9
9
  import { SETTINGS_PATH } from '../install/settings.js';
10
10
  import { adoptLegacyInstall } from '../update/legado.js';
11
+ import { checkLegacyDocsCitations, checkMigratedPlansRunning } from './migracao.js';
11
12
  const MIN_NODE_MAJOR = 22;
12
13
  const PLUGIN_ID = 'fontdata@ai-harness';
13
14
  const HARNESS_HOOKS_DIR = '.claude/hooks/ai-harness/';
@@ -413,6 +414,8 @@ export function runDoctor(root, options = {}) {
413
414
  ...checkStaleBlocks(root, options.now ?? new Date(), options.staleBlockDays ?? config.bloqueios.dias_para_aviso),
414
415
  ...checkEventLogs(root),
415
416
  ...checkCompletedTasks(root),
417
+ ...checkMigratedPlansRunning(root),
418
+ ...checkLegacyDocsCitations(root),
416
419
  ];
417
420
  }
418
421
  export function formatFindings(findings) {
@@ -0,0 +1,97 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { extname, join } from 'node:path';
3
+ import { readArtifact, WORKS_DIR } from '../contratos/artefatos.js';
4
+ /** As mesmas extensões e pastas ignoradas do hook de comentários (templates/hooks/comentarios/check.mjs). */
5
+ const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.cs', '.php', '.pas', '.dpr', '.sql', '.sh']);
6
+ const SKIPPED_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', 'out', 'vendor', 'generated', '__generated__']);
7
+ const MAX_FILE_BYTES = 1_000_000;
8
+ const LEGACY_CITATION = /\b[Dd]ocs\/(?:specs|plans|bugs|investigacoes)\//g;
9
+ const TEST_FILE = /\.(test|spec)\.[jt]sx?$/;
10
+ const COMMENT_LINE = /^\s*(\*|\(\*|\{(?!\$))/;
11
+ const COMMENT_MARKER = /\/\/|\/\*|#|--/;
12
+ const EXAMPLES_SHOWN = 5;
13
+ function codeFiles(root, dir = '', found = []) {
14
+ let entries;
15
+ try {
16
+ entries = readdirSync(join(root, dir), { withFileTypes: true });
17
+ }
18
+ catch {
19
+ return found;
20
+ }
21
+ for (const entry of entries) {
22
+ const rel = dir ? `${dir}/${entry.name}` : entry.name;
23
+ if (entry.isDirectory()) {
24
+ if (!entry.name.startsWith('.') && !SKIPPED_DIRS.has(entry.name))
25
+ codeFiles(root, rel, found);
26
+ }
27
+ else if (entry.isFile() && CODE_EXTENSIONS.has(extname(entry.name)) && !entry.name.endsWith('.d.ts') && !TEST_FILE.test(entry.name)) {
28
+ found.push(rel);
29
+ }
30
+ }
31
+ return found;
32
+ }
33
+ /**
34
+ * O `migrar` reescreve os links dos documentos, mas não toca no código: a citação às pastas antigas num comentário
35
+ * vira caminho quebrado, e a rule de comentários já proíbe citar docs/ no código.
36
+ */
37
+ export function checkLegacyDocsCitations(root) {
38
+ const places = [];
39
+ let total = 0;
40
+ for (const rel of codeFiles(root).sort()) {
41
+ const file = join(root, rel);
42
+ if (statSync(file).size > MAX_FILE_BYTES)
43
+ continue;
44
+ const lines = readFileSync(file, 'utf8').split('\n');
45
+ lines.forEach((line, index) => {
46
+ // Só a citação em comentário: a mesma string num literal de código é dado, não rastreabilidade.
47
+ const inComment = COMMENT_LINE.test(line)
48
+ ? [...line.matchAll(LEGACY_CITATION)]
49
+ : [...line.matchAll(LEGACY_CITATION)].filter((match) => COMMENT_MARKER.test(line.slice(0, match.index)));
50
+ const count = inComment.length;
51
+ if (!count)
52
+ return;
53
+ total += count;
54
+ places.push(`${rel}:${index + 1}`);
55
+ });
56
+ }
57
+ if (!total)
58
+ return [];
59
+ const files = new Set(places.map((place) => place.slice(0, place.lastIndexOf(':')))).size;
60
+ const examples = places.slice(0, EXAMPLES_SHOWN).join(', ');
61
+ const more = places.length > EXAMPLES_SHOWN ? ` e mais ${places.length - EXAMPLES_SHOWN}` : '';
62
+ return [
63
+ {
64
+ id: 'citacao-docs-em-codigo',
65
+ severidade: 'aviso',
66
+ mensagem: `${total} citação(ões) em ${files} arquivo(s) de código apontam para docs/specs, docs/plans, docs/bugs ou ` +
67
+ `docs/investigacoes, pastas que o migrar substituiu por docs/trabalhos/: ${examples}${more}.`,
68
+ correcao: 'Remova a citação do comentário (o vínculo fica no commit ou em docs/trabalhos/), como pede a rule ' +
69
+ '.claude/rules/comentarios.md. Arquivo tolerado pela catraca pode ser limpo aos poucos.',
70
+ },
71
+ ];
72
+ }
73
+ /** Planos migrados antes da 2.3.0 ficaram `em-execucao` e aparecem como ativos em todo `conflitos`. */
74
+ export function checkMigratedPlansRunning(root) {
75
+ const dir = join(root, WORKS_DIR);
76
+ if (!existsSync(dir))
77
+ return [];
78
+ const stuck = readdirSync(dir).filter((id) => {
79
+ const rel = `docs/trabalhos/${id}/PLAN.md`;
80
+ if (!existsSync(join(root, rel)))
81
+ return false;
82
+ const { data } = readArtifact(rel, readFileSync(join(root, rel), 'utf8'));
83
+ const arquivos = Array.isArray(data?.arquivos) ? data.arquivos : [];
84
+ return data?.migrado === true && data.status === 'em-execucao' && !arquivos.length;
85
+ });
86
+ if (!stuck.length)
87
+ return [];
88
+ return [
89
+ {
90
+ id: 'plano-migrado-em-execucao',
91
+ severidade: 'aviso',
92
+ mensagem: `${stuck.length} plano(s) migrado(s) do layout antigo estão como em-execucao sem declarar arquivos: ${stuck.slice(0, EXAMPLES_SHOWN).join(', ')}${stuck.length > EXAMPLES_SHOWN ? ' …' : ''}.`,
93
+ correcao: 'Rode `npx @fontdata/ai-harness migrar --reclassificar --yes`: plano sem caixa aberta vira concluido, e o ' +
94
+ 'restante vira legado, que não conta como trabalho ativo.',
95
+ },
96
+ ];
97
+ }
@@ -179,6 +179,10 @@ function rootCauseOf(body) {
179
179
  return 'hipotese';
180
180
  return UNCONFIRMED.test(body.slice(label.index, label.index + 600)) ? 'hipotese' : 'comprovada';
181
181
  }
182
+ /** Plano antigo com caixa aberta não está necessariamente em execução: `legado` não conta como trabalho ativo. */
183
+ function legacyPlanStatus(body) {
184
+ return OPEN_BOX.test(body) ? 'legado' : 'concluido';
185
+ }
182
186
  const TASK_HEADING = /^(#{2,6})\s*(?:(?:task|tarefa)\s+|T-?)(\d+)\.(\d+)\b\s*[—–:.-]*\s*(.*)$/i;
183
187
  const TASK_ITEM = /^\s*[-*]\s+\[([ xX])\]\s+\*{0,2}(?:(?:task|tarefa)\s+|T-?)(\d+)\.(\d+)\b\s*[—–:.-]*\s*(.*)$/i;
184
188
  const ANY_HEADING = /^(#{1,6})\s/;
@@ -256,7 +260,7 @@ function contractFrontmatter(content, move, today) {
256
260
  const existing = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : {};
257
261
  const body = match ? content.slice(match[0].length) : content;
258
262
  const heading = /^#\s+(.+)$/m.exec(body)?.[1]?.trim();
259
- const status = move.tipo === 'spec' ? 'aprovado' : move.tipo === 'plano' ? (OPEN_BOX.test(body) ? 'em-execucao' : 'concluido') : 'final';
263
+ const status = move.tipo === 'spec' ? 'aprovado' : move.tipo === 'plano' ? legacyPlanStatus(body) : 'final';
260
264
  const data = {
261
265
  schema_versao: SCHEMA_VERSION,
262
266
  tipo: move.tipo,
@@ -366,18 +370,83 @@ export function applyMigration(root, plan) {
366
370
  for (const dir of SOURCE_DIRS)
367
371
  removeLegacyDir(root, `docs/${dir}`, git);
368
372
  }
369
- const MIGRATION_FLAGS = ['--yes', '-y', '--sim'];
373
+ const FRONTMATTER = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
374
+ /**
375
+ * Planos migrados antes da 2.3.0 ficaram `em-execucao` quando tinham caixa aberta. Os que não declaram `arquivos`
376
+ * ganham o status que a migração daria hoje; quem preencheu `arquivos` está trabalhando neles e fica como está.
377
+ */
378
+ export function planReclassification(root) {
379
+ const dir = join(root, 'docs', 'trabalhos');
380
+ if (!existsSync(dir))
381
+ return [];
382
+ const found = [];
383
+ for (const id of readdirSync(dir).sort()) {
384
+ const file = join(dir, id, 'PLAN.md');
385
+ if (!existsSync(file))
386
+ continue;
387
+ const content = readFileSync(file, 'utf8');
388
+ const match = FRONTMATTER.exec(content);
389
+ if (!match)
390
+ continue;
391
+ let data;
392
+ try {
393
+ data = parse(match[1] ?? '');
394
+ }
395
+ catch {
396
+ continue;
397
+ }
398
+ if (typeof data !== 'object' || data === null)
399
+ continue;
400
+ const { migrado, status, arquivos } = data;
401
+ if (migrado !== true || status !== 'em-execucao' || (Array.isArray(arquivos) && arquivos.length))
402
+ continue;
403
+ found.push({ id, de: status, para: legacyPlanStatus(content.slice(match[0].length)) });
404
+ }
405
+ return found;
406
+ }
407
+ /** Troca só a linha `status:` do frontmatter, para não reformatar o resto do arquivo. */
408
+ export function applyReclassification(root, changes) {
409
+ for (const change of changes) {
410
+ const file = join(root, 'docs', 'trabalhos', change.id, 'PLAN.md');
411
+ const content = readFileSync(file, 'utf8');
412
+ const match = FRONTMATTER.exec(content);
413
+ if (!match)
414
+ continue;
415
+ const frontmatter = match[0].replace(/^status:[ \t]*['"]?em-execucao['"]?[ \t]*$/m, `status: ${change.para}`);
416
+ writeFileSync(file, frontmatter + content.slice(match[0].length));
417
+ }
418
+ }
419
+ function runReclassification(root, yes) {
420
+ const changes = planReclassification(root);
421
+ if (!changes.length) {
422
+ console.log('Nenhum plano migrado sem `arquivos` está como em-execucao: nada a reclassificar.');
423
+ return 0;
424
+ }
425
+ console.log([`Planos migrados a reclassificar (${changes.length}):`, ...changes.map((change) => ` ${change.id}: ${change.de} → ${change.para}`)].join('\n'));
426
+ if (!yes) {
427
+ console.log('\nSimulação — nada foi alterado. Rode de novo com --yes para reclassificar.');
428
+ return 0;
429
+ }
430
+ applyReclassification(root, changes);
431
+ console.log(`\nReclassificados: ${changes.length} plano(s).`);
432
+ return 0;
433
+ }
434
+ const MIGRATION_FLAGS = ['--yes', '-y', '--sim', '--reclassificar'];
370
435
  const REFERENCES_SHOWN = 40;
371
- /** `ai-harness migrar [caminho] [--yes]`; sem `--yes` só simula. */
436
+ /** `ai-harness migrar [caminho] [--reclassificar] [--yes]`; sem `--yes` só simula. */
372
437
  export function runMigrationCommand(argv) {
373
438
  let path;
374
439
  let yes = false;
440
+ let reclassify = false;
375
441
  for (const token of argv) {
376
442
  if (!token.startsWith('-')) {
377
443
  if (path !== undefined)
378
444
  throw new Error(`Argumento inesperado: ${token}`);
379
445
  path = token;
380
446
  }
447
+ else if (token === '--reclassificar') {
448
+ reclassify = true;
449
+ }
381
450
  else if (MIGRATION_FLAGS.includes(token)) {
382
451
  yes = true;
383
452
  }
@@ -389,6 +458,8 @@ export function runMigrationCommand(argv) {
389
458
  const root = resolve(process.cwd(), path ?? '.');
390
459
  if (!existsSync(root))
391
460
  throw new Error(`Pasta não encontrada: ${root}`);
461
+ if (reclassify)
462
+ return runReclassification(root, yes);
392
463
  const plan = planMigration(root);
393
464
  if (!plan.moves.length && !plan.conflicts.length) {
394
465
  const ignored = plan.ignored.length ? `\nIgnorados: ${plan.ignored.join(', ')}` : '';
@@ -20,7 +20,7 @@ function readPlan(root, workId) {
20
20
  return null;
21
21
  const listed = Array.isArray(data.arquivos) ? data.arquivos : [];
22
22
  const arquivos = listed.filter((entry) => typeof entry === 'string').map(normalize).filter(Boolean);
23
- return { status: String(data.status ?? ''), arquivos };
23
+ return { status: String(data.status ?? ''), arquivos, migrado: data.migrado === true };
24
24
  }
25
25
  /**
26
26
  * Trabalhos em execução (PLAN.md `em-execucao` ou listados no estado.json) que declaram arquivos em comum
@@ -40,7 +40,9 @@ export async function findConflicts(root, workId) {
40
40
  if (!plan || FINISHED.has(plan.status) || (plan.status !== 'em-execucao' && !listed.has(id)))
41
41
  continue;
42
42
  if (!plan.arquivos.length) {
43
- report.semArquivos.push(id);
43
+ // Plano migrado do layout antigo não diz o que tocou; só avisa quando alguém o está executando de fato.
44
+ if (!plan.migrado || listed.has(id))
45
+ report.semArquivos.push(id);
44
46
  continue;
45
47
  }
46
48
  const shared = own.arquivos.filter((a) => plan.arquivos.some((b) => overlaps(a, b)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fontdata/ai-harness",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Harness de engenharia com IA da Fontdata para Claude Code: roda os slash commands direto na pasta do projeto via npx, sem instalar nada — ou instala-os em .claude/commands.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
3
- import { extname, relative, join, dirname } from 'node:path';
2
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, openSync, closeSync, renameSync, rmSync, statSync } from 'node:fs';
3
+ import { extname, relative, join, dirname, sep } from 'node:path';
4
4
 
5
5
  const MAX_BLOCO = Number(process.env.COMENTARIOS_MAX_BLOCO ?? 3);
6
6
  const MAX_DOCSTRING = Number(process.env.COMENTARIOS_MAX_DOCSTRING ?? 5);
@@ -21,14 +21,18 @@ const IGNORAR = [
21
21
  ];
22
22
 
23
23
  const CABECALHO_PERMITIDO =
24
- /eslint|ts-nocheck|ts-ignore|prettier|@license|copyright|spdx|jshint|globals|^#!|use (strict|client|server)/i;
24
+ /eslint|ts-nocheck|ts-check|ts-ignore|prettier|@license|copyright|spdx|jshint|globals|^#!|use (strict|client|server)/i;
25
25
 
26
26
  const BANNER = /^[-=─-╿_*#~+]{3,}|[-=─-╿_*#~+]{3,}$/;
27
27
 
28
+ // Só a palavra ignora caixa: com a flag `i`, o romano casaria o "i" de "specialty".
29
+ const semCaixa = (texto) => texto.replace(/\p{L}/gu, (c) => `[${c.toLowerCase()}${c.toUpperCase()}]`);
30
+ const PROCESSO = new RegExp(`\\b(${semCaixa('task|tarefa|fase|etapa|plano|spec|sprint|epico|épico')})\\s*(\\d|[IVX]+\\b)`, 'u');
31
+
28
32
  const RASTREIO = [
29
- [/\b(task|tarefa|fase|etapa|plano|spec|sprint|epico|épico)\s*[\dIVX]/i, 'referência a task/fase/plano/spec'],
33
+ [PROCESSO, 'referência a task/fase/plano/spec'],
30
34
  [/\bT\d+(\.\d+)+\b/, 'referência a task (T0.0)'],
31
- [/docs\/(plans|specs|bugs|investigacoes)\//i, 'caminho de docs/ dentro do código'],
35
+ [/docs\/(plans|specs|bugs|investigacoes|trabalhos)\//i, 'caminho de docs/ dentro do código'],
32
36
  [/\bachado d[oe]\b/i, 'achado de review no código'],
33
37
  [/@[\w-]*(reviewer|auditor|orquestrador|agent)\b/i, 'menção a agent no código'],
34
38
  [/\b(commit|sha|hash)\s+[0-9a-f]{7,40}\b/i, 'hash de commit'],
@@ -121,12 +125,47 @@ function analisar(caminho, fonte) {
121
125
  return { achados, comentadas, uteis, densidade: uteis ? comentadas / uteis : 0 };
122
126
  }
123
127
 
128
+ const raiz = process.env.CLAUDE_PROJECT_DIR || process.cwd();
129
+ const BASELINE = join(raiz, '.claude', 'hooks', 'comentarios', 'baseline.json');
130
+
131
+ // Pasta oculta na raiz (.claude, .ai-harness, .vscode...) é de ferramenta, não do projeto.
132
+ function emPastaOculta(caminho) {
133
+ const rel = relative(raiz, caminho);
134
+ if (!rel || rel.startsWith('..')) return false;
135
+ return rel.split(sep).slice(0, -1).some((parte) => parte.startsWith('.'));
136
+ }
137
+
124
138
  function elegivel(caminho) {
125
- return EXTENSOES.has(extname(caminho)) && !IGNORAR.some((re) => re.test(caminho));
139
+ return EXTENSOES.has(extname(caminho)) && !IGNORAR.some((re) => re.test(caminho)) && !emPastaOculta(caminho);
126
140
  }
127
141
 
128
- const raiz = process.env.CLAUDE_PROJECT_DIR || process.cwd();
129
- const BASELINE = join(raiz, '.claude', 'hooks', 'comentarios', 'baseline.json');
142
+ function gravarAtomico(arquivo, conteudo) {
143
+ const temp = `${arquivo}.${process.pid}.tmp`;
144
+ writeFileSync(temp, conteudo);
145
+ renameSync(temp, arquivo);
146
+ }
147
+
148
+ const esperar = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
149
+
150
+ // Vários agents editam em paralelo: a trava serializa o reler-e-gravar, e cada um mexe só na própria chave.
151
+ function atualizarBaseline(chave, valor) {
152
+ const trava = `${BASELINE}.lock`;
153
+ let travado = false;
154
+ for (let tentativa = 0; tentativa < 300 && !travado; tentativa++) {
155
+ try { closeSync(openSync(trava, 'wx')); travado = true; } catch {
156
+ try { if (Date.now() - statSync(trava).mtimeMs > 10_000) rmSync(trava, { force: true }); } catch {}
157
+ if (!travado) esperar(10);
158
+ }
159
+ }
160
+ try {
161
+ const atual = JSON.parse(readFileSync(BASELINE, 'utf8'));
162
+ if (valor === 0) delete atual[chave];
163
+ else atual[chave] = Math.min(valor, atual[chave] ?? valor);
164
+ gravarAtomico(BASELINE, `${JSON.stringify(atual, null, 2)}\n`);
165
+ } catch {} finally {
166
+ if (travado) rmSync(trava, { force: true });
167
+ }
168
+ }
130
169
 
131
170
  function varrer(dir, saida = []) {
132
171
  let itens;
@@ -134,7 +173,7 @@ function varrer(dir, saida = []) {
134
173
  for (const item of itens) {
135
174
  const cheio = join(dir, item.name);
136
175
  if (item.isDirectory()) {
137
- if (item.name === 'node_modules' || item.name === '.git') continue;
176
+ if (item.name === 'node_modules' || item.name.startsWith('.')) continue;
138
177
  if (IGNORAR.some((re) => re.test(`${cheio}/`))) continue;
139
178
  varrer(cheio, saida);
140
179
  } else if (elegivel(cheio)) saida.push(cheio);
@@ -153,7 +192,7 @@ if (process.argv.includes('--gerar-baseline')) {
153
192
  if (achados.length > 0) { mapa[relative(raiz, f)] = achados.length; arquivos++; violacoes += achados.length; }
154
193
  }
155
194
  if (!existsSync(dirname(BASELINE))) mkdirSync(dirname(BASELINE), { recursive: true });
156
- writeFileSync(BASELINE, `${JSON.stringify(mapa, null, 2)}\n`);
195
+ gravarAtomico(BASELINE, `${JSON.stringify(mapa, null, 2)}\n`);
157
196
  console.log(`[comentarios] baseline gravado: ${arquivos} arquivos legados, ${violacoes} violações toleradas.`);
158
197
  console.log('[comentarios] arquivo novo responde ao padrão cheio; legado só não pode piorar.');
159
198
  process.exit(0);
@@ -179,9 +218,7 @@ const tolerado = baseline?.[chave] ?? 0;
179
218
 
180
219
  if (achados.length <= tolerado) {
181
220
  if (baseline && tolerado > 0 && achados.length < tolerado) {
182
- if (achados.length === 0) delete baseline[chave];
183
- else baseline[chave] = achados.length;
184
- try { writeFileSync(BASELINE, `${JSON.stringify(baseline, null, 2)}\n`); } catch {}
221
+ atualizarBaseline(chave, achados.length);
185
222
  console.error(`[comentarios] ${rel}: ${tolerado} → ${achados.length} violações. Catraca desceu — não pode mais subir.`);
186
223
  } else if (achados.length === 0 && densidade > DENSIDADE_ALVO) {
187
224
  console.error(`[comentarios] aviso: ${rel} tem ${(densidade * 100).toFixed(0)}% de linhas de comentário (${comentadas}/${uteis}), alvo <= ${DENSIDADE_ALVO * 100}%.`);
@@ -61,7 +61,7 @@ export function buildEvent(fields) {
61
61
 
62
62
  /**
63
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
64
+ * anterior) ou, sem eles, os PLAN.md com `status: em-execucao` (menos os migrados sem `arquivos`). Várias sessões podem executar
65
65
  * trabalhos diferentes na mesma pasta, então a lista pode ter mais de um id.
66
66
  * @param {string} root
67
67
  * @returns {string[]}
@@ -88,7 +88,10 @@ export function findActiveWorks(root) {
88
88
  if (!existsSync(plan)) return false;
89
89
  const frontmatter = /^\s*---\r?\n([\s\S]*?)\r?\n---/.exec(readFileSync(plan, 'utf8'))?.[1] ?? '';
90
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);
91
+ if (!/^status:\s*['"]?em-execucao['"]?\s*$/m.test(frontmatter)) return false;
92
+ // Plano migrado do layout antigo sem `arquivos` ficou em-execucao pela migração, não por alguém executá-lo.
93
+ const migradoSemArquivos = /^migrado:\s*true\s*$/m.test(frontmatter) && !/^arquivos:[ \t]*(\[[ \t]*[^\]\s]|\r?\n[ \t]*-\s)/m.test(frontmatter);
94
+ return !migradoSemArquivos;
92
95
  });
93
96
  }
94
97