@fontdata/ai-harness 3.2.2 → 3.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.
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +6 -1
- package/dist/banco-dados/lgpd/aplicar.js +7 -2
- package/dist/banco-dados/lgpd/detectar.js +4 -0
- package/dist/cli.js +8 -0
- package/dist/contratos/artefatos.js +5 -3
- package/dist/doctor/checks.js +98 -5
- package/dist/install/settings.js +1 -0
- package/dist/memoria/indice.js +64 -42
- package/dist/trabalhos/arquivar.js +196 -0
- package/dist/trabalhos/command.js +105 -0
- package/dist/trabalhos/indice-arquivo.js +68 -0
- package/dist/trabalhos/listar.js +37 -0
- package/dist/update/update.js +24 -4
- package/package.json +1 -1
- package/templates/agents/investigador.md +2 -2
- package/templates/hooks/comentarios/check.mjs +4 -0
- package/templates/rules/comentarios.md +6 -2
- package/templates/skills/evoluir-harness/references/auditoria.md +3 -2
- package/templates/skills/evoluir-harness/references/skill.md +2 -1
- package/templates/skills/memoria/SKILL.md +2 -1
- package/templates/skills/nova-tarefa/SKILL.md +4 -3
- package/templates/skills/nova-tarefa/references/isso-ja-existe.md +2 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fontdata",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"mcpServers": {
|
|
5
|
-
"banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@3.
|
|
5
|
+
"banco_dados": { "command": "npx", "args": ["-y", "@fontdata/ai-harness@3.3.0", "banco-dados-mcp"] }
|
|
6
6
|
},
|
|
7
7
|
"description": "Harness de engenharia com IA da Fontdata para Claude Code",
|
|
8
8
|
"author": { "name": "Fontdata" },
|
package/README.md
CHANGED
|
@@ -162,6 +162,8 @@ projeto, cadastro, limites do filtro, produção, mascaramento e dúvidas freque
|
|
|
162
162
|
| `memoria [caminho]` | Índice de quem mexeu em cada arquivo (`--arquivo <caminho>`, `--regressao <id>`) |
|
|
163
163
|
| `evento <evento>` | Grava no rastro do trabalho (usado por fluxos e agents) |
|
|
164
164
|
| `conflitos <id>` | Aponta trabalhos em execução que mexem nos mesmos arquivos; `--aguardar` espera liberar (usado pelo `/executar-plan`) |
|
|
165
|
+
| `trabalhos arquivar [caminho]` | Simula e, com `--yes`, move para `docs/trabalhos/_arquivo/` os trabalhos concluídos há mais de 60 dias (`--antes-de <dias>`); `--remover` apaga do disco os que o commit do índice recupera |
|
|
166
|
+
| `trabalhos listar [caminho]` | Uma linha por trabalho, arquivados inclusive (`--busca`, `--tipo`, `--status`, `--limite`, padrão 20) |
|
|
165
167
|
| `banco-dados <subcomando>` | Conexões de banco somente leitura: `adicionar`, `listar`, `testar`, `usar`, `remover` |
|
|
166
168
|
| `banco-dados-mcp` | Servidor MCP de leitura do banco, iniciado pelo Claude Code |
|
|
167
169
|
| `comentarios [caminho]` | Instala o hook do padrão de comentários com a catraca do legado (`--yes`) |
|
|
@@ -178,6 +180,8 @@ projeto, cadastro, limites do filtro, produção, mascaramento e dúvidas freque
|
|
|
178
180
|
| `docs/trabalhos/<id>/DECISOES.md` | Decisões tomadas no planejamento e na execução |
|
|
179
181
|
| `docs/trabalhos/<id>/BLOQUEIOS.md` | Dúvidas que pararam uma task |
|
|
180
182
|
| `docs/trabalhos/<id>/relatorios/TECNICO.md` e `CLIENTE.md` | Relatório para engenharia e para cliente e suporte |
|
|
183
|
+
| `docs/trabalhos/_arquivo/<ano>/<id>/` e `INDICE.jsonl` | Trabalhos arquivados pelo `trabalhos arquivar` e o índice que mantém a memória deles |
|
|
184
|
+
| `.ignore` (raiz) | Linha `docs/trabalhos/_arquivo/`, que tira o histórico do Grep e do Glob dos agentes; o git continua versionando |
|
|
181
185
|
| `docs/eventos/<id>.jsonl` | Rastro de eventos (não versionado por padrão) |
|
|
182
186
|
| `.ai-harness/lock.json` e `config.json` | Lock da instalação e configuração dos hooks (versionar) |
|
|
183
187
|
|
|
@@ -249,7 +253,8 @@ também acionado pelo modelo quando o pedido combina.
|
|
|
249
253
|
- `/harness` **(skill)** — conduz o setup em etapas (ideia, contexto, ignores, plano do start, scaffold, peças do harness, auto-tag e execução); `/harness <etapa>` roda uma etapa isolada
|
|
250
254
|
|
|
251
255
|
O padrão de comentários não é um comando: a rule, o hook e a catraca do legado vêm do `init` e do `update`. O
|
|
252
|
-
`update` também o instala em projetos cujo lock é anterior a ele
|
|
256
|
+
`update` também o instala em projetos cujo lock é anterior a ele; um hook próprio que já existe, de versão antiga ou
|
|
257
|
+
adaptado, é mantido e só entra no lock. Para recusar, use `--sem-comentarios` no `init` ou
|
|
253
258
|
no `update`: a escolha fica em `comentarios.instalar: false` no `.ai-harness/config.json`, e o `update` a respeita.
|
|
254
259
|
Para reativar, apague a chave e rode `update --yes` (ou `npx @fontdata/ai-harness comentarios --yes`). Sem o padrão
|
|
255
260
|
e sem a recusa, o `doctor` avisa com `comentarios-ausente`.
|
|
@@ -106,9 +106,14 @@ function itensDoSelect(sql, dialeto, colunas) {
|
|
|
106
106
|
contagem: ehContagem(item),
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
/** O SQL não diz de que tabela vem cada identificador, então só a entrada sem tabela de `liberar` vale aqui. */
|
|
110
|
+
function colunaPessoalCitada(ids, mascarar, liberar) {
|
|
110
111
|
const extras = new Set(mascarar.map((r) => r.coluna));
|
|
112
|
+
const livres = new Set(liberar.filter((r) => r.tabela === null).map((r) => r.coluna));
|
|
111
113
|
for (const id of ids) {
|
|
114
|
+
const nome = id.toLowerCase();
|
|
115
|
+
if (livres.has(nome) && !extras.has(nome))
|
|
116
|
+
continue;
|
|
112
117
|
const categoria = classificarColuna(id);
|
|
113
118
|
if (categoria)
|
|
114
119
|
return { categoria, identificador: id };
|
|
@@ -248,7 +253,7 @@ export function mascararResultado(resultado, opcoes) {
|
|
|
248
253
|
};
|
|
249
254
|
const citacao = (i) => {
|
|
250
255
|
if (!citacoes.has(i)) {
|
|
251
|
-
citacoes.set(i, colunaPessoalCitada(item(i)?.ids ?? identificadores(opcoes.sql, opcoes.dialeto), mascarar));
|
|
256
|
+
citacoes.set(i, colunaPessoalCitada(item(i)?.ids ?? identificadores(opcoes.sql, opcoes.dialeto), mascarar, liberar));
|
|
252
257
|
}
|
|
253
258
|
return citacoes.get(i);
|
|
254
259
|
};
|
|
@@ -39,6 +39,8 @@ const COMBINACOES = [
|
|
|
39
39
|
const PARTES_DE_TRIBUTO = new Set([
|
|
40
40
|
'valor', 'vl', 'vlr', 'percentual', 'perc', 'aliquota', 'aliq', 'bc', 'base', 'retencao', 'ret', 'cst', 'cofins',
|
|
41
41
|
]);
|
|
42
|
+
/** Com uma destas partes, `documento` é chave interna (`cod_loja_documento`, `id_documento`), não o número do documento. */
|
|
43
|
+
const PARTES_DE_CODIGO = new Set(['cod', 'codigo', 'id', 'seq', 'sequencial']);
|
|
42
44
|
const TERMOS_FORTES = [
|
|
43
45
|
['cpf', 'cpf'],
|
|
44
46
|
['cnpj', 'cnpj'],
|
|
@@ -73,6 +75,8 @@ export function classificarColuna(nome) {
|
|
|
73
75
|
const tem = new Set(partes);
|
|
74
76
|
if (partes.some((p) => PARTES_DE_TRIBUTO.has(p)))
|
|
75
77
|
tem.delete('pis');
|
|
78
|
+
if (partes.some((p) => PARTES_DE_CODIGO.has(p)))
|
|
79
|
+
tem.delete('documento');
|
|
76
80
|
for (const [categoria, listas] of COMBINACOES) {
|
|
77
81
|
if (listas.every((lista) => lista.some((p) => tem.has(p))))
|
|
78
82
|
return categoria;
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const RESERVED = new Set([
|
|
|
15
15
|
'migrar',
|
|
16
16
|
'evento',
|
|
17
17
|
'conflitos',
|
|
18
|
+
'trabalhos',
|
|
18
19
|
'comentarios',
|
|
19
20
|
'memoria',
|
|
20
21
|
'banco-dados',
|
|
@@ -60,6 +61,9 @@ Gerenciar:
|
|
|
60
61
|
memoria [caminho] Índice de quem mexeu em cada arquivo (--arquivo <caminho>, --regressao <id do bug>)
|
|
61
62
|
evento <evento> Grava no rastro do trabalho (--trabalho, --task, --nome, --resultado, --dados '{json}')
|
|
62
63
|
conflitos <id> Aponta trabalhos em execução que mexem nos mesmos arquivos (--aguardar espera liberar)
|
|
64
|
+
trabalhos listar [caminho] Uma linha por trabalho, arquivados inclusive (--busca, --tipo, --status, --limite)
|
|
65
|
+
trabalhos arquivar [caminho] Move os concluídos há mais de 60 dias para docs/trabalhos/_arquivo (--yes aplica)
|
|
66
|
+
--antes-de <dias> muda o corte; --remover apaga do disco os que o git recupera
|
|
63
67
|
banco-dados <subcomando> Conexões de banco somente leitura: adicionar, listar, testar, usar, remover
|
|
64
68
|
banco-dados-mcp Servidor MCP de leitura do banco, usado pelo Claude Code (stdio)
|
|
65
69
|
list Lista todos os comandos disponíveis no catálogo
|
|
@@ -156,6 +160,10 @@ async function main() {
|
|
|
156
160
|
const { runConflictsCommand } = await import('./trabalhos/conflitos.js');
|
|
157
161
|
process.exit(await runConflictsCommand(argv.slice(1)));
|
|
158
162
|
}
|
|
163
|
+
if (cmd === 'trabalhos') {
|
|
164
|
+
const { runWorksCommand } = await import('./trabalhos/command.js');
|
|
165
|
+
process.exit(await runWorksCommand(argv.slice(1)));
|
|
166
|
+
}
|
|
159
167
|
if (cmd === 'banco-dados') {
|
|
160
168
|
const { runBancoCommand } = await import('./banco-dados/command.js');
|
|
161
169
|
process.exit(await runBancoCommand(argv.slice(1)));
|
|
@@ -4,6 +4,8 @@ import { parse, YAMLParseError } from 'yaml';
|
|
|
4
4
|
/** Versão do contrato de artefatos (docs/contratos/ARTEFATOS.md). */
|
|
5
5
|
export const SCHEMA_VERSION = 1;
|
|
6
6
|
export const WORKS_DIR = join('docs', 'trabalhos');
|
|
7
|
+
/** Subpasta de docs/trabalhos/ com os trabalhos arquivados; não é pasta de trabalho. */
|
|
8
|
+
export const ARCHIVE_DIR_NAME = '_arquivo';
|
|
7
9
|
/** Id de trabalho: `AAAA-MM-DD-<slug>` com slug em minúsculas, sem acento. */
|
|
8
10
|
export const WORK_ID = /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
11
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -27,7 +29,7 @@ export const STATUS_BY_TYPE = {
|
|
|
27
29
|
};
|
|
28
30
|
export const WORK_TYPES = ['feature', 'bug', 'investigacao', 'auditoria'];
|
|
29
31
|
export const TASK_STATUS = ['pendente', 'em-andamento', 'concluida', 'bloqueada', 'pulada'];
|
|
30
|
-
const REQUIRED_BY_WORK_TYPE = {
|
|
32
|
+
export const REQUIRED_BY_WORK_TYPE = {
|
|
31
33
|
feature: ['SPEC.md', 'PLAN.md'],
|
|
32
34
|
bug: ['PLAN.md'],
|
|
33
35
|
investigacao: ['relatorios/TECNICO.md'],
|
|
@@ -222,7 +224,7 @@ function validateBlocks(file, data) {
|
|
|
222
224
|
});
|
|
223
225
|
}
|
|
224
226
|
/** Arquivos da pasta do trabalho, relativos a ela, com separador `/`. */
|
|
225
|
-
function listWorkFiles(dir) {
|
|
227
|
+
export function listWorkFiles(dir) {
|
|
226
228
|
return readdirSync(dir, { recursive: true, encoding: 'utf8' })
|
|
227
229
|
.filter((rel) => statSync(join(dir, rel)).isFile())
|
|
228
230
|
.map((rel) => rel.split(sep).join('/'))
|
|
@@ -284,7 +286,7 @@ export function validateAllWorks(root) {
|
|
|
284
286
|
if (!existsSync(dir))
|
|
285
287
|
return [];
|
|
286
288
|
return readdirSync(dir)
|
|
287
|
-
.filter((name) => statSync(join(dir, name)).isDirectory())
|
|
289
|
+
.filter((name) => name !== ARCHIVE_DIR_NAME && statSync(join(dir, name)).isDirectory())
|
|
288
290
|
.sort()
|
|
289
291
|
.flatMap((name) => validateWork(root, name));
|
|
290
292
|
}
|
package/dist/doctor/checks.js
CHANGED
|
@@ -4,11 +4,15 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { lerEnvSettingsLocal, lerVariavel, SETTINGS_LOCAL_PATH } from '../banco-dados/ambiente.js';
|
|
5
5
|
import { caminhoCadastro, lerCadastro } from '../banco-dados/cadastro.js';
|
|
6
6
|
import { allCommands, loadCatalog } from '../catalog.js';
|
|
7
|
+
import { COMMENTS_HOOK_PATH } from '../comentarios/instalar.js';
|
|
7
8
|
import { CONFIG_PATH, readConfig } from '../config/config.js';
|
|
8
|
-
import { readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
|
|
9
|
+
import { ARCHIVE_DIR_NAME, ARTIFACT_TYPES, listWorkFiles, readArtifact, validateAllWorks, WORKS_DIR } from '../contratos/artefatos.js';
|
|
9
10
|
import { validateEventLog } from '../contratos/eventos.js';
|
|
10
11
|
import { hashContent, LOCK_PATH, LOCK_VERSION } from '../install/lock.js';
|
|
11
12
|
import { SETTINGS_PATH } from '../install/settings.js';
|
|
13
|
+
import { planArchive } from '../trabalhos/arquivar.js';
|
|
14
|
+
import { ARCHIVE_DEFAULT_DAYS } from '../trabalhos/command.js';
|
|
15
|
+
import { ARCHIVE_INDEX, readArchiveIndex } from '../trabalhos/indice-arquivo.js';
|
|
12
16
|
import { adoptLegacyInstall } from '../update/legado.js';
|
|
13
17
|
import { checkLegacyDocsCitations, checkMigratedPlansRunning } from './migracao.js';
|
|
14
18
|
const MIN_NODE_MAJOR = 22;
|
|
@@ -18,6 +22,8 @@ const REINSTALL = 'Restaure com `npx @fontdata/ai-harness update --yes`.';
|
|
|
18
22
|
const CONTRACT_URL = 'https://github.com/fontdata-tecnologia/ai-harness/blob/master/docs/contratos/ARTEFATOS.md';
|
|
19
23
|
const EVENTS_CONTRACT_URL = 'https://github.com/fontdata-tecnologia/ai-harness/blob/master/docs/contratos/EVENTOS.md';
|
|
20
24
|
const EVENTS_DIR = join('docs', 'eventos');
|
|
25
|
+
const STALE_DRAFT_PLAN_DAYS = 30;
|
|
26
|
+
const ARCHIVE_SUGGESTION_MIN = 30;
|
|
21
27
|
/** Regras cuja violação impede ler o artefato ou seguir o plano; as demais são avisos. */
|
|
22
28
|
const CONTRACT_ERRORS = new Set(['yaml-invalido', 'schema-futuro', 'ciclo-dependencia']);
|
|
23
29
|
function checkNode(version) {
|
|
@@ -264,6 +270,84 @@ function checkStaleBlocks(root, now, maxDays) {
|
|
|
264
270
|
}
|
|
265
271
|
return findings;
|
|
266
272
|
}
|
|
273
|
+
function activeWorkFolders(root) {
|
|
274
|
+
const dir = join(root, WORKS_DIR);
|
|
275
|
+
if (!existsSync(dir))
|
|
276
|
+
return [];
|
|
277
|
+
return readdirSync(dir)
|
|
278
|
+
.filter((name) => name !== ARCHIVE_DIR_NAME && statSync(join(dir, name)).isDirectory())
|
|
279
|
+
.sort();
|
|
280
|
+
}
|
|
281
|
+
function checkArchiveIndex(root) {
|
|
282
|
+
const file = ARCHIVE_INDEX.split('\\').join('/');
|
|
283
|
+
return readArchiveIndex(root).problemas.map(({ linha, mensagem }) => ({
|
|
284
|
+
id: 'arquivo-indice-invalido',
|
|
285
|
+
severidade: 'aviso',
|
|
286
|
+
mensagem: `${file} (linha ${linha}): ${mensagem}. A linha fica fora da memória e da listagem de trabalhos.`,
|
|
287
|
+
correcao: `Corrija ou apague a linha ${linha} de ${file}; cada linha é um objeto JSON com \`id\` texto.`,
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
/** Sem os trabalhos ativos do estado.json, que exigem leitura assíncrona, a contagem é uma estimativa por cima. */
|
|
291
|
+
function checkArchiveSuggestion(root, now) {
|
|
292
|
+
const count = planArchive(root, { now, dias: ARCHIVE_DEFAULT_DAYS, ativos: [] }).elegiveis.length;
|
|
293
|
+
if (count < ARCHIVE_SUGGESTION_MIN)
|
|
294
|
+
return [];
|
|
295
|
+
return [
|
|
296
|
+
{
|
|
297
|
+
id: 'arquivamento-sugerido',
|
|
298
|
+
severidade: 'aviso',
|
|
299
|
+
mensagem: `${count} trabalhos concluídos há mais de ${ARCHIVE_DEFAULT_DAYS} dias podem ir para docs/trabalhos/_arquivo/ (estimativa).`,
|
|
300
|
+
correcao: 'Veja a simulação com `npx @fontdata/ai-harness trabalhos arquivar` e aplique com --yes.',
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
function checkStaleDraftPlans(root, now) {
|
|
305
|
+
const findings = [];
|
|
306
|
+
for (const work of activeWorkFolders(root)) {
|
|
307
|
+
const rel = `docs/trabalhos/${work}/PLAN.md`;
|
|
308
|
+
if (!existsSync(join(root, rel)))
|
|
309
|
+
continue;
|
|
310
|
+
const { data } = readArtifact(rel, readFileSync(join(root, rel), 'utf8'));
|
|
311
|
+
if (data?.status !== 'rascunho' || typeof data.atualizado_em !== 'string')
|
|
312
|
+
continue;
|
|
313
|
+
const updated = Date.parse(`${data.atualizado_em}T00:00:00Z`);
|
|
314
|
+
if (Number.isNaN(updated))
|
|
315
|
+
continue;
|
|
316
|
+
const age = Math.floor((now.getTime() - updated) / 86_400_000);
|
|
317
|
+
if (age <= STALE_DRAFT_PLAN_DAYS)
|
|
318
|
+
continue;
|
|
319
|
+
findings.push({
|
|
320
|
+
id: 'plano-rascunho-antigo',
|
|
321
|
+
severidade: 'aviso',
|
|
322
|
+
mensagem: `O ${rel} está em rascunho e não é atualizado há ${age} dias.`,
|
|
323
|
+
correcao: 'Retome o planejamento com /nova-tarefa ou marque o plano como cancelado.',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return findings;
|
|
327
|
+
}
|
|
328
|
+
/** Nome que só difere de um artefato pela caixa já é apontado por `contrato-nome-fora-do-padrao`. */
|
|
329
|
+
function isOutsideContract(rel) {
|
|
330
|
+
if (rel in ARTIFACT_TYPES || rel.startsWith('anexos/'))
|
|
331
|
+
return false;
|
|
332
|
+
if (rel.split('/').at(-1)?.startsWith('.'))
|
|
333
|
+
return false;
|
|
334
|
+
return !Object.keys(ARTIFACT_TYPES).some((name) => name.toLowerCase() === rel.toLowerCase());
|
|
335
|
+
}
|
|
336
|
+
function checkFilesOutsideContract(root) {
|
|
337
|
+
return activeWorkFolders(root).flatMap((work) => {
|
|
338
|
+
const extras = listWorkFiles(join(root, WORKS_DIR, work)).filter(isOutsideContract);
|
|
339
|
+
if (!extras.length)
|
|
340
|
+
return [];
|
|
341
|
+
return [
|
|
342
|
+
{
|
|
343
|
+
id: 'arquivo-fora-do-contrato',
|
|
344
|
+
severidade: 'aviso',
|
|
345
|
+
mensagem: `docs/trabalhos/${work}/ tem arquivo(s) fora do contrato de artefatos: ${extras.join(', ')}.`,
|
|
346
|
+
correcao: `Se é relatório, mova o conteúdo para relatorios/TECNICO.md ou o arquivo para anexos/. Contrato: ${CONTRACT_URL}`,
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
});
|
|
350
|
+
}
|
|
267
351
|
function eventLogFiles(root) {
|
|
268
352
|
const dir = join(root, EVENTS_DIR);
|
|
269
353
|
if (!existsSync(dir))
|
|
@@ -390,9 +474,14 @@ function checkOldCommentsBlock(root) {
|
|
|
390
474
|
},
|
|
391
475
|
];
|
|
392
476
|
}
|
|
393
|
-
/**
|
|
394
|
-
|
|
395
|
-
|
|
477
|
+
/**
|
|
478
|
+
* Lock anterior ao padrão de comentários e sem a recusa no config: o update o instala. Hook no disco fora do lock é
|
|
479
|
+
* instalação antiga ou adaptada, que o update só registra no lock.
|
|
480
|
+
*/
|
|
481
|
+
function checkComments(root, lock, config) {
|
|
482
|
+
if (!config.comentarios.instalar || existsSync(join(root, COMMENTS_HOOK_PATH)))
|
|
483
|
+
return [];
|
|
484
|
+
if (lock.itens.some((item) => item.tipo === 'hook' && item.id === 'comentarios'))
|
|
396
485
|
return [];
|
|
397
486
|
return [
|
|
398
487
|
{
|
|
@@ -520,7 +609,7 @@ export function runDoctor(root, options = {}) {
|
|
|
520
609
|
return [
|
|
521
610
|
...checkNode(options.nodeVersion ?? process.versions.node),
|
|
522
611
|
...lockFindings,
|
|
523
|
-
...(lock ? [...checkFiles(root, lock), ...checkRequiredSkills(root, lock), ...checkComments(lock, config)] : []),
|
|
612
|
+
...(lock ? [...checkFiles(root, lock), ...checkRequiredSkills(root, lock), ...checkComments(root, lock, config)] : []),
|
|
524
613
|
...checkShadowedCommands(root),
|
|
525
614
|
...checkOldCommentsBlock(root),
|
|
526
615
|
...checkAgents(root),
|
|
@@ -529,6 +618,10 @@ export function runDoctor(root, options = {}) {
|
|
|
529
618
|
...checkConfig(configProblems),
|
|
530
619
|
...checkBanco(root, config),
|
|
531
620
|
...checkWorks(root),
|
|
621
|
+
...checkArchiveIndex(root),
|
|
622
|
+
...checkStaleDraftPlans(root, options.now ?? new Date()),
|
|
623
|
+
...checkArchiveSuggestion(root, options.now ?? new Date()),
|
|
624
|
+
...checkFilesOutsideContract(root),
|
|
532
625
|
...checkStaleBlocks(root, options.now ?? new Date(), options.staleBlockDays ?? config.bloqueios.dias_para_aviso),
|
|
533
626
|
...checkEventLogs(root),
|
|
534
627
|
...checkCompletedTasks(root),
|
package/dist/install/settings.js
CHANGED
|
@@ -19,6 +19,7 @@ export const TOOL_PERMISSIONS = ['Read', 'Grep', 'Glob', 'Skill'];
|
|
|
19
19
|
export const READ_PERMISSIONS = [
|
|
20
20
|
'Bash(npx @fontdata/ai-harness doctor:*)',
|
|
21
21
|
'Bash(npx @fontdata/ai-harness memoria:*)',
|
|
22
|
+
'Bash(npx @fontdata/ai-harness trabalhos listar:*)',
|
|
22
23
|
'Bash(git status:*)',
|
|
23
24
|
'Bash(git diff:*)',
|
|
24
25
|
'Bash(git log:*)',
|
package/dist/memoria/indice.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { ARTIFACT_TYPES, readArtifact, WORKS_DIR } from '../contratos/artefatos.js';
|
|
5
5
|
import { suggestFlag } from '../install/flags.js';
|
|
6
6
|
import { acharRaiz } from '../raiz.js';
|
|
7
|
+
import { ARCHIVE_DIR_NAME, ARCHIVE_INDEX, readArchiveIndex } from '../trabalhos/indice-arquivo.js';
|
|
7
8
|
export const INDEX_PATH = join('.ai-harness', 'memoria', 'indice.json');
|
|
8
9
|
export const INDEX_VERSION = 1;
|
|
9
10
|
const BACKSLASH = String.fromCharCode(92);
|
|
@@ -34,9 +35,56 @@ function conclusionDate(root, workId) {
|
|
|
34
35
|
}
|
|
35
36
|
return date;
|
|
36
37
|
}
|
|
38
|
+
/** Resumo de um trabalho ativo a partir dos artefatos; `resumo` é null quando nenhum artefato é legível. */
|
|
39
|
+
export function summarizeWork(root, workId) {
|
|
40
|
+
const worksDir = join(root, WORKS_DIR);
|
|
41
|
+
const problemas = [];
|
|
42
|
+
const summary = { trabalho_tipo: null, titulo: null, status: null, data: null, causa_raiz: null, criado_em: null, arquivos: [], arquivado: false };
|
|
43
|
+
const files = new Set();
|
|
44
|
+
let updatedAt = null;
|
|
45
|
+
let fallbackStatus = null;
|
|
46
|
+
let readable = false;
|
|
47
|
+
for (const rel of Object.keys(ARTIFACT_TYPES)) {
|
|
48
|
+
const path = join(worksDir, workId, rel);
|
|
49
|
+
if (!existsSync(path))
|
|
50
|
+
continue;
|
|
51
|
+
const { data, problems } = readArtifact(`docs/trabalhos/${workId}/${rel}`, readFileSync(path, 'utf8'));
|
|
52
|
+
if (!data) {
|
|
53
|
+
problemas.push(...problems.map(({ arquivo, mensagem }) => ({ arquivo, mensagem })));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
readable = true;
|
|
57
|
+
summary.titulo ??= text(data.titulo);
|
|
58
|
+
summary.trabalho_tipo ??= text(data.trabalho_tipo);
|
|
59
|
+
if (rel === 'PLAN.md') {
|
|
60
|
+
summary.status = text(data.status);
|
|
61
|
+
summary.causa_raiz = text(data.causa_raiz);
|
|
62
|
+
}
|
|
63
|
+
fallbackStatus ??= text(data.status);
|
|
64
|
+
summary.criado_em = earliest(summary.criado_em, text(data.criado_em));
|
|
65
|
+
updatedAt = latest(updatedAt, text(data.atualizado_em));
|
|
66
|
+
if (Array.isArray(data.arquivos)) {
|
|
67
|
+
for (const file of data.arquivos)
|
|
68
|
+
if (typeof file === 'string')
|
|
69
|
+
files.add(normalizePath(file));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!readable)
|
|
73
|
+
return { resumo: null, problemas };
|
|
74
|
+
summary.status ??= fallbackStatus;
|
|
75
|
+
summary.data = conclusionDate(root, workId) ?? updatedAt ?? summary.criado_em;
|
|
76
|
+
summary.arquivos = [...files].sort();
|
|
77
|
+
return { resumo: summary, problemas };
|
|
78
|
+
}
|
|
79
|
+
function addWork(index, id, summary) {
|
|
80
|
+
index.trabalhos[id] = summary;
|
|
81
|
+
const { trabalho_tipo, titulo, status, data, causa_raiz } = summary;
|
|
82
|
+
for (const file of summary.arquivos)
|
|
83
|
+
(index.arquivos[file] ??= []).push({ trabalho: id, trabalho_tipo, titulo, status, data, causa_raiz });
|
|
84
|
+
}
|
|
37
85
|
/**
|
|
38
|
-
* Índice "quem mexeu neste arquivo e por quê", gerado do frontmatter dos trabalhos e do
|
|
39
|
-
* Não é busca semântica: toda resposta aponta um trabalho que pode ser conferido.
|
|
86
|
+
* Índice "quem mexeu neste arquivo e por quê", gerado do frontmatter dos trabalhos, do rastro e do
|
|
87
|
+
* índice dos arquivados. Não é busca semântica: toda resposta aponta um trabalho que pode ser conferido.
|
|
40
88
|
*/
|
|
41
89
|
export function buildIndex(root, now = new Date()) {
|
|
42
90
|
const index = { indice_versao: INDEX_VERSION, gerado_em: now.toISOString(), trabalhos: {}, arquivos: {}, problemas: [] };
|
|
@@ -44,49 +92,23 @@ export function buildIndex(root, now = new Date()) {
|
|
|
44
92
|
if (!existsSync(worksDir))
|
|
45
93
|
return index;
|
|
46
94
|
const workIds = readdirSync(worksDir)
|
|
47
|
-
.filter((name) => statSync(join(worksDir, name)).isDirectory())
|
|
95
|
+
.filter((name) => name !== ARCHIVE_DIR_NAME && statSync(join(worksDir, name)).isDirectory())
|
|
48
96
|
.sort();
|
|
49
97
|
for (const id of workIds) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!data) {
|
|
61
|
-
index.problemas.push(...problems.map(({ arquivo, mensagem }) => ({ arquivo, mensagem })));
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
readable = true;
|
|
65
|
-
summary.titulo ??= text(data.titulo);
|
|
66
|
-
summary.trabalho_tipo ??= text(data.trabalho_tipo);
|
|
67
|
-
if (rel === 'PLAN.md') {
|
|
68
|
-
summary.status = text(data.status);
|
|
69
|
-
summary.causa_raiz = text(data.causa_raiz);
|
|
70
|
-
}
|
|
71
|
-
fallbackStatus ??= text(data.status);
|
|
72
|
-
summary.criado_em = earliest(summary.criado_em, text(data.criado_em));
|
|
73
|
-
updatedAt = latest(updatedAt, text(data.atualizado_em));
|
|
74
|
-
if (Array.isArray(data.arquivos)) {
|
|
75
|
-
for (const file of data.arquivos)
|
|
76
|
-
if (typeof file === 'string')
|
|
77
|
-
files.add(normalizePath(file));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!readable)
|
|
98
|
+
const { resumo, problemas } = summarizeWork(root, id);
|
|
99
|
+
index.problemas.push(...problemas);
|
|
100
|
+
if (resumo)
|
|
101
|
+
addWork(index, id, resumo);
|
|
102
|
+
}
|
|
103
|
+
const archive = readArchiveIndex(root);
|
|
104
|
+
const archiveFile = ARCHIVE_INDEX.split(BACKSLASH).join('/');
|
|
105
|
+
index.problemas.push(...archive.problemas.map(({ linha, mensagem }) => ({ arquivo: archiveFile, mensagem: `linha ${linha}: ${mensagem}` })));
|
|
106
|
+
for (const entry of archive.trabalhos) {
|
|
107
|
+
if (workIds.includes(entry.id))
|
|
81
108
|
continue;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
index.trabalhos[id] = summary;
|
|
86
|
-
for (const file of summary.arquivos) {
|
|
87
|
-
const { trabalho_tipo, titulo, status, data, causa_raiz } = summary;
|
|
88
|
-
(index.arquivos[file] ??= []).push({ trabalho: id, trabalho_tipo, titulo, status, data, causa_raiz });
|
|
89
|
-
}
|
|
109
|
+
const { trabalho_tipo, titulo, status, criado_em, data, causa_raiz } = entry;
|
|
110
|
+
const arquivos = [...new Set(entry.arquivos.map(normalizePath))];
|
|
111
|
+
addWork(index, entry.id, { trabalho_tipo, titulo, status, data, causa_raiz, criado_em, arquivos, arquivado: true });
|
|
90
112
|
}
|
|
91
113
|
for (const entries of Object.values(index.arquivos)) {
|
|
92
114
|
entries.sort((a, b) => (a.data ?? '').localeCompare(b.data ?? '') || a.trabalho.localeCompare(b.trabalho));
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { ARCHIVE_DIR_NAME, readArtifact, REQUIRED_BY_WORK_TYPE, WORK_ID, WORKS_DIR } from '../contratos/artefatos.js';
|
|
5
|
+
import { summarizeWork } from '../memoria/indice.js';
|
|
6
|
+
import { ARCHIVE_DIR, appendArchiveEntries, readArchiveIndex } from './indice-arquivo.js';
|
|
7
|
+
export const IGNORE_FILE = '.ignore';
|
|
8
|
+
export const IGNORE_LINE = `${toPosix(ARCHIVE_DIR)}/`;
|
|
9
|
+
const CLOSED_PLAN = ['concluido', 'cancelado', 'legado'];
|
|
10
|
+
const REPORTS = ['relatorios/TECNICO.md', 'relatorios/CLIENTE.md'];
|
|
11
|
+
const PLANLESS_TYPES = ['investigacao', 'auditoria'];
|
|
12
|
+
const DAY_MS = 86_400_000;
|
|
13
|
+
function toPosix(path) {
|
|
14
|
+
return path.split('\\').join('/');
|
|
15
|
+
}
|
|
16
|
+
export function workPath(id) {
|
|
17
|
+
return toPosix(join(WORKS_DIR, id));
|
|
18
|
+
}
|
|
19
|
+
export function archiveDestination(id) {
|
|
20
|
+
return toPosix(join(ARCHIVE_DIR, id.slice(0, 4), id));
|
|
21
|
+
}
|
|
22
|
+
function frontmatter(root, id, rel) {
|
|
23
|
+
const path = join(root, WORKS_DIR, id, rel);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return null;
|
|
26
|
+
return readArtifact(`${workPath(id)}/${rel}`, readFileSync(path, 'utf8')).data ?? null;
|
|
27
|
+
}
|
|
28
|
+
function hasOpenBlock(data) {
|
|
29
|
+
if (!data || !Array.isArray(data.bloqueios))
|
|
30
|
+
return false;
|
|
31
|
+
return data.bloqueios.some((entry) => typeof entry === 'object' && entry !== null && entry.status === 'aberto');
|
|
32
|
+
}
|
|
33
|
+
function ageInDays(date, now) {
|
|
34
|
+
const today = Date.parse(`${now.toISOString().slice(0, 10)}T00:00:00Z`);
|
|
35
|
+
return (today - Date.parse(`${date.slice(0, 10)}T00:00:00Z`)) / DAY_MS;
|
|
36
|
+
}
|
|
37
|
+
/** Motivo de o trabalho ficar onde está, ou null quando ele pode ser arquivado. */
|
|
38
|
+
function reasonToKeep(root, id, options) {
|
|
39
|
+
if (!WORK_ID.test(id))
|
|
40
|
+
return 'nome fora do padrão AAAA-MM-DD-<slug>';
|
|
41
|
+
if (options.ativos.includes(id))
|
|
42
|
+
return 'trabalho ativo';
|
|
43
|
+
const { resumo, problemas } = summarizeWork(root, id);
|
|
44
|
+
if (problemas.length)
|
|
45
|
+
return `frontmatter ilegível: ${problemas.map((p) => `${p.arquivo}: ${p.mensagem}`).join('; ')}`;
|
|
46
|
+
if (!resumo)
|
|
47
|
+
return 'frontmatter ilegível: nenhum artefato do contrato na pasta';
|
|
48
|
+
if (hasOpenBlock(frontmatter(root, id, 'BLOQUEIOS.md')))
|
|
49
|
+
return 'bloqueio aberto em BLOQUEIOS.md';
|
|
50
|
+
const plan = frontmatter(root, id, 'PLAN.md');
|
|
51
|
+
if (plan) {
|
|
52
|
+
const status = typeof plan.status === 'string' ? plan.status : 'PLAN.md sem status';
|
|
53
|
+
if (!CLOSED_PLAN.includes(status))
|
|
54
|
+
return status;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const type = resumo.trabalho_tipo ?? '';
|
|
58
|
+
const required = PLANLESS_TYPES.includes(type) ? REQUIRED_BY_WORK_TYPE[type] : undefined;
|
|
59
|
+
if (!required) {
|
|
60
|
+
return `sem PLAN.md: só investigacao ou auditoria com relatórios final são arquivadas (trabalho_tipo ${resumo.trabalho_tipo ?? 'ausente'})`;
|
|
61
|
+
}
|
|
62
|
+
const reports = REPORTS.map((rel) => ({ rel, data: frontmatter(root, id, rel) })).filter((report) => report.data);
|
|
63
|
+
const missing = required.filter((rel) => !reports.some((report) => report.rel === rel));
|
|
64
|
+
if (missing.length)
|
|
65
|
+
return `relatório ausente: ${missing.join(', ')}`;
|
|
66
|
+
const pending = reports.filter((report) => report.data?.status !== 'final').map((report) => report.rel);
|
|
67
|
+
if (pending.length)
|
|
68
|
+
return `relatório ainda não final: ${pending.join(', ')}`;
|
|
69
|
+
}
|
|
70
|
+
if (!resumo.data)
|
|
71
|
+
return 'sem data de conclusão nem de atualização';
|
|
72
|
+
if (ageInDays(resumo.data, options.now) <= options.dias)
|
|
73
|
+
return `concluído há menos de ${options.dias} dias`;
|
|
74
|
+
const destino = archiveDestination(id);
|
|
75
|
+
if (existsSync(join(root, destino)))
|
|
76
|
+
return `destino já existe: ${destino}`;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/** Separa os trabalhos que podem ser arquivados dos que ficam, sem alterar nenhum arquivo. */
|
|
80
|
+
export function planArchive(root, options) {
|
|
81
|
+
const plan = { elegiveis: [], ignorados: [] };
|
|
82
|
+
const worksDir = join(root, WORKS_DIR);
|
|
83
|
+
if (!existsSync(worksDir))
|
|
84
|
+
return plan;
|
|
85
|
+
const folders = readdirSync(worksDir)
|
|
86
|
+
.filter((name) => name !== ARCHIVE_DIR_NAME && statSync(join(worksDir, name)).isDirectory())
|
|
87
|
+
.sort();
|
|
88
|
+
for (const id of folders) {
|
|
89
|
+
const motivo = reasonToKeep(root, id, options);
|
|
90
|
+
if (motivo)
|
|
91
|
+
plan.ignorados.push({ id, motivo });
|
|
92
|
+
else
|
|
93
|
+
plan.elegiveis.push({ id, destino: archiveDestination(id) });
|
|
94
|
+
}
|
|
95
|
+
return plan;
|
|
96
|
+
}
|
|
97
|
+
export function git(root, args) {
|
|
98
|
+
try {
|
|
99
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function isVersioned(root, path) {
|
|
106
|
+
return Boolean(git(root, ['ls-files', '--', path])?.trim());
|
|
107
|
+
}
|
|
108
|
+
function recoverableCommit(root, path) {
|
|
109
|
+
if (!isVersioned(root, path))
|
|
110
|
+
return null;
|
|
111
|
+
if (git(root, ['status', '--porcelain', '--', path]) !== '')
|
|
112
|
+
return null;
|
|
113
|
+
return git(root, ['rev-parse', 'HEAD'])?.trim() || null;
|
|
114
|
+
}
|
|
115
|
+
function ensureIgnoreLine(root) {
|
|
116
|
+
const file = join(root, IGNORE_FILE);
|
|
117
|
+
const current = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
118
|
+
const lines = current.split(/\r?\n/).map((line) => line.trim());
|
|
119
|
+
if (lines.includes(IGNORE_LINE) || lines.includes(`/${IGNORE_LINE}`))
|
|
120
|
+
return;
|
|
121
|
+
const prefix = current && !current.endsWith('\n') ? '\n' : '';
|
|
122
|
+
writeFileSync(file, `${current}${prefix}${IGNORE_LINE}\n`);
|
|
123
|
+
}
|
|
124
|
+
/** Move os elegíveis para `_arquivo/<ano>/<id>/`, registra cada um no índice e completa o `.ignore`. */
|
|
125
|
+
export function applyArchive(root, plan, options) {
|
|
126
|
+
const archivedAt = options.now.toISOString().slice(0, 10);
|
|
127
|
+
const archived = [];
|
|
128
|
+
let movedAny = false;
|
|
129
|
+
try {
|
|
130
|
+
for (const { id, destino } of plan.elegiveis) {
|
|
131
|
+
const caminho = workPath(id);
|
|
132
|
+
const { resumo } = summarizeWork(root, id);
|
|
133
|
+
if (!resumo)
|
|
134
|
+
throw new Error(`Trabalho sem artefato legível, não arquivado: ${caminho}`);
|
|
135
|
+
if (existsSync(join(root, destino)))
|
|
136
|
+
throw new Error(`Destino já existe, trabalho não arquivado: ${destino}`);
|
|
137
|
+
const commit = recoverableCommit(root, caminho);
|
|
138
|
+
mkdirSync(dirname(join(root, destino)), { recursive: true });
|
|
139
|
+
const moved = isVersioned(root, caminho) && git(root, ['mv', caminho, destino]) !== null;
|
|
140
|
+
if (!moved)
|
|
141
|
+
renameSync(join(root, caminho), join(root, destino));
|
|
142
|
+
movedAny = true;
|
|
143
|
+
const { trabalho_tipo, titulo, status, criado_em, data, arquivos, causa_raiz } = resumo;
|
|
144
|
+
appendArchiveEntries(root, [
|
|
145
|
+
{ v: 1, id, trabalho_tipo, titulo, status, criado_em, data, arquivos, causa_raiz, caminho, commit, arquivado_em: archivedAt },
|
|
146
|
+
]);
|
|
147
|
+
archived.push({ id, commit });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (movedAny)
|
|
152
|
+
ensureIgnoreLine(root);
|
|
153
|
+
}
|
|
154
|
+
return archived;
|
|
155
|
+
}
|
|
156
|
+
/** `rev:./path` resolve a partir de `root`, que pode ser subpasta do repositório. */
|
|
157
|
+
function treeOf(root, rev, path) {
|
|
158
|
+
return git(root, ['rev-parse', '--verify', '--quiet', `${rev}:./${path}`])?.trim() || null;
|
|
159
|
+
}
|
|
160
|
+
function reasonToKeepArchived(root, entry, destino) {
|
|
161
|
+
if (!entry.commit)
|
|
162
|
+
return 'sem commit para recuperar';
|
|
163
|
+
if (git(root, ['rev-parse', '--git-dir']) === null)
|
|
164
|
+
return 'projeto fora de um repositório git: o commit não tem como recuperar a pasta';
|
|
165
|
+
const original = treeOf(root, entry.commit, entry.caminho);
|
|
166
|
+
if (!original)
|
|
167
|
+
return `o commit ${entry.commit} não recupera ${entry.caminho}`;
|
|
168
|
+
const current = treeOf(root, 'HEAD', destino);
|
|
169
|
+
if (!current)
|
|
170
|
+
return 'arquivado agora: faça commit e rode de novo';
|
|
171
|
+
if (current !== original)
|
|
172
|
+
return 'alterada depois do arquivamento';
|
|
173
|
+
const status = (git(root, ['status', '--porcelain', '--ignored', '--', destino]) ?? '').split('\n').filter(Boolean);
|
|
174
|
+
if (status.some((line) => !line.startsWith('??') && !line.startsWith('!!')))
|
|
175
|
+
return 'alterada depois do arquivamento';
|
|
176
|
+
if (status.length)
|
|
177
|
+
return 'há arquivos não versionados ou ignorados na pasta';
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
/** Apaga com `git rm` as pastas arquivadas que o commit da linha do índice recupera; as demais ficam, com o motivo. */
|
|
181
|
+
export function removeArchived(root) {
|
|
182
|
+
const result = { removidos: [], mantidos: [] };
|
|
183
|
+
for (const entry of readArchiveIndex(root).trabalhos) {
|
|
184
|
+
const destino = archiveDestination(entry.id);
|
|
185
|
+
if (!existsSync(join(root, destino)))
|
|
186
|
+
continue;
|
|
187
|
+
const motivo = reasonToKeepArchived(root, entry, destino);
|
|
188
|
+
if (motivo)
|
|
189
|
+
result.mantidos.push({ id: entry.id, motivo });
|
|
190
|
+
else if (git(root, ['rm', '-r', '-q', '--', destino]) === null)
|
|
191
|
+
result.mantidos.push({ id: entry.id, motivo: 'git rm falhou' });
|
|
192
|
+
else
|
|
193
|
+
result.removidos.push(entry.id);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { EOL } from 'node:os';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { suggestFlag } from '../install/flags.js';
|
|
5
|
+
import { buildIndex } from '../memoria/indice.js';
|
|
6
|
+
import { acharRaiz } from '../raiz.js';
|
|
7
|
+
import { loadRastroLib } from '../rastro/command.js';
|
|
8
|
+
import { applyArchive, planArchive, removeArchived } from './arquivar.js';
|
|
9
|
+
import { listWorks } from './listar.js';
|
|
10
|
+
export const ARCHIVE_DEFAULT_DAYS = 60;
|
|
11
|
+
const SUBCOMMANDS = ['arquivar', 'listar'];
|
|
12
|
+
const ARCHIVE_FLAGS = { valued: ['--antes-de'], boolean: ['--remover', '--yes'] };
|
|
13
|
+
const LIST_FLAGS = { valued: ['--busca', '--tipo', '--status', '--limite'], boolean: [] };
|
|
14
|
+
function parseArgs(argv, spec) {
|
|
15
|
+
let path;
|
|
16
|
+
const values = new Map();
|
|
17
|
+
const switches = new Set();
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const token = argv[i] ?? '';
|
|
20
|
+
if (!token.startsWith('-')) {
|
|
21
|
+
if (path !== undefined)
|
|
22
|
+
throw new Error(`Argumento inesperado: ${token}`);
|
|
23
|
+
path = token;
|
|
24
|
+
}
|
|
25
|
+
else if (spec.boolean.includes(token)) {
|
|
26
|
+
switches.add(token);
|
|
27
|
+
}
|
|
28
|
+
else if (spec.valued.includes(token)) {
|
|
29
|
+
const value = argv[++i];
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
throw new Error(`${token} precisa de um valor.`);
|
|
32
|
+
values.set(token, value);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const suggestion = suggestFlag(token, [...spec.valued, ...spec.boolean]);
|
|
36
|
+
throw new Error(`Flag desconhecida: ${token}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const root = path === undefined ? acharRaiz() : resolve(process.cwd(), path);
|
|
40
|
+
if (!existsSync(root))
|
|
41
|
+
throw new Error(`Pasta não encontrada: ${root}`);
|
|
42
|
+
return { root, values, switches };
|
|
43
|
+
}
|
|
44
|
+
function parseCount(value, min, error) {
|
|
45
|
+
if (!/^\d+$/.test(value) || Number(value) < min)
|
|
46
|
+
throw new Error(error);
|
|
47
|
+
return Number(value);
|
|
48
|
+
}
|
|
49
|
+
async function runArchive(argv) {
|
|
50
|
+
const { root, values, switches } = parseArgs(argv, ARCHIVE_FLAGS);
|
|
51
|
+
const rawDays = values.get('--antes-de');
|
|
52
|
+
const dias = rawDays === undefined ? ARCHIVE_DEFAULT_DAYS : parseCount(rawDays, 1, '--antes-de precisa de um número de dias inteiro e maior que zero.');
|
|
53
|
+
const now = new Date();
|
|
54
|
+
const ativos = (await loadRastroLib()).findActiveWorks(root);
|
|
55
|
+
const plan = planArchive(root, { now, dias, ativos });
|
|
56
|
+
const lines = [
|
|
57
|
+
`Elegíveis para arquivar (${plan.elegiveis.length}):`,
|
|
58
|
+
...plan.elegiveis.map(({ id, destino }) => ` ${id} -> ${destino}`),
|
|
59
|
+
`Ignorados (${plan.ignorados.length}):`,
|
|
60
|
+
...plan.ignorados.map(({ id, motivo }) => ` ${id}: ${motivo}`),
|
|
61
|
+
];
|
|
62
|
+
if (!switches.has('--yes')) {
|
|
63
|
+
lines.push('Simulação — nada foi movido. Rode de novo com --yes para arquivar.');
|
|
64
|
+
console.log(lines.join(EOL));
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const archived = applyArchive(root, plan, { now });
|
|
68
|
+
lines.push(`Arquivados (${archived.length}):`, ...archived.map(({ id, commit }) => ` ${id}${commit ? ` (commit ${commit.slice(0, 12)})` : ' (sem commit)'}`));
|
|
69
|
+
if (switches.has('--remover')) {
|
|
70
|
+
const { removidos, mantidos } = removeArchived(root);
|
|
71
|
+
lines.push(`Removidos do disco (${removidos.length}):`, ...removidos.map((id) => ` ${id}`), `Mantidos no disco (${mantidos.length}):`, ...mantidos.map(({ id, motivo }) => ` ${id}: ${motivo}`));
|
|
72
|
+
}
|
|
73
|
+
console.log(lines.join(EOL));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
function runList(argv) {
|
|
77
|
+
const { root, values } = parseArgs(argv, LIST_FLAGS);
|
|
78
|
+
const options = {};
|
|
79
|
+
const busca = values.get('--busca');
|
|
80
|
+
const tipo = values.get('--tipo');
|
|
81
|
+
const status = values.get('--status');
|
|
82
|
+
const limite = values.get('--limite');
|
|
83
|
+
if (busca !== undefined)
|
|
84
|
+
options.busca = busca;
|
|
85
|
+
if (tipo !== undefined)
|
|
86
|
+
options.tipo = tipo;
|
|
87
|
+
if (status !== undefined)
|
|
88
|
+
options.status = status;
|
|
89
|
+
if (limite !== undefined)
|
|
90
|
+
options.limite = parseCount(limite, 0, '--limite precisa de um número inteiro.');
|
|
91
|
+
console.log(listWorks(buildIndex(root), options).join(EOL));
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
/** `ai-harness trabalhos arquivar [caminho] [--antes-de <dias>] [--remover] [--yes]` e `trabalhos listar [caminho] [filtros]`. */
|
|
95
|
+
export async function runWorksCommand(argv) {
|
|
96
|
+
const [subcommand, ...rest] = argv;
|
|
97
|
+
if (subcommand === 'arquivar')
|
|
98
|
+
return runArchive(rest);
|
|
99
|
+
if (subcommand === 'listar')
|
|
100
|
+
return runList(rest);
|
|
101
|
+
if (!subcommand)
|
|
102
|
+
throw new Error(`Informe o subcomando: ${SUBCOMMANDS.join(' ou ')}. Ex.: npx @fontdata/ai-harness trabalhos listar`);
|
|
103
|
+
const suggestion = suggestFlag(subcommand, SUBCOMMANDS);
|
|
104
|
+
throw new Error(`Subcomando desconhecido: ${subcommand}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''} Use: ${SUBCOMMANDS.join(', ')}.`);
|
|
105
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ARCHIVE_DIR_NAME, WORKS_DIR } from '../contratos/artefatos.js';
|
|
4
|
+
export { ARCHIVE_DIR_NAME };
|
|
5
|
+
export const ARCHIVE_DIR = join(WORKS_DIR, ARCHIVE_DIR_NAME);
|
|
6
|
+
export const ARCHIVE_INDEX = join(ARCHIVE_DIR, 'INDICE.jsonl');
|
|
7
|
+
const text = (value) => (typeof value === 'string' ? value : null);
|
|
8
|
+
function ordered(raw) {
|
|
9
|
+
return {
|
|
10
|
+
v: 1,
|
|
11
|
+
id: raw.id,
|
|
12
|
+
trabalho_tipo: text(raw.trabalho_tipo),
|
|
13
|
+
titulo: text(raw.titulo),
|
|
14
|
+
status: text(raw.status),
|
|
15
|
+
criado_em: text(raw.criado_em),
|
|
16
|
+
data: text(raw.data),
|
|
17
|
+
arquivos: Array.isArray(raw.arquivos) ? raw.arquivos.filter((a) => typeof a === 'string') : [],
|
|
18
|
+
causa_raiz: text(raw.causa_raiz),
|
|
19
|
+
caminho: text(raw.caminho) ?? join(WORKS_DIR, raw.id).replaceAll('\\', '/'),
|
|
20
|
+
commit: text(raw.commit),
|
|
21
|
+
arquivado_em: text(raw.arquivado_em) ?? '',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Lê o índice do arquivo; vale a última linha de cada id, e a última linha sem `\n` é ignorada. */
|
|
25
|
+
export function readArchiveIndex(root) {
|
|
26
|
+
const file = join(root, ARCHIVE_INDEX);
|
|
27
|
+
if (!existsSync(file))
|
|
28
|
+
return { trabalhos: [], problemas: [] };
|
|
29
|
+
const lines = readFileSync(file, 'utf8').split('\n');
|
|
30
|
+
lines.pop();
|
|
31
|
+
const byId = new Map();
|
|
32
|
+
const problemas = [];
|
|
33
|
+
lines.forEach((line, index) => {
|
|
34
|
+
if (!line.trim())
|
|
35
|
+
return;
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(line);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
problemas.push({ linha: index + 1, mensagem: 'linha não é JSON válido' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || typeof parsed.id !== 'string') {
|
|
45
|
+
problemas.push({ linha: index + 1, mensagem: 'linha sem `id` texto' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { v } = parsed;
|
|
49
|
+
if (typeof v === 'number' && v > 1) {
|
|
50
|
+
problemas.push({ linha: index + 1, mensagem: `versão ${v} do índice é mais nova que a suportada (1); atualize o ai-harness` });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const entry = ordered(parsed);
|
|
54
|
+
byId.delete(entry.id);
|
|
55
|
+
byId.set(entry.id, entry);
|
|
56
|
+
});
|
|
57
|
+
return { trabalhos: [...byId.values()], problemas };
|
|
58
|
+
}
|
|
59
|
+
/** Acrescenta uma linha por entrada ao índice, sem reescrever as anteriores. */
|
|
60
|
+
export function appendArchiveEntries(root, entries) {
|
|
61
|
+
if (entries.length === 0)
|
|
62
|
+
return;
|
|
63
|
+
const file = join(root, ARCHIVE_INDEX);
|
|
64
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
65
|
+
const current = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
66
|
+
const prefix = current && !current.endsWith('\n') ? '\n' : '';
|
|
67
|
+
appendFileSync(file, prefix + entries.map((entry) => `${JSON.stringify(ordered(entry))}\n`).join(''));
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const NO_WORKS_FOUND = 'Nenhum trabalho encontrado.';
|
|
2
|
+
const fold = (value) => value.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
|
|
3
|
+
const show = (value) => value ?? '?';
|
|
4
|
+
function compareWorks([idA, a], [idB, b]) {
|
|
5
|
+
if (a.data !== b.data) {
|
|
6
|
+
if (!a.data)
|
|
7
|
+
return 1;
|
|
8
|
+
if (!b.data)
|
|
9
|
+
return -1;
|
|
10
|
+
return a.data < b.data ? 1 : -1;
|
|
11
|
+
}
|
|
12
|
+
return idA < idB ? 1 : idA > idB ? -1 : 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Linhas `id | tipo | status | título` dos trabalhos do índice, arquivados incluídos, da data mais recente
|
|
16
|
+
* para a mais antiga. `busca` casa com id ou título sem diferenciar caixa nem acento; `tipo` e `status`
|
|
17
|
+
* comparam com o valor cru. Sem resultado, devolve só a linha de nenhum trabalho encontrado.
|
|
18
|
+
*/
|
|
19
|
+
export function listWorks(index, options = {}) {
|
|
20
|
+
const { busca, tipo, status, limite = 20 } = options;
|
|
21
|
+
const term = busca ? fold(busca) : null;
|
|
22
|
+
const lines = Object.entries(index.trabalhos)
|
|
23
|
+
.filter(([id, work]) => {
|
|
24
|
+
if (tipo !== undefined && work.trabalho_tipo !== tipo)
|
|
25
|
+
return false;
|
|
26
|
+
if (status !== undefined && work.status !== status)
|
|
27
|
+
return false;
|
|
28
|
+
return !term || fold(id).includes(term) || (work.titulo !== null && fold(work.titulo).includes(term));
|
|
29
|
+
})
|
|
30
|
+
.sort(compareWorks)
|
|
31
|
+
.slice(0, Math.max(0, limite))
|
|
32
|
+
.map(([id, work]) => {
|
|
33
|
+
const workStatus = work.arquivado ? `${show(work.status)} (arquivado)` : show(work.status);
|
|
34
|
+
return [id, show(work.trabalho_tipo), workStatus, show(work.titulo)].join(' | ');
|
|
35
|
+
});
|
|
36
|
+
return lines.length > 0 ? lines : [NO_WORKS_FOUND];
|
|
37
|
+
}
|
package/dist/update/update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
-
import { allCommands, loadCatalog } from '../catalog.js';
|
|
4
|
-
import { commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
|
|
3
|
+
import { allCommands, loadCatalog, templatesDir } from '../catalog.js';
|
|
4
|
+
import { COMMENTS_HOOK_PATH, commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
|
|
5
5
|
import { CONFIG_PATH, gravarInstalarComentarios, gravarRegistrarMcp, migrarSecaoBanco, readConfig, syncGitignore, temSecaoBancoLegada, writeDefaultConfig, } from '../config/config.js';
|
|
6
6
|
import { applyAtomically } from '../install/atomic.js';
|
|
7
7
|
import { suggestFlag } from '../install/flags.js';
|
|
@@ -50,6 +50,12 @@ function legacyPath(root, path) {
|
|
|
50
50
|
return candidate;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
const COMMENTS_HOOK_TEMPLATE = join(templatesDir, 'hooks', 'comentarios', 'check.mjs');
|
|
54
|
+
/** O hook de comentários existe no projeto com conteúdo diferente do template do pacote. */
|
|
55
|
+
function hookProprio(root) {
|
|
56
|
+
const local = join(root, COMMENTS_HOOK_PATH);
|
|
57
|
+
return existsSync(local) && hashContent(readFileSync(local)) !== hashContent(readFileSync(COMMENTS_HOOK_TEMPLATE));
|
|
58
|
+
}
|
|
53
59
|
export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBanco = false, semComentarios = false } = {}) {
|
|
54
60
|
const recorded = readLock(root);
|
|
55
61
|
const lock = recorded ?? adoptLegacyInstall(root, legacy);
|
|
@@ -139,7 +145,10 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBa
|
|
|
139
145
|
}
|
|
140
146
|
// O padrão de comentários também chega ao lock anterior a ele, salvo com a recusa no config ou na flag. O hook é a
|
|
141
147
|
// referência: o comando `comentarios` não registra a rule quando o projeto já tem uma própria.
|
|
142
|
-
const
|
|
148
|
+
const querComentarios = !semComentarios && config.comentarios.instalar;
|
|
149
|
+
const hookNoLock = trackedItems.has('hook:comentarios');
|
|
150
|
+
const registrarHookComentarios = querComentarios && !hookNoLock && hookProprio(root);
|
|
151
|
+
const registrarComentarios = querComentarios && !registrarHookComentarios && (adotada || !hookNoLock);
|
|
143
152
|
if (registrarComentarios) {
|
|
144
153
|
addItem('rule', 'comentarios', true);
|
|
145
154
|
addItem('hook', 'comentarios', true);
|
|
@@ -159,6 +168,7 @@ export function checkUpdate(root, catalog = loadCatalog(), legacy, home, { semBa
|
|
|
159
168
|
adotada,
|
|
160
169
|
registrarHooks: adotada && !pluginHooks,
|
|
161
170
|
registrarComentarios,
|
|
171
|
+
registrarHookComentarios,
|
|
162
172
|
recusarComentarios: semComentarios && config.comentarios.instalar,
|
|
163
173
|
permissaoAusente: permissoesFaltando.length > 0,
|
|
164
174
|
permissoesFaltando,
|
|
@@ -281,6 +291,10 @@ export function applyUpdate(root, report, options, pkg, now = new Date()) {
|
|
|
281
291
|
for (const added of addedItems.values()) {
|
|
282
292
|
itens.push({ ...added, arquivos: added.arquivos.sort((a, b) => a.caminho.localeCompare(b.caminho)) });
|
|
283
293
|
}
|
|
294
|
+
// Com o hash do template, o arquivo do projeto aparece como modificado-local e nunca é regravado sem --forcar.
|
|
295
|
+
if (report.registrarHookComentarios) {
|
|
296
|
+
itens.push({ tipo: 'hook', id: 'comentarios', arquivos: [{ caminho: COMMENTS_HOOK_PATH, sha256: hashContent(readFileSync(COMMENTS_HOOK_TEMPLATE)) }] });
|
|
297
|
+
}
|
|
284
298
|
writeLock(root, { lock_versao: LOCK_VERSION, pacote: pkg, gerado_em: now.toISOString(), itens });
|
|
285
299
|
return { written: write.map((file) => file.path), removed: remove, kept, renamed, avisos, ajustes };
|
|
286
300
|
}
|
|
@@ -321,6 +335,12 @@ export function describeUpdate(report) {
|
|
|
321
335
|
`Para recusar, rode com --sem-comentarios, que grava comentarios.instalar: false em ${CONFIG_PATH}.`,
|
|
322
336
|
]
|
|
323
337
|
: []),
|
|
338
|
+
...(report.registrarHookComentarios
|
|
339
|
+
? [
|
|
340
|
+
`O hook de comentários ${COMMENTS_HOOK_PATH} é do projeto (instalado por versão antiga ou adaptado) e não está no lock: ` +
|
|
341
|
+
'o --yes o deixa registrado no lock como alterado localmente, sem tocar no arquivo.',
|
|
342
|
+
]
|
|
343
|
+
: []),
|
|
324
344
|
...(report.recusarComentarios ? [`--sem-comentarios: o --yes grava comentarios.instalar: false em ${CONFIG_PATH} e não instala o padrão de comentários.`] : []),
|
|
325
345
|
];
|
|
326
346
|
const legadas = report.permissoesLegadas.length
|
|
@@ -386,7 +406,7 @@ export function runUpdateCommand(argv) {
|
|
|
386
406
|
const report = checkUpdate(root, undefined, undefined, undefined, { semBanco: args.semBanco, semComentarios: args.semComentarios });
|
|
387
407
|
console.log(describeUpdate(report));
|
|
388
408
|
// Sem arquivo a atualizar, a adoção ainda grava o lock, e a permissão e o MCP ainda são acrescentados.
|
|
389
|
-
const pendente = report.adotada || report.permissaoAusente || report.permissoesLegadas.length > 0 || report.mudancaMcp !== null || report.recusarMcp || report.recusarComentarios || report.renomearSecaoBanco;
|
|
409
|
+
const pendente = report.adotada || report.permissaoAusente || report.permissoesLegadas.length > 0 || report.mudancaMcp !== null || report.recusarMcp || report.recusarComentarios || report.registrarHookComentarios || report.renomearSecaoBanco;
|
|
390
410
|
if (args.check || (!report.changes.length && !pendente))
|
|
391
411
|
return 0;
|
|
392
412
|
if (!args.yes) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fontdata/ai-harness",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.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": {
|
|
@@ -14,8 +14,8 @@ ou quando ficou claro que ela ainda é hipótese. Você não corrige o bug.
|
|
|
14
14
|
2. **Levante hipóteses** e, para cada uma, busque evidência que a confirme ou a descarte:
|
|
15
15
|
- leitura do código envolvido;
|
|
16
16
|
- histórico: `git log -p -S "<trecho>"`, `git log --follow <arquivo>`, `git blame -L <ini>,<fim> <arquivo>`;
|
|
17
|
-
- trabalhos anteriores que tocaram os mesmos arquivos
|
|
18
|
-
|
|
17
|
+
- trabalhos anteriores que tocaram os mesmos arquivos: `npx @fontdata/ai-harness memoria --arquivo <caminho>` e
|
|
18
|
+
`npx @fontdata/ai-harness memoria --regressao <id>`, que incluem os trabalhos arquivados.
|
|
19
19
|
3. **Comprove.** A causa raiz só é `comprovada` quando você mostra a linha ou a condição que produz o sintoma e
|
|
20
20
|
por que a reprodução falha por causa dela. Caso contrário, ela é `hipotese`.
|
|
21
21
|
4. Use `Bash` só para ler, buscar no histórico e reproduzir: você é somente leitura, então não altere arquivos, instale dependências,
|
|
@@ -170,6 +170,10 @@ const ehTexto = (c) => typeof c === 'string';
|
|
|
170
170
|
function analisar(caminho, fonte) {
|
|
171
171
|
const ext = extname(caminho);
|
|
172
172
|
const linhas = fonte.split('\n');
|
|
173
|
+
if (PASCAL.has(ext)) {
|
|
174
|
+
const fim = linhas.findLastIndex((l) => /^\s*end\.\s*$/i.test(l));
|
|
175
|
+
if (fim >= 0) linhas.length = fim + 1;
|
|
176
|
+
}
|
|
173
177
|
const achados = [];
|
|
174
178
|
const marca = (linha, msg) => achados.push({ linha, msg });
|
|
175
179
|
const ehComentario = classificar(linhas, ext);
|
|
@@ -46,7 +46,8 @@ Se o porquê não cabe em 3 linhas, **ele não pertence ao código**.
|
|
|
46
46
|
| Por que a mudança foi feita | mensagem de commit | comentário |
|
|
47
47
|
| Task / fase / plano / spec | `PLAN.md` e `SPEC.md` do trabalho em `docs/trabalhos/<id>/` | comentário |
|
|
48
48
|
| Diagnóstico e correção de um defeito | trabalho do tipo `bug` em `docs/trabalhos/<id>/` | comentário |
|
|
49
|
-
| Investigação, medição, auditoria, paridade, comparação |
|
|
49
|
+
| Investigação, medição, auditoria, paridade, comparação | `relatorios/TECNICO.md` do trabalho em `docs/trabalhos/<id>/` | comentário |
|
|
50
|
+
| Documento de apoio do trabalho (imagem, planilha, export, contrato de API) | `anexos/` do trabalho em `docs/trabalhos/<id>/` | comentário |
|
|
50
51
|
| Achado de review | resposta do review, commit | comentário |
|
|
51
52
|
| Decisão de arquitetura | `SPEC.md` | comentário |
|
|
52
53
|
| Armadilha ao editar **aquela linha** | comentário de até 3 linhas | — |
|
|
@@ -56,7 +57,9 @@ correção aplicada no código — é trabalho do tipo `bug` (`/resolver-bug`).
|
|
|
56
57
|
auditoria, paridade com legado, medição, comparação, levantamento, handoff ou follow-up — é
|
|
57
58
|
`auditoria` ou `investigacao`. Se o documento descreve trabalho a fazer, é `feature` (`/nova-tarefa`).
|
|
58
59
|
|
|
59
|
-
Convenção: uma pasta por trabalho, `docs/trabalhos/<yyyy-mm-dd>-<slug>/`.
|
|
60
|
+
Convenção: uma pasta por trabalho, `docs/trabalhos/<yyyy-mm-dd>-<slug>/`. Relatório não ganha nome livre em
|
|
61
|
+
`relatorios/`: o conteúdo vai para `relatorios/TECNICO.md` e o arquivo de apoio para `anexos/`, senão o `doctor`
|
|
62
|
+
avisa `arquivo-fora-do-contrato`.
|
|
60
63
|
|
|
61
64
|
## Antes e depois
|
|
62
65
|
|
|
@@ -154,6 +157,7 @@ function HashApiKey(const APlainKey: string): string;
|
|
|
154
157
|
- **TypeScript/JavaScript/Java/C#/C++/Go/Rust/PHP:** `/** ... */` no topo do símbolo **público**.
|
|
155
158
|
- **Python:** docstring `"""..."""` curta logo abaixo da assinatura.
|
|
156
159
|
- **Delphi/Pascal:** `///` (XMLDoc) nas rotinas públicas da `interface`; nunca repetir na `implementation`.
|
|
160
|
+
O que vem depois do último `end.` (o rodapé de histórico, que o compilador ignora) fica fora deste padrão e do hook.
|
|
157
161
|
- **SQL/Shell/YAML:** somente regra de negócio ou pegadinha não evidente.
|
|
158
162
|
|
|
159
163
|
## Enforcement
|
|
@@ -100,8 +100,9 @@ cada tipo na sua pasta dentro de `docs/`). Hoje cada trabalho mora em `docs/trab
|
|
|
100
100
|
|
|
101
101
|
## 8. O que criar
|
|
102
102
|
|
|
103
|
-
Sugira peças novas para dores recorrentes.
|
|
104
|
-
relatórios
|
|
103
|
+
Sugira peças novas para dores recorrentes. Comece por `npx @fontdata/ai-harness trabalhos listar` e busque
|
|
104
|
+
evidência em `DECISOES.md`, `BLOQUEIOS.md` e relatórios só dos trabalhos ativos e dos indicados por ele, em achados
|
|
105
|
+
de review repetidos e em `npx @fontdata/ai-harness memoria --arquivo <arquivo>`. Para cada
|
|
105
106
|
sugestão: tipo (`agent`, `skill`, `rule`, `hook`, `code-reviewer`, `design-system`), o que faria, quando seria
|
|
106
107
|
usada e qual **dor recorrente** elimina.
|
|
107
108
|
|
|
@@ -5,7 +5,8 @@ Cria uma skill do projeto em `.claude/skills/<nome>/` para um problema **recorre
|
|
|
5
5
|
## Leitura
|
|
6
6
|
|
|
7
7
|
`CLAUDE.md` e `SPEC.md` da raiz, as skills que já existem (do projeto e do harness) e, como evidência de
|
|
8
|
-
recorrência, `DECISOES.md`, `BLOQUEIOS.md` e relatórios
|
|
8
|
+
recorrência, `DECISOES.md`, `BLOQUEIOS.md` e relatórios dos trabalhos: comece por
|
|
9
|
+
`npx @fontdata/ai-harness trabalhos listar` e leia só os trabalhos ativos e os indicados por ele.
|
|
9
10
|
|
|
10
11
|
## Skill, rule, agent ou hook?
|
|
11
12
|
|
|
@@ -8,7 +8,8 @@ argument-hint: "[caminho de arquivo ou id de um bug]"
|
|
|
8
8
|
|
|
9
9
|
Pergunta: $ARGUMENTS
|
|
10
10
|
|
|
11
|
-
A memória é um **índice** gerado a partir do frontmatter dos trabalhos em `docs/trabalhos
|
|
11
|
+
A memória é um **índice** gerado a partir do frontmatter dos trabalhos em `docs/trabalhos/`, arquivados inclusive, e
|
|
12
|
+
do rastro de eventos.
|
|
12
13
|
Não é busca semântica: toda resposta aponta um trabalho e os documentos dele, e pode ser conferida.
|
|
13
14
|
|
|
14
15
|
## Passo 1 — Atualizar o índice
|
|
@@ -18,8 +18,8 @@ uma pergunta durante a execução é falha do plano.
|
|
|
18
18
|
|
|
19
19
|
1. Leia o `SPEC.md` e o `CLAUDE.md` da raiz e, se existir, a rule de projeto `.claude/rules/harness-projeto.md`: o
|
|
20
20
|
mapa de agents por domínio e os portões extras entram no plano.
|
|
21
|
-
2.
|
|
22
|
-
nem contradizer um trabalho em andamento.
|
|
21
|
+
2. Rode `npx @fontdata/ai-harness trabalhos listar --busca <termo do pedido>` (id, tipo, status e título, com os
|
|
22
|
+
arquivados) para não duplicar nem contradizer um trabalho em andamento.
|
|
23
23
|
3. **Start inicial** (não há trabalho do tipo `feature` ainda, ou o pedido é "start"/"primeira fatia"): derive o
|
|
24
24
|
recorte direto do `SPEC.md` — a primeira fatia vertical. **Nova tarefa:** siga para o Passo 1.
|
|
25
25
|
|
|
@@ -48,7 +48,8 @@ mais conservadora.
|
|
|
48
48
|
|
|
49
49
|
## Passo 3 — Pasta do trabalho e SPEC.md
|
|
50
50
|
|
|
51
|
-
1. Defina o id `AAAA-MM-DD-<slug>` (data de hoje, slug curto em minúsculas, sem acento)
|
|
51
|
+
1. Defina o id `AAAA-MM-DD-<slug>` (data de hoje, slug curto em minúsculas, sem acento) e confira que
|
|
52
|
+
`npx @fontdata/ai-harness trabalhos listar --busca <id>` não devolve nenhum trabalho, nem arquivado.
|
|
52
53
|
2. Crie `docs/trabalhos/<id>/SPEC.md` a partir de `templates/SPEC.md`, preenchendo `{{id}}`, `{{titulo}}` e
|
|
53
54
|
`{{data}}`. Os nomes dos arquivos da pasta são sempre em caixa alta.
|
|
54
55
|
A seção **Como vai operar** é obrigatória: é dela que o `/executar-plan` tira o relatório para o cliente e para
|
|
@@ -10,7 +10,8 @@ Antes de planejar, procure evidência — nunca presuma.
|
|
|
10
10
|
2. **Pontos de entrada:** rotas e controllers, menus e telas, comandos de CLI, jobs agendados.
|
|
11
11
|
3. **Dados:** tabelas e colunas (migrations, models, scripts SQL) que já guardam o que o pedido precisa.
|
|
12
12
|
4. **Configuração e flags:** a funcionalidade pode existir desligada por configuração ou feature flag.
|
|
13
|
-
5. **Trabalhos anteriores:** `
|
|
13
|
+
5. **Trabalhos anteriores:** `npx @fontdata/ai-harness trabalhos listar --busca <termo>` e, para os arquivos que o
|
|
14
|
+
pedido toca, `npx @fontdata/ai-harness memoria --arquivo <caminho>`; os dois incluem os trabalhos arquivados.
|
|
14
15
|
|
|
15
16
|
## Como concluir
|
|
16
17
|
|