@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +9 -2
- package/LICENSE +21 -0
- package/README.md +174 -140
- package/{src → dist}/catalog.js +5 -12
- package/dist/cli.js +174 -0
- package/dist/comentarios/command.js +74 -0
- package/dist/comentarios/instalar.js +33 -0
- package/dist/config/config.js +98 -0
- package/dist/contratos/artefatos.js +284 -0
- package/dist/contratos/eventos.js +103 -0
- package/dist/doctor/checks.js +424 -0
- package/dist/init.js +240 -0
- package/dist/install/atomic.js +51 -0
- package/dist/install/flags.js +99 -0
- package/dist/install/hooks.js +47 -0
- package/dist/install/lock.js +39 -0
- package/dist/install/plan.js +209 -0
- package/dist/install/selection.js +77 -0
- package/dist/install/settings.js +108 -0
- package/dist/memoria/indice.js +195 -0
- package/dist/migrar/migrar.js +426 -0
- package/dist/rastro/command.js +46 -0
- package/dist/run.js +86 -0
- package/dist/types.js +1 -0
- package/dist/update/legado.js +74 -0
- package/dist/update/update.js +284 -0
- package/package.json +17 -8
- package/templates/CLAUDE.md +59 -30
- package/templates/agents/auditor-plano.md +42 -0
- package/templates/agents/investigador.md +32 -0
- package/templates/agents/qa.md +44 -0
- package/templates/agents/revisor-testes.md +40 -0
- package/templates/catalog.json +24 -61
- package/templates/commands/atualizar-deps.md +43 -9
- package/templates/commands/diagrama.md +35 -10
- package/templates/commands/gerar-env.md +39 -10
- package/templates/commands/gerar-testes.md +42 -10
- package/templates/commands/git-commit-push.md +0 -0
- package/templates/commands/onboarding.md +44 -15
- package/templates/commands/refatorar.md +50 -10
- package/templates/commands/revisar-pr.md +56 -12
- package/templates/commands/sincronizar-claude-md.md +177 -9
- package/templates/commands/versao.md +0 -0
- package/templates/hooks/comentarios/check.mjs +210 -0
- package/templates/hooks/hooks.json +34 -0
- package/templates/hooks/lembrete.mjs +29 -0
- package/templates/hooks/lib/config.mjs +40 -0
- package/templates/hooks/lib/hook.mjs +85 -0
- package/templates/hooks/lib/rastro.mjs +130 -0
- package/templates/hooks/lib/segredos.mjs +91 -0
- package/templates/hooks/lib/temas.mjs +111 -0
- package/templates/hooks/lib/trabalho.mjs +94 -0
- package/templates/hooks/proteger-contratos.mjs +56 -0
- package/templates/hooks/segredo-no-commit.mjs +76 -0
- package/templates/hooks/sem-jargao.mjs +58 -0
- package/templates/hooks/task-so-fecha-verde.mjs +33 -0
- package/templates/legado/manifesto.json +1267 -0
- package/templates/rules/comentarios.md +166 -0
- package/templates/rules/harness-projeto.md +41 -0
- package/templates/skills/auditar-performance/SKILL.md +53 -0
- package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
- package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
- package/templates/skills/auditar-seguranca/SKILL.md +67 -0
- package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
- package/templates/skills/auditar-ui/SKILL.md +64 -0
- package/templates/skills/auditar-ui/references/lentes.md +33 -0
- package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
- package/templates/skills/evoluir-harness/SKILL.md +81 -0
- package/templates/skills/evoluir-harness/references/agent.md +83 -0
- package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
- package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
- package/templates/skills/evoluir-harness/references/design-system.md +56 -0
- package/templates/skills/evoluir-harness/references/hook.md +95 -0
- package/templates/skills/evoluir-harness/references/rule.md +67 -0
- package/templates/skills/evoluir-harness/references/skill.md +63 -0
- package/templates/skills/executar-plan/SKILL.md +114 -0
- package/templates/skills/executar-plan/references/delegacao.md +43 -0
- package/templates/skills/executar-plan/references/retomada.md +33 -0
- package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
- package/templates/skills/harness/SKILL.md +75 -0
- package/templates/skills/harness/references/auto-tag.md +118 -0
- package/templates/skills/harness/references/ignores.md +40 -0
- package/templates/skills/harness/references/scaffold.md +29 -0
- package/templates/skills/harness/references/validar-ideia.md +42 -0
- package/templates/skills/memoria/SKILL.md +52 -0
- package/templates/skills/nova-tarefa/SKILL.md +97 -0
- package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
- package/templates/skills/nova-tarefa/references/regras-do-plano.md +33 -0
- package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
- package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
- package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
- package/templates/skills/resolver-bug/SKILL.md +104 -0
- package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
- package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
- package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
- package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
- package/bin/cli.js +0 -122
- package/src/init.js +0 -317
- package/src/installer.js +0 -92
- package/src/run.js +0 -99
- package/templates/commands/auditar-performance.md +0 -27
- package/templates/commands/auditar-seguranca.md +0 -64
- package/templates/commands/auditar-ui.md +0 -38
- package/templates/commands/codigo-morto.md +0 -22
- package/templates/commands/configurar-auto-tag.md +0 -185
- package/templates/commands/configurar-comentarios.md +0 -543
- package/templates/commands/configurar-orquestrador.md +0 -13
- package/templates/commands/corrigir-review.md +0 -11
- package/templates/commands/criar-agents.md +0 -42
- package/templates/commands/criar-code-reviewer.md +0 -30
- package/templates/commands/criar-hooks.md +0 -32
- package/templates/commands/criar-rag.md +0 -25
- package/templates/commands/criar-rules.md +0 -31
- package/templates/commands/criar-skill.md +0 -22
- package/templates/commands/design-system.md +0 -27
- package/templates/commands/executar-plan.md +0 -36
- package/templates/commands/gerar-claude-md.md +0 -66
- package/templates/commands/gerar-ignore.md +0 -37
- package/templates/commands/gerar-spec.md +0 -33
- package/templates/commands/harness.md +0 -34
- package/templates/commands/nova-tarefa.md +0 -60
- package/templates/commands/resolver-bug.md +0 -25
- package/templates/commands/review-por-task.md +0 -16
- package/templates/commands/scaffold.md +0 -25
- package/templates/commands/validar-commands.md +0 -24
- package/templates/commands/validar-ideia.md +0 -36
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, sep } from 'node:path';
|
|
3
|
+
import { allCommands, recommendedCommands, templatesDir } from '../catalog.js';
|
|
4
|
+
import { runnableIds, suggestIds } from '../run.js';
|
|
5
|
+
import { applyAtomically } from './atomic.js';
|
|
6
|
+
import { buildLockItem, LOCK_VERSION, readLock, writeLock } from './lock.js';
|
|
7
|
+
import { HOOKS_ITEM_ID, PROJECT_HOOKS_DIR, projectHooksConfig } from './hooks.js';
|
|
8
|
+
import { neededSkillsFor, resolveSelection } from './selection.js';
|
|
9
|
+
import { COMMENTS_RULE_PATH, commentsHooksConfig, ensureCommentsBaseline } from '../comentarios/instalar.js';
|
|
10
|
+
import { combineHooks, EVENT_PERMISSION, mergeSettings } from './settings.js';
|
|
11
|
+
/** Rule de convenção com o que é específico do projeto (D-38): é do projeto, então fica fora do lock. */
|
|
12
|
+
export const PROJECT_RULE_PATH = '.claude/rules/harness-projeto.md';
|
|
13
|
+
/** Esqueleto da rule de projeto; os comandos `-legado` renomeados pelo update entram na última seção. */
|
|
14
|
+
export function projectRuleContent(legacy) {
|
|
15
|
+
const template = readFileSync(join(templatesDir, 'rules', 'harness-projeto.md'), 'utf8');
|
|
16
|
+
const list = legacy.map((path) => `- \`${path}\``).join('\n');
|
|
17
|
+
return list ? `${template.trimEnd()}\n\n${list}\n` : template;
|
|
18
|
+
}
|
|
19
|
+
/** Padrões do modo não interativo: o mais conservador possível para rodar em CI. */
|
|
20
|
+
export function resolveInstallOptions(args) {
|
|
21
|
+
return {
|
|
22
|
+
scope: args.scope ?? 'recommended',
|
|
23
|
+
commands: args.commands,
|
|
24
|
+
includeMaster: args.includeMaster ?? true,
|
|
25
|
+
claudeMd: args.claudeMd ?? 'create',
|
|
26
|
+
conflict: args.conflict ?? 'skip',
|
|
27
|
+
hooks: args.hooks ?? true,
|
|
28
|
+
agents: args.agents ?? true,
|
|
29
|
+
comments: args.comments ?? true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function packageInfo() {
|
|
33
|
+
const pkg = JSON.parse(readFileSync(join(templatesDir, '..', 'package.json'), 'utf8'));
|
|
34
|
+
return { nome: pkg.name, versao: pkg.version };
|
|
35
|
+
}
|
|
36
|
+
/** Arquivos de uma pasta de template, relativos a ela, com separador `/` e em ordem estável. */
|
|
37
|
+
function listFiles(dir) {
|
|
38
|
+
return readdirSync(dir, { recursive: true, encoding: 'utf8' })
|
|
39
|
+
.filter((rel) => !rel.endsWith('.DS_Store') && statSync(join(dir, rel)).isFile())
|
|
40
|
+
.map((rel) => rel.split(sep).join('/'))
|
|
41
|
+
.sort();
|
|
42
|
+
}
|
|
43
|
+
/** Arquivos que o pacote atual instala para um item; `null` quando o item saiu do catálogo. */
|
|
44
|
+
export function itemSources(catalog, tipo, id) {
|
|
45
|
+
if (tipo === 'comando') {
|
|
46
|
+
const cmd = id === catalog.master.id ? catalog.master : allCommands(catalog).find((c) => c.id === id);
|
|
47
|
+
return cmd?.file ? [{ path: `.claude/commands/${id}.md`, source: join(templatesDir, cmd.file) }] : null;
|
|
48
|
+
}
|
|
49
|
+
if (tipo === 'skill') {
|
|
50
|
+
// Skills avulsas (como a lgpd-checker) e fluxos do catálogo publicados como skill.
|
|
51
|
+
const dir = catalog.skills.find((s) => s.id === id)?.dir ??
|
|
52
|
+
allCommands(catalog).find((c) => c.id === id)?.skill ??
|
|
53
|
+
(catalog.master.id === id ? catalog.master.skill : undefined);
|
|
54
|
+
if (!dir)
|
|
55
|
+
return null;
|
|
56
|
+
const base = join(templatesDir, dir);
|
|
57
|
+
return listFiles(base).map((rel) => ({ path: `.claude/skills/${id}/${rel}`, source: join(base, rel) }));
|
|
58
|
+
}
|
|
59
|
+
if (tipo === 'claude-md' && id === catalog.claudeMd.target) {
|
|
60
|
+
return [{ path: id, source: join(templatesDir, catalog.claudeMd.file) }];
|
|
61
|
+
}
|
|
62
|
+
if (tipo === 'hook' && id === HOOKS_ITEM_ID) {
|
|
63
|
+
// Os scripts da raiz e a lib; subpastas de outros comandos (como comentarios/) ficam de fora.
|
|
64
|
+
const base = join(templatesDir, 'hooks');
|
|
65
|
+
return listFiles(base)
|
|
66
|
+
.filter((rel) => rel.endsWith('.mjs') && (!rel.includes('/') || rel.startsWith('lib/')))
|
|
67
|
+
.map((rel) => ({ path: `${PROJECT_HOOKS_DIR}/${rel}`, source: join(base, rel) }));
|
|
68
|
+
}
|
|
69
|
+
if (tipo === 'agent') {
|
|
70
|
+
const agent = catalog.agents?.find((candidate) => candidate.id === id);
|
|
71
|
+
return agent ? [{ path: `.claude/agents/${id}.md`, source: join(templatesDir, agent.file) }] : null;
|
|
72
|
+
}
|
|
73
|
+
if (tipo === 'rule' && id === 'comentarios') {
|
|
74
|
+
return [{ path: COMMENTS_RULE_PATH, source: join(templatesDir, 'rules', 'comentarios.md') }];
|
|
75
|
+
}
|
|
76
|
+
if (tipo === 'hook' && id === 'comentarios') {
|
|
77
|
+
return [{ path: '.claude/hooks/comentarios/check.mjs', source: join(templatesDir, 'hooks', 'comentarios', 'check.mjs') }];
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function selectCommands(catalog, options) {
|
|
82
|
+
if (options.scope === 'all')
|
|
83
|
+
return allCommands(catalog);
|
|
84
|
+
if (options.scope === 'recommended')
|
|
85
|
+
return recommendedCommands(catalog);
|
|
86
|
+
const known = new Set([...allCommands(catalog).map((c) => c.id), catalog.master.id]);
|
|
87
|
+
for (const id of options.commands) {
|
|
88
|
+
if (known.has(id))
|
|
89
|
+
continue;
|
|
90
|
+
const near = suggestIds(id, runnableIds(catalog));
|
|
91
|
+
throw new Error(`Comando desconhecido: ${id}${near.length ? `\nVocê quis dizer: ${near.join(', ')}` : ''}`);
|
|
92
|
+
}
|
|
93
|
+
return allCommands(catalog).filter((c) => options.commands.includes(c.id));
|
|
94
|
+
}
|
|
95
|
+
/** Calcula o que a instalação vai gravar, sem tocar no projeto. */
|
|
96
|
+
export function planInstall(catalog, root, options) {
|
|
97
|
+
const wantMaster = options.includeMaster || options.commands.includes(catalog.master.id);
|
|
98
|
+
const { selected, autoAdded } = resolveSelection(catalog, selectCommands(catalog, options), wantMaster);
|
|
99
|
+
const plan = {
|
|
100
|
+
files: [],
|
|
101
|
+
skipped: [],
|
|
102
|
+
items: [],
|
|
103
|
+
autoAdded,
|
|
104
|
+
claudeMdBackup: false,
|
|
105
|
+
registerHooks: Boolean(options.hooks),
|
|
106
|
+
registerComments: Boolean(options.comments),
|
|
107
|
+
};
|
|
108
|
+
const add = (tipo, id) => {
|
|
109
|
+
const paths = [];
|
|
110
|
+
for (const entry of itemSources(catalog, tipo, id) ?? []) {
|
|
111
|
+
if (options.conflict === 'skip' && existsSync(join(root, entry.path))) {
|
|
112
|
+
plan.skipped.push(entry.path);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
plan.files.push({ path: entry.path, content: readFileSync(entry.source) });
|
|
116
|
+
paths.push(entry.path);
|
|
117
|
+
}
|
|
118
|
+
if (paths.length)
|
|
119
|
+
plan.items.push({ tipo, id, paths });
|
|
120
|
+
};
|
|
121
|
+
for (const cmd of selected)
|
|
122
|
+
add(cmd.skill ? 'skill' : 'comando', cmd.id);
|
|
123
|
+
if (wantMaster)
|
|
124
|
+
add(catalog.master.skill ? 'skill' : 'comando', catalog.master.id);
|
|
125
|
+
for (const skill of neededSkillsFor(catalog, selected))
|
|
126
|
+
add('skill', skill.id);
|
|
127
|
+
if (options.hooks)
|
|
128
|
+
add('hook', HOOKS_ITEM_ID);
|
|
129
|
+
if (options.agents)
|
|
130
|
+
for (const agent of catalog.agents ?? [])
|
|
131
|
+
add('agent', agent.id);
|
|
132
|
+
if (options.comments) {
|
|
133
|
+
add('rule', 'comentarios');
|
|
134
|
+
add('hook', 'comentarios');
|
|
135
|
+
}
|
|
136
|
+
// Esqueleto da rule de projeto quando há fluxos ou agents que a leem; um arquivo existente nunca é tocado.
|
|
137
|
+
const readsProjectRule = options.agents || selected.some((cmd) => cmd.skill);
|
|
138
|
+
if (readsProjectRule && !existsSync(join(root, PROJECT_RULE_PATH))) {
|
|
139
|
+
plan.files.push({ path: PROJECT_RULE_PATH, content: Buffer.from(projectRuleContent([])) });
|
|
140
|
+
}
|
|
141
|
+
const target = catalog.claudeMd.target;
|
|
142
|
+
const claudeExists = existsSync(join(root, target));
|
|
143
|
+
const writeClaude = options.claudeMd === 'overwrite' || options.claudeMd === 'backup' || (options.claudeMd === 'create' && !claudeExists);
|
|
144
|
+
if (writeClaude) {
|
|
145
|
+
plan.files.push({ path: target, content: readFileSync(join(templatesDir, catalog.claudeMd.file)) });
|
|
146
|
+
plan.items.push({ tipo: 'claude-md', id: target, paths: [target] });
|
|
147
|
+
plan.claudeMdBackup = options.claudeMd === 'backup' && claudeExists;
|
|
148
|
+
}
|
|
149
|
+
return plan;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Aplica o plano de forma atômica e regrava o lock. Itens de instalações
|
|
153
|
+
* anteriores continuam no lock; arquivos regravados substituem os hashes antigos.
|
|
154
|
+
*/
|
|
155
|
+
export function executePlan(root, plan, pkg, now = new Date()) {
|
|
156
|
+
const previous = readLock(root);
|
|
157
|
+
const claudeItem = plan.items.find((item) => item.tipo === 'claude-md');
|
|
158
|
+
if (plan.claudeMdBackup && claudeItem) {
|
|
159
|
+
copyFileSync(join(root, claudeItem.id), join(root, `${claudeItem.id}.bak`));
|
|
160
|
+
}
|
|
161
|
+
applyAtomically(root, plan.files);
|
|
162
|
+
const permission = needsEventPermission(plan);
|
|
163
|
+
const hooks = combineHooks(plan.registerHooks ? projectHooksConfig() : {}, plan.registerComments ? commentsHooksConfig() : {});
|
|
164
|
+
if (Object.keys(hooks).length || permission)
|
|
165
|
+
mergeSettings(root, { hooks, allow: permission ? [EVENT_PERMISSION] : [] }, now);
|
|
166
|
+
if (plan.registerComments)
|
|
167
|
+
ensureCommentsBaseline(root);
|
|
168
|
+
const items = new Map();
|
|
169
|
+
for (const item of previous?.itens ?? [])
|
|
170
|
+
items.set(`${item.tipo}:${item.id}`, item);
|
|
171
|
+
for (const planned of plan.items) {
|
|
172
|
+
const key = `${planned.tipo}:${planned.id}`;
|
|
173
|
+
const fresh = buildLockItem(root, planned.tipo, planned.id, planned.paths);
|
|
174
|
+
const files = new Map((items.get(key)?.arquivos ?? []).map((entry) => [entry.caminho, entry]));
|
|
175
|
+
for (const entry of fresh.arquivos)
|
|
176
|
+
files.set(entry.caminho, entry);
|
|
177
|
+
items.set(key, { ...fresh, arquivos: [...files.values()].sort((a, b) => a.caminho.localeCompare(b.caminho)) });
|
|
178
|
+
}
|
|
179
|
+
const lock = {
|
|
180
|
+
lock_versao: LOCK_VERSION,
|
|
181
|
+
pacote: pkg,
|
|
182
|
+
gerado_em: now.toISOString(),
|
|
183
|
+
itens: [...items.values()].sort((a, b) => `${a.tipo}:${a.id}`.localeCompare(`${b.tipo}:${b.id}`)),
|
|
184
|
+
};
|
|
185
|
+
writeLock(root, lock);
|
|
186
|
+
return lock;
|
|
187
|
+
}
|
|
188
|
+
/** Skills e agents do harness registram o rastro com `npx @fontdata/ai-harness evento`. */
|
|
189
|
+
function needsEventPermission(plan) {
|
|
190
|
+
return plan.items.some((item) => item.tipo === 'skill' || item.tipo === 'agent');
|
|
191
|
+
}
|
|
192
|
+
export function describePlan(plan) {
|
|
193
|
+
const lines = [`Arquivos a gravar (${plan.files.length}):`, ...plan.files.map((file) => ` + ${file.path}`)];
|
|
194
|
+
if (plan.skipped.length) {
|
|
195
|
+
lines.push(`Já existentes, mantidos (${plan.skipped.length}):`, ...plan.skipped.map((path) => ` = ${path}`));
|
|
196
|
+
}
|
|
197
|
+
if (plan.autoAdded.length)
|
|
198
|
+
lines.push(`Adicionados como dependência: ${plan.autoAdded.join(', ')}`);
|
|
199
|
+
if (plan.claudeMdBackup)
|
|
200
|
+
lines.push('O CLAUDE.md atual será copiado para CLAUDE.md.bak.');
|
|
201
|
+
if (plan.registerHooks)
|
|
202
|
+
lines.push('Hooks do harness registrados em .claude/settings.json (sem duplicar os existentes).');
|
|
203
|
+
if (needsEventPermission(plan))
|
|
204
|
+
lines.push(`Permissão ${EVENT_PERMISSION} em .claude/settings.json, para o rastro não pedir aprovação.`);
|
|
205
|
+
if (plan.registerComments) {
|
|
206
|
+
lines.push('Padrão de comentários: rule carregada em toda sessão, hook que bloqueia a violação e catraca do legado (gerada só na primeira instalação).');
|
|
207
|
+
}
|
|
208
|
+
return lines.join('\n');
|
|
209
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { allCommands } from '../catalog.js';
|
|
4
|
+
/**
|
|
5
|
+
* Manifestos de stack: se algum existir na raiz, é um projeto já existente.
|
|
6
|
+
*/
|
|
7
|
+
const PROJECT_MANIFESTS = [
|
|
8
|
+
'package.json', 'pyproject.toml', 'requirements.txt', 'go.mod',
|
|
9
|
+
'Cargo.toml', 'pom.xml', 'build.gradle', 'build.gradle.kts',
|
|
10
|
+
'composer.json', 'Gemfile', 'Makefile', 'CMakeLists.txt',
|
|
11
|
+
'tsconfig.json', 'pubspec.yaml', 'mix.exs',
|
|
12
|
+
];
|
|
13
|
+
/** Pastas de código-fonte que, se tiverem conteúdo, indicam projeto existente. */
|
|
14
|
+
const SOURCE_DIRS = ['src', 'app', 'lib', 'source'];
|
|
15
|
+
/**
|
|
16
|
+
* Detecta se a pasta alvo já é um projeto com código ('existing') ou está
|
|
17
|
+
* essencialmente vazia/recém-criada ('new'). Ignora .git e .claude — a presença
|
|
18
|
+
* deles sozinha não caracteriza um projeto existente.
|
|
19
|
+
*/
|
|
20
|
+
export function detectProjectKind(repoPath) {
|
|
21
|
+
// 1. Manifesto de stack na raiz.
|
|
22
|
+
if (PROJECT_MANIFESTS.some((f) => existsSync(resolve(repoPath, f))))
|
|
23
|
+
return 'existing';
|
|
24
|
+
// 2. Arquivo de solução/projeto .NET na raiz.
|
|
25
|
+
let rootEntries = [];
|
|
26
|
+
try {
|
|
27
|
+
rootEntries = readdirSync(repoPath);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return 'new';
|
|
31
|
+
}
|
|
32
|
+
if (rootEntries.some((f) => /\.(sln|csproj|fsproj|vbproj)$/i.test(f)))
|
|
33
|
+
return 'existing';
|
|
34
|
+
// 3. Pasta de código-fonte com algum conteúdo.
|
|
35
|
+
for (const dir of SOURCE_DIRS) {
|
|
36
|
+
const dirPath = resolve(repoPath, dir);
|
|
37
|
+
try {
|
|
38
|
+
if (statSync(dirPath).isDirectory() && readdirSync(dirPath).length)
|
|
39
|
+
return 'existing';
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// pasta não existe — segue.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return 'new';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Completa a seleção com as dependências: os comandos que o /harness invoca
|
|
49
|
+
* (quando escolhido) e os declarados em `requires` por comando.
|
|
50
|
+
* Retorna a seleção final e os títulos adicionados automaticamente.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveSelection(catalog, selected, wantMaster) {
|
|
53
|
+
let result = selected;
|
|
54
|
+
let autoAdded = [];
|
|
55
|
+
if (wantMaster) {
|
|
56
|
+
const required = catalog.master.requires || [];
|
|
57
|
+
const selectedIds = new Set(result.map((c) => c.id));
|
|
58
|
+
const missing = allCommands(catalog).filter((c) => required.includes(c.id) && !selectedIds.has(c.id));
|
|
59
|
+
if (missing.length) {
|
|
60
|
+
result = [...result, ...missing];
|
|
61
|
+
autoAdded = missing.map((c) => c.title);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const selectedIds = new Set(result.map((c) => c.id));
|
|
65
|
+
const deps = allCommands(catalog).filter((c) => !selectedIds.has(c.id) &&
|
|
66
|
+
result.some((sel) => (sel.requires || []).includes(c.id)));
|
|
67
|
+
if (deps.length) {
|
|
68
|
+
result = [...result, ...deps];
|
|
69
|
+
autoAdded = [...autoAdded, ...deps.map((c) => c.title)];
|
|
70
|
+
}
|
|
71
|
+
return { selected: result, autoAdded };
|
|
72
|
+
}
|
|
73
|
+
/** Skills exigidas pelos comandos selecionados (campo `requiresSkills`). */
|
|
74
|
+
export function neededSkillsFor(catalog, selected) {
|
|
75
|
+
const neededSkillIds = new Set(selected.flatMap((c) => c.requiresSkills || []));
|
|
76
|
+
return (catalog.skills || []).filter((sk) => neededSkillIds.has(sk.id));
|
|
77
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export const SETTINGS_PATH = join('.claude', 'settings.json');
|
|
4
|
+
/** As skills de fluxo e os agents registram o rastro por este comando; sem a regra, cada evento pede aprovação. */
|
|
5
|
+
export const EVENT_PERMISSION = 'Bash(npx @fontdata/ai-harness evento:*)';
|
|
6
|
+
function isPlainObject(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function timestamp(date) {
|
|
10
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
11
|
+
return (`${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-` +
|
|
12
|
+
`${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`);
|
|
13
|
+
}
|
|
14
|
+
/** Junta configurações de hooks, evento por evento, na ordem recebida. */
|
|
15
|
+
export function combineHooks(...configs) {
|
|
16
|
+
const combined = {};
|
|
17
|
+
for (const config of configs) {
|
|
18
|
+
for (const [event, groups] of Object.entries(config))
|
|
19
|
+
combined[event] = [...(combined[event] ?? []), ...groups];
|
|
20
|
+
}
|
|
21
|
+
return combined;
|
|
22
|
+
}
|
|
23
|
+
/** A regra já está em `permissions.allow` do `.claude/settings.json` do projeto? */
|
|
24
|
+
export function hasAllowRule(root, rule) {
|
|
25
|
+
try {
|
|
26
|
+
const settings = JSON.parse(readFileSync(join(root, SETTINGS_PATH), 'utf8'));
|
|
27
|
+
const allow = settings.permissions?.allow;
|
|
28
|
+
return Array.isArray(allow) && allow.includes(rule);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function mergeHooksIntoSettings(root, hooks, now = new Date()) {
|
|
35
|
+
return mergeSettings(root, { hooks }, now);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Acrescenta hooks e regras de `permissions.allow` ao `.claude/settings.json` do projeto
|
|
39
|
+
* sem perder o que já existe. Um hook é considerado presente quando algum grupo do mesmo
|
|
40
|
+
* evento já tem o mesmo `command`, e uma regra quando já está na lista, o que torna a
|
|
41
|
+
* operação idempotente. Tudo é gravado de uma vez, com um único backup datado do arquivo
|
|
42
|
+
* original; JSON inválido é recusado sem tocar em nada.
|
|
43
|
+
*/
|
|
44
|
+
export function mergeSettings(root, additions, now = new Date()) {
|
|
45
|
+
const file = join(root, SETTINGS_PATH);
|
|
46
|
+
const original = existsSync(file) ? readFileSync(file, 'utf8') : null;
|
|
47
|
+
let settings = {};
|
|
48
|
+
if (original !== null) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(original);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
parsed = undefined;
|
|
55
|
+
}
|
|
56
|
+
const invalid = !isPlainObject(parsed) ||
|
|
57
|
+
(parsed.hooks !== undefined && !isPlainObject(parsed.hooks)) ||
|
|
58
|
+
(parsed.permissions !== undefined && !isPlainObject(parsed.permissions)) ||
|
|
59
|
+
(isPlainObject(parsed.permissions) && parsed.permissions.allow !== undefined && !Array.isArray(parsed.permissions.allow));
|
|
60
|
+
if (invalid) {
|
|
61
|
+
throw new Error(`O ${SETTINGS_PATH} está com JSON inválido; corrija-o antes de instalar os hooks.`);
|
|
62
|
+
}
|
|
63
|
+
settings = parsed;
|
|
64
|
+
}
|
|
65
|
+
const next = { ...settings };
|
|
66
|
+
let changed = false;
|
|
67
|
+
const hookEntries = Object.entries(additions.hooks ?? {});
|
|
68
|
+
if (hookEntries.length) {
|
|
69
|
+
const merged = { ...(settings.hooks ?? {}) };
|
|
70
|
+
for (const [event, groups] of hookEntries) {
|
|
71
|
+
const eventGroups = [...(merged[event] ?? [])];
|
|
72
|
+
const known = new Set(eventGroups.flatMap((group) => group.hooks.map((hook) => hook.command)));
|
|
73
|
+
for (const group of groups) {
|
|
74
|
+
const missing = group.hooks.filter((hook) => !known.has(hook.command));
|
|
75
|
+
if (!missing.length)
|
|
76
|
+
continue;
|
|
77
|
+
eventGroups.push({ ...group, hooks: missing });
|
|
78
|
+
for (const hook of missing)
|
|
79
|
+
known.add(hook.command);
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
merged[event] = eventGroups;
|
|
83
|
+
}
|
|
84
|
+
next.hooks = merged;
|
|
85
|
+
}
|
|
86
|
+
const rules = additions.allow ?? [];
|
|
87
|
+
if (rules.length) {
|
|
88
|
+
const permissions = { ...(settings.permissions ?? {}) };
|
|
89
|
+
const allow = [...(permissions.allow ?? [])];
|
|
90
|
+
for (const rule of rules) {
|
|
91
|
+
if (allow.includes(rule))
|
|
92
|
+
continue;
|
|
93
|
+
allow.push(rule);
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
next.permissions = { ...permissions, allow };
|
|
97
|
+
}
|
|
98
|
+
if (!changed)
|
|
99
|
+
return { changed: false, backup: null };
|
|
100
|
+
let backup = null;
|
|
101
|
+
if (original !== null) {
|
|
102
|
+
backup = join('.claude', `settings.json.bak-${timestamp(now)}`);
|
|
103
|
+
copyFileSync(file, join(root, backup));
|
|
104
|
+
}
|
|
105
|
+
mkdirSync(join(root, '.claude'), { recursive: true });
|
|
106
|
+
writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
|
|
107
|
+
return { changed: true, backup };
|
|
108
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { EOL } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { ARTIFACT_TYPES, readArtifact, WORKS_DIR } from '../contratos/artefatos.js';
|
|
5
|
+
import { suggestFlag } from '../install/flags.js';
|
|
6
|
+
export const INDEX_PATH = join('.ai-harness', 'memoria', 'indice.json');
|
|
7
|
+
export const INDEX_VERSION = 1;
|
|
8
|
+
const BACKSLASH = String.fromCharCode(92);
|
|
9
|
+
function normalizePath(path) {
|
|
10
|
+
return path.trim().split(BACKSLASH).join('/').replace(/^(\.\/)+/, '');
|
|
11
|
+
}
|
|
12
|
+
const text = (value) => (typeof value === 'string' ? value : null);
|
|
13
|
+
const earliest = (a, b) => (!a ? b : !b ? a : a < b ? a : b);
|
|
14
|
+
const latest = (a, b) => (!a ? b : !b ? a : a > b ? a : b);
|
|
15
|
+
/** Data do último `trabalho_concluido` no rastro do trabalho, incluindo arquivos rotacionados. */
|
|
16
|
+
function conclusionDate(root, workId) {
|
|
17
|
+
const dir = join(root, 'docs', 'eventos');
|
|
18
|
+
if (!existsSync(dir))
|
|
19
|
+
return null;
|
|
20
|
+
let date = null;
|
|
21
|
+
const logs = readdirSync(dir).filter((name) => name === `${workId}.jsonl` || (name.startsWith(`${workId}.`) && /^\d+\.jsonl$/.test(name.slice(workId.length + 1))));
|
|
22
|
+
for (const log of logs) {
|
|
23
|
+
for (const line of readFileSync(join(dir, log), 'utf8').split('\n')) {
|
|
24
|
+
try {
|
|
25
|
+
const event = JSON.parse(line);
|
|
26
|
+
if (event.evento === 'trabalho_concluido' && typeof event.ts === 'string')
|
|
27
|
+
date = latest(date, event.ts.slice(0, 10));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// linha vazia ou incompleta.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return date;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Índice "quem mexeu neste arquivo e por quê", gerado do frontmatter dos trabalhos e do rastro.
|
|
38
|
+
* Não é busca semântica: toda resposta aponta um trabalho que pode ser conferido.
|
|
39
|
+
*/
|
|
40
|
+
export function buildIndex(root, now = new Date()) {
|
|
41
|
+
const index = { indice_versao: INDEX_VERSION, gerado_em: now.toISOString(), trabalhos: {}, arquivos: {}, problemas: [] };
|
|
42
|
+
const worksDir = join(root, WORKS_DIR);
|
|
43
|
+
if (!existsSync(worksDir))
|
|
44
|
+
return index;
|
|
45
|
+
const workIds = readdirSync(worksDir)
|
|
46
|
+
.filter((name) => statSync(join(worksDir, name)).isDirectory())
|
|
47
|
+
.sort();
|
|
48
|
+
for (const id of workIds) {
|
|
49
|
+
const summary = { trabalho_tipo: null, titulo: null, status: null, data: null, causa_raiz: null, criado_em: null, arquivos: [] };
|
|
50
|
+
const files = new Set();
|
|
51
|
+
let updatedAt = null;
|
|
52
|
+
let fallbackStatus = null;
|
|
53
|
+
let readable = false;
|
|
54
|
+
for (const rel of Object.keys(ARTIFACT_TYPES)) {
|
|
55
|
+
const path = join(worksDir, id, rel);
|
|
56
|
+
if (!existsSync(path))
|
|
57
|
+
continue;
|
|
58
|
+
const { data, problems } = readArtifact(`docs/trabalhos/${id}/${rel}`, readFileSync(path, 'utf8'));
|
|
59
|
+
if (!data) {
|
|
60
|
+
index.problemas.push(...problems.map(({ arquivo, mensagem }) => ({ arquivo, mensagem })));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
readable = true;
|
|
64
|
+
summary.titulo ??= text(data.titulo);
|
|
65
|
+
summary.trabalho_tipo ??= text(data.trabalho_tipo);
|
|
66
|
+
if (rel === 'PLAN.md') {
|
|
67
|
+
summary.status = text(data.status);
|
|
68
|
+
summary.causa_raiz = text(data.causa_raiz);
|
|
69
|
+
}
|
|
70
|
+
fallbackStatus ??= text(data.status);
|
|
71
|
+
summary.criado_em = earliest(summary.criado_em, text(data.criado_em));
|
|
72
|
+
updatedAt = latest(updatedAt, text(data.atualizado_em));
|
|
73
|
+
if (Array.isArray(data.arquivos)) {
|
|
74
|
+
for (const file of data.arquivos)
|
|
75
|
+
if (typeof file === 'string')
|
|
76
|
+
files.add(normalizePath(file));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!readable)
|
|
80
|
+
continue;
|
|
81
|
+
summary.status ??= fallbackStatus;
|
|
82
|
+
summary.data = conclusionDate(root, id) ?? updatedAt ?? summary.criado_em;
|
|
83
|
+
summary.arquivos = [...files].sort();
|
|
84
|
+
index.trabalhos[id] = summary;
|
|
85
|
+
for (const file of summary.arquivos) {
|
|
86
|
+
const { trabalho_tipo, titulo, status, data, causa_raiz } = summary;
|
|
87
|
+
(index.arquivos[file] ??= []).push({ trabalho: id, trabalho_tipo, titulo, status, data, causa_raiz });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const entries of Object.values(index.arquivos)) {
|
|
91
|
+
entries.sort((a, b) => (a.data ?? '').localeCompare(b.data ?? '') || a.trabalho.localeCompare(b.trabalho));
|
|
92
|
+
}
|
|
93
|
+
index.arquivos = Object.fromEntries(Object.entries(index.arquivos).sort(([a], [b]) => a.localeCompare(b)));
|
|
94
|
+
return index;
|
|
95
|
+
}
|
|
96
|
+
export function writeIndex(root, index) {
|
|
97
|
+
const file = join(root, INDEX_PATH);
|
|
98
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
99
|
+
writeFileSync(file, `${JSON.stringify(index, null, 2)}\n`);
|
|
100
|
+
}
|
|
101
|
+
export function lookupFile(index, path) {
|
|
102
|
+
return index.arquivos[normalizePath(path)] ?? [];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Trabalhos que tocaram os arquivos da causa raiz de um bug. Só é candidata a regressão quando valem
|
|
106
|
+
* as três condições: o arquivo está na causa raiz, o trabalho terminou antes de o bug aparecer e a
|
|
107
|
+
* causa raiz foi comprovada.
|
|
108
|
+
*/
|
|
109
|
+
export function findRegressionCandidates(index, bugId) {
|
|
110
|
+
const bug = index.trabalhos[bugId];
|
|
111
|
+
if (!bug)
|
|
112
|
+
throw new Error(`Trabalho não encontrado no índice da memória: ${bugId}`);
|
|
113
|
+
const shared = new Map();
|
|
114
|
+
for (const file of bug.arquivos) {
|
|
115
|
+
for (const entry of index.arquivos[file] ?? []) {
|
|
116
|
+
if (entry.trabalho === bugId)
|
|
117
|
+
continue;
|
|
118
|
+
shared.set(entry.trabalho, [...(shared.get(entry.trabalho) ?? []), file]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const proven = bug.causa_raiz === 'comprovada';
|
|
122
|
+
return [...shared]
|
|
123
|
+
.map(([id, arquivos]) => {
|
|
124
|
+
const work = index.trabalhos[id];
|
|
125
|
+
const before = Boolean(work?.data && bug.criado_em && work.data < bug.criado_em);
|
|
126
|
+
const motivo = !proven
|
|
127
|
+
? 'causa raiz do bug ainda não comprovada'
|
|
128
|
+
: !before
|
|
129
|
+
? 'o trabalho terminou depois de o bug aparecer'
|
|
130
|
+
: 'tocou arquivo da causa raiz comprovada antes de o bug aparecer';
|
|
131
|
+
return { trabalho: id, titulo: work?.titulo ?? null, data: work?.data ?? null, arquivos: arquivos.sort(), candidata: proven && before, motivo };
|
|
132
|
+
})
|
|
133
|
+
.sort((a, b) => (a.data ?? '').localeCompare(b.data ?? '') || a.trabalho.localeCompare(b.trabalho));
|
|
134
|
+
}
|
|
135
|
+
const FLAGS = ['--arquivo', '--regressao'];
|
|
136
|
+
/** `ai-harness memoria [caminho] [--arquivo <caminho>] [--regressao <id>]` */
|
|
137
|
+
export function runMemoryCommand(argv) {
|
|
138
|
+
let path;
|
|
139
|
+
let file;
|
|
140
|
+
let bugId;
|
|
141
|
+
for (let i = 0; i < argv.length; i++) {
|
|
142
|
+
const token = argv[i] ?? '';
|
|
143
|
+
if (!token.startsWith('-')) {
|
|
144
|
+
if (path !== undefined)
|
|
145
|
+
throw new Error(`Argumento inesperado: ${token}`);
|
|
146
|
+
path = token;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!FLAGS.includes(token)) {
|
|
150
|
+
const suggestion = suggestFlag(token, FLAGS);
|
|
151
|
+
throw new Error(`Flag desconhecida: ${token}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
|
|
152
|
+
}
|
|
153
|
+
const value = argv[i + 1];
|
|
154
|
+
if (value === undefined)
|
|
155
|
+
throw new Error(`${token} precisa de um valor.`);
|
|
156
|
+
i++;
|
|
157
|
+
if (token === '--arquivo')
|
|
158
|
+
file = value;
|
|
159
|
+
else
|
|
160
|
+
bugId = value;
|
|
161
|
+
}
|
|
162
|
+
const root = resolve(process.cwd(), path ?? '.');
|
|
163
|
+
if (!existsSync(root))
|
|
164
|
+
throw new Error(`Pasta não encontrada: ${root}`);
|
|
165
|
+
const index = buildIndex(root);
|
|
166
|
+
writeIndex(root, index);
|
|
167
|
+
if (file) {
|
|
168
|
+
const entries = lookupFile(index, file);
|
|
169
|
+
console.log(entries.length
|
|
170
|
+
? [
|
|
171
|
+
`Trabalhos que tocaram ${normalizePath(file)}, do mais antigo ao mais recente:`,
|
|
172
|
+
...entries.map((entry) => ` ${entry.data ?? 'sem data'} ${entry.trabalho} [${entry.trabalho_tipo ?? '?'}] ${entry.titulo ?? ''} (${entry.status ?? 'sem status'}${entry.causa_raiz ? `, causa raiz ${entry.causa_raiz}` : ''})`),
|
|
173
|
+
].join(EOL)
|
|
174
|
+
: `Nenhum trabalho registrado tocou ${normalizePath(file)}.`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
if (bugId) {
|
|
178
|
+
const suspects = findRegressionCandidates(index, bugId);
|
|
179
|
+
console.log(suspects.length
|
|
180
|
+
? [
|
|
181
|
+
`Trabalhos que tocaram os arquivos de ${bugId}:`,
|
|
182
|
+
...suspects.map((suspect) => ` ${suspect.candidata ? 'candidata ' : 'descartada'} ${suspect.trabalho} — ${suspect.titulo ?? ''} (${suspect.data ?? 'sem data'}): ${suspect.motivo}; arquivos: ${suspect.arquivos.join(', ')}`),
|
|
183
|
+
].join(EOL)
|
|
184
|
+
: `Nenhum outro trabalho tocou os arquivos de ${bugId}.`);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
const works = Object.keys(index.trabalhos).length;
|
|
188
|
+
const files = Object.keys(index.arquivos).length;
|
|
189
|
+
const lines = [`Índice da memória atualizado em ${INDEX_PATH}: ${works} trabalhos, ${files} arquivos.`];
|
|
190
|
+
if (index.problemas.length) {
|
|
191
|
+
lines.push('Artefatos ignorados por estarem fora do contrato:', ...index.problemas.map((p) => ` ! ${p.arquivo}: ${p.mensagem}`));
|
|
192
|
+
}
|
|
193
|
+
console.log(lines.join(EOL));
|
|
194
|
+
return 0;
|
|
195
|
+
}
|