@fontdata/ai-harness 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +9 -2
  3. package/LICENSE +21 -0
  4. package/README.md +175 -140
  5. package/{src → dist}/catalog.js +5 -12
  6. package/dist/cli.js +180 -0
  7. package/dist/comentarios/command.js +74 -0
  8. package/dist/comentarios/instalar.js +33 -0
  9. package/dist/config/config.js +99 -0
  10. package/dist/contratos/artefatos.js +284 -0
  11. package/dist/contratos/eventos.js +103 -0
  12. package/dist/doctor/checks.js +424 -0
  13. package/dist/init.js +240 -0
  14. package/dist/install/atomic.js +51 -0
  15. package/dist/install/flags.js +99 -0
  16. package/dist/install/hooks.js +47 -0
  17. package/dist/install/lock.js +39 -0
  18. package/dist/install/plan.js +209 -0
  19. package/dist/install/selection.js +77 -0
  20. package/dist/install/settings.js +112 -0
  21. package/dist/memoria/indice.js +195 -0
  22. package/dist/migrar/migrar.js +426 -0
  23. package/dist/rastro/command.js +46 -0
  24. package/dist/run.js +86 -0
  25. package/dist/trabalhos/conflitos.js +115 -0
  26. package/dist/types.js +1 -0
  27. package/dist/update/legado.js +74 -0
  28. package/dist/update/update.js +284 -0
  29. package/package.json +17 -8
  30. package/templates/CLAUDE.md +59 -30
  31. package/templates/agents/auditor-plano.md +44 -0
  32. package/templates/agents/investigador.md +32 -0
  33. package/templates/agents/qa.md +44 -0
  34. package/templates/agents/revisor-testes.md +40 -0
  35. package/templates/catalog.json +24 -61
  36. package/templates/commands/atualizar-deps.md +43 -9
  37. package/templates/commands/diagrama.md +35 -10
  38. package/templates/commands/gerar-env.md +39 -10
  39. package/templates/commands/gerar-testes.md +42 -10
  40. package/templates/commands/git-commit-push.md +0 -0
  41. package/templates/commands/onboarding.md +44 -15
  42. package/templates/commands/refatorar.md +50 -10
  43. package/templates/commands/revisar-pr.md +56 -12
  44. package/templates/commands/sincronizar-claude-md.md +177 -9
  45. package/templates/commands/versao.md +0 -0
  46. package/templates/hooks/comentarios/check.mjs +210 -0
  47. package/templates/hooks/hooks.json +34 -0
  48. package/templates/hooks/lembrete.mjs +29 -0
  49. package/templates/hooks/lib/config.mjs +40 -0
  50. package/templates/hooks/lib/hook.mjs +85 -0
  51. package/templates/hooks/lib/rastro.mjs +144 -0
  52. package/templates/hooks/lib/segredos.mjs +91 -0
  53. package/templates/hooks/lib/temas.mjs +111 -0
  54. package/templates/hooks/lib/trabalho.mjs +94 -0
  55. package/templates/hooks/proteger-contratos.mjs +80 -0
  56. package/templates/hooks/segredo-no-commit.mjs +76 -0
  57. package/templates/hooks/sem-jargao.mjs +58 -0
  58. package/templates/hooks/task-so-fecha-verde.mjs +33 -0
  59. package/templates/legado/manifesto.json +1267 -0
  60. package/templates/rules/comentarios.md +166 -0
  61. package/templates/rules/harness-projeto.md +41 -0
  62. package/templates/skills/auditar-performance/SKILL.md +53 -0
  63. package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
  64. package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
  65. package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
  66. package/templates/skills/auditar-seguranca/SKILL.md +67 -0
  67. package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
  68. package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
  69. package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
  70. package/templates/skills/auditar-ui/SKILL.md +64 -0
  71. package/templates/skills/auditar-ui/references/lentes.md +33 -0
  72. package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
  73. package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
  74. package/templates/skills/evoluir-harness/SKILL.md +81 -0
  75. package/templates/skills/evoluir-harness/references/agent.md +83 -0
  76. package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
  77. package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
  78. package/templates/skills/evoluir-harness/references/design-system.md +56 -0
  79. package/templates/skills/evoluir-harness/references/hook.md +95 -0
  80. package/templates/skills/evoluir-harness/references/rule.md +67 -0
  81. package/templates/skills/evoluir-harness/references/skill.md +63 -0
  82. package/templates/skills/executar-plan/SKILL.md +130 -0
  83. package/templates/skills/executar-plan/references/delegacao.md +43 -0
  84. package/templates/skills/executar-plan/references/retomada.md +33 -0
  85. package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
  86. package/templates/skills/harness/SKILL.md +75 -0
  87. package/templates/skills/harness/references/auto-tag.md +118 -0
  88. package/templates/skills/harness/references/ignores.md +40 -0
  89. package/templates/skills/harness/references/scaffold.md +29 -0
  90. package/templates/skills/harness/references/validar-ideia.md +42 -0
  91. package/templates/skills/memoria/SKILL.md +52 -0
  92. package/templates/skills/nova-tarefa/SKILL.md +99 -0
  93. package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
  94. package/templates/skills/nova-tarefa/references/regras-do-plano.md +35 -0
  95. package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
  96. package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
  97. package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
  98. package/templates/skills/resolver-bug/SKILL.md +104 -0
  99. package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
  100. package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
  101. package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
  102. package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
  103. package/bin/cli.js +0 -122
  104. package/src/init.js +0 -317
  105. package/src/installer.js +0 -92
  106. package/src/run.js +0 -99
  107. package/templates/commands/auditar-performance.md +0 -27
  108. package/templates/commands/auditar-seguranca.md +0 -64
  109. package/templates/commands/auditar-ui.md +0 -38
  110. package/templates/commands/codigo-morto.md +0 -22
  111. package/templates/commands/configurar-auto-tag.md +0 -185
  112. package/templates/commands/configurar-comentarios.md +0 -543
  113. package/templates/commands/configurar-orquestrador.md +0 -13
  114. package/templates/commands/corrigir-review.md +0 -11
  115. package/templates/commands/criar-agents.md +0 -42
  116. package/templates/commands/criar-code-reviewer.md +0 -30
  117. package/templates/commands/criar-hooks.md +0 -32
  118. package/templates/commands/criar-rag.md +0 -25
  119. package/templates/commands/criar-rules.md +0 -31
  120. package/templates/commands/criar-skill.md +0 -22
  121. package/templates/commands/design-system.md +0 -27
  122. package/templates/commands/executar-plan.md +0 -36
  123. package/templates/commands/gerar-claude-md.md +0 -66
  124. package/templates/commands/gerar-ignore.md +0 -37
  125. package/templates/commands/gerar-spec.md +0 -33
  126. package/templates/commands/harness.md +0 -34
  127. package/templates/commands/nova-tarefa.md +0 -60
  128. package/templates/commands/resolver-bug.md +0 -25
  129. package/templates/commands/review-por-task.md +0 -16
  130. package/templates/commands/scaffold.md +0 -25
  131. package/templates/commands/validar-commands.md +0 -24
  132. package/templates/commands/validar-ideia.md +0 -36
@@ -0,0 +1,426 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, posix, resolve, sep } from 'node:path';
4
+ import { parse, stringify } from 'yaml';
5
+ import { SCHEMA_VERSION, WORK_ID } from '../contratos/artefatos.js';
6
+ import { suggestFlag } from '../install/flags.js';
7
+ /** Layout antigo (até a 2.0) → artefato do contrato de pastas por trabalho. */
8
+ const SOURCES = {
9
+ specs: { tipo: 'spec', target: 'SPEC.md' },
10
+ plans: { tipo: 'plano', target: 'PLAN.md' },
11
+ bugs: { tipo: 'plano', target: 'PLAN.md' },
12
+ investigacoes: { tipo: 'relatorio_tecnico', target: 'relatorios/TECNICO.md' },
13
+ };
14
+ const SOURCE_DIRS = Object.keys(SOURCES);
15
+ const IGNORED_NAME = /^(readme|index)\.md$/i;
16
+ /** Sufixos de documentos que acompanham outro de mesmo nome base. */
17
+ const SATELLITE_SUFFIXES = ['-plano', '-design', '-contrato-api', '-validacao'];
18
+ const LEGACY_PATH = /\b[Dd]ocs\/(specs|plans|bugs|investigacoes)\b/;
19
+ const OPEN_BOX = /^\s*[-*]\s+\[ \]/m;
20
+ function slugOf(fileName) {
21
+ const slug = fileName
22
+ .replace(/\.md$/i, '')
23
+ .normalize('NFD')
24
+ .replace(/\p{Diacritic}/gu, '')
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, '-')
27
+ .replace(/^-+|-+$/g, '');
28
+ return slug || 'documento';
29
+ }
30
+ function isGitRepo(root) {
31
+ return existsSync(join(root, '.git'));
32
+ }
33
+ function isTracked(root, path) {
34
+ try {
35
+ execFileSync('git', ['ls-files', '--error-unmatch', path], { cwd: root, stdio: 'ignore' });
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ /** `docs` quando existe com esse nome; senão a pasta com outra caixa (como `Docs`), se houver. */
43
+ function realDocsDir(root) {
44
+ const entries = readdirSync(root);
45
+ if (entries.includes('docs'))
46
+ return 'docs';
47
+ return entries.find((name) => name.toLowerCase() === 'docs' && statSync(join(root, name)).isDirectory()) ?? null;
48
+ }
49
+ /** Data do commit que criou o arquivo; fora do git, a data de modificação. */
50
+ function creationDate(root, realPath, git) {
51
+ if (git) {
52
+ try {
53
+ const dates = execFileSync('git', ['log', '--diff-filter=A', '--follow', '--format=%cs', '--', realPath], {
54
+ cwd: root,
55
+ encoding: 'utf8',
56
+ stdio: ['ignore', 'pipe', 'ignore'],
57
+ })
58
+ .split('\n')
59
+ .filter(Boolean);
60
+ const first = dates.at(-1);
61
+ if (first)
62
+ return first;
63
+ }
64
+ catch {
65
+ // Fora do índice: vale a data do arquivo.
66
+ }
67
+ }
68
+ return statSync(join(root, realPath)).mtime.toISOString().slice(0, 10);
69
+ }
70
+ function markdownFiles(dir) {
71
+ if (!existsSync(dir))
72
+ return [];
73
+ return readdirSync(dir, { recursive: true, encoding: 'utf8' })
74
+ .filter((rel) => rel.endsWith('.md') && statSync(join(dir, rel)).isFile())
75
+ .map((rel) => rel.split(sep).join('/'))
76
+ .sort();
77
+ }
78
+ function legacyReferences(root) {
79
+ const files = [
80
+ 'CLAUDE.md',
81
+ ...['rules', 'agents', 'skills', 'commands'].flatMap((dir) => markdownFiles(join(root, '.claude', dir)).map((rel) => `.claude/${dir}/${rel}`)),
82
+ ];
83
+ return files
84
+ .filter((rel) => existsSync(join(root, rel)))
85
+ .flatMap((rel) => readFileSync(join(root, rel), 'utf8')
86
+ .split('\n')
87
+ .flatMap((line, index) => (LEGACY_PATH.test(line) ? [`${rel}:${index + 1}`] : [])));
88
+ }
89
+ /** Destino de cada documento de um trabalho. Satélite sem lugar próprio vai para `anexos/`, sem frontmatter. */
90
+ function assignTargets(members) {
91
+ const assigned = members.map(({ doc, suffix }) => {
92
+ if (suffix === '-plano')
93
+ return { doc, suffix, target: 'PLAN.md', tipo: 'plano' };
94
+ if (suffix === '-design')
95
+ return { doc, suffix, target: 'SPEC.md', tipo: 'spec' };
96
+ if (suffix)
97
+ return { doc, suffix, target: `anexos/${doc.name}`, tipo: 'anexo' };
98
+ return { doc, suffix, ...SOURCES[doc.dir] };
99
+ });
100
+ for (const entry of assigned.filter((candidate) => candidate.suffix === '-plano' || candidate.suffix === '-design')) {
101
+ const rival = assigned.find((other) => other !== entry && !other.suffix && other.target === entry.target);
102
+ if (!rival)
103
+ continue;
104
+ // Relatório do bug + plano de correção: o relatório vira o relatório técnico do trabalho.
105
+ if (entry.suffix === '-plano' && rival.doc.dir === 'bugs' && !assigned.some((other) => other.target === 'relatorios/TECNICO.md')) {
106
+ rival.target = 'relatorios/TECNICO.md';
107
+ rival.tipo = 'relatorio_tecnico';
108
+ }
109
+ else {
110
+ entry.target = `anexos/${entry.doc.name}`;
111
+ entry.tipo = 'anexo';
112
+ }
113
+ }
114
+ return assigned;
115
+ }
116
+ export function planMigration(root, options = {}) {
117
+ const today = options.today ?? new Date().toISOString().slice(0, 10);
118
+ const docsDir = realDocsDir(root);
119
+ const plan = { moves: [], ignored: [], conflicts: [], docsDir: docsDir ?? 'docs', references: [], today };
120
+ if (!docsDir)
121
+ return plan;
122
+ const git = isGitRepo(root);
123
+ const docs = [];
124
+ for (const dir of SOURCE_DIRS) {
125
+ const abs = join(root, docsDir, dir);
126
+ if (!existsSync(abs) || !statSync(abs).isDirectory())
127
+ continue;
128
+ const names = readdirSync(abs)
129
+ .filter((name) => name.toLowerCase().endsWith('.md') && statSync(join(abs, name)).isFile())
130
+ .sort();
131
+ for (const name of names) {
132
+ const from = `docs/${dir}/${name}`;
133
+ if (IGNORED_NAME.test(name))
134
+ plan.ignored.push(from);
135
+ else
136
+ docs.push({ dir, name, realPath: `${docsDir}/${dir}/${name}`, from, slug: slugOf(name) });
137
+ }
138
+ }
139
+ const slugs = new Set(docs.map((doc) => doc.slug));
140
+ const groups = new Map();
141
+ for (const doc of docs) {
142
+ const suffix = SATELLITE_SUFFIXES.find((candidate) => doc.slug.endsWith(candidate) && slugs.has(doc.slug.slice(0, -candidate.length))) ?? null;
143
+ const key = suffix ? doc.slug.slice(0, -suffix.length) : doc.slug;
144
+ groups.set(key, [...(groups.get(key) ?? []), { doc, suffix }]);
145
+ }
146
+ for (const [key, members] of groups) {
147
+ const firstDate = members.map(({ doc }) => creationDate(root, doc.realPath, git)).sort()[0] ?? today;
148
+ const id = WORK_ID.test(key) ? key : `${firstDate}-${key}`;
149
+ const trabalhoTipo = members.some(({ doc }) => doc.dir === 'bugs')
150
+ ? 'bug'
151
+ : members.some(({ doc }) => doc.dir === 'specs' || doc.dir === 'plans')
152
+ ? 'feature'
153
+ : 'investigacao';
154
+ const byTarget = new Map();
155
+ for (const entry of assignTargets(members))
156
+ byTarget.set(entry.target, [...(byTarget.get(entry.target) ?? []), entry]);
157
+ for (const [target, entries] of byTarget) {
158
+ const to = `docs/trabalhos/${id}/${target}`;
159
+ const [entry] = entries;
160
+ if (!entry || entries.length > 1 || existsSync(join(root, to))) {
161
+ plan.conflicts.push({ destino: to, origens: entries.map((candidate) => candidate.doc.from) });
162
+ continue;
163
+ }
164
+ plan.moves.push({ from: entry.doc.from, to, id, trabalhoTipo, tipo: entry.tipo });
165
+ }
166
+ }
167
+ const order = new Map(docs.map((doc, index) => [doc.from, index]));
168
+ plan.moves.sort((a, b) => (order.get(a.from) ?? 0) - (order.get(b.from) ?? 0));
169
+ if (plan.moves.length)
170
+ plan.references = legacyReferences(root);
171
+ return plan;
172
+ }
173
+ const ROOT_CAUSE_LABEL = /^(?:#{1,6}\s*(?:\d+[.)]\s*)?|\s*[-*]?\s*\*{0,2})causa[\s-]*raiz/im;
174
+ const UNCONFIRMED = /n[ãa]o\s+(?:foi\s+)?(?:confirmad|comprovad)|hip[óo]tese|a\s+confirmar|suspeita/i;
175
+ /** Com seção ou rótulo de causa raiz, a causa é comprovada, a não ser que o trecho diga que ainda é hipótese. */
176
+ function rootCauseOf(body) {
177
+ const label = ROOT_CAUSE_LABEL.exec(body);
178
+ if (!label)
179
+ return 'hipotese';
180
+ return UNCONFIRMED.test(body.slice(label.index, label.index + 600)) ? 'hipotese' : 'comprovada';
181
+ }
182
+ const TASK_HEADING = /^(#{2,6})\s*(?:(?:task|tarefa)\s+|T-?)(\d+)\.(\d+)\b\s*[—–:.-]*\s*(.*)$/i;
183
+ const TASK_ITEM = /^\s*[-*]\s+\[([ xX])\]\s+\*{0,2}(?:(?:task|tarefa)\s+|T-?)(\d+)\.(\d+)\b\s*[—–:.-]*\s*(.*)$/i;
184
+ const ANY_HEADING = /^(#{1,6})\s/;
185
+ const BOX = /^\s*[-*]\s+\[([ xX])\]/;
186
+ const DONE_MARK = /✅|\bconclu[ií]d[ao]\b/i;
187
+ function cleanTitle(raw, sprint, number) {
188
+ const title = raw
189
+ .replace(/[[(][^\])]*conclu[^\])]*[\])]/gi, '')
190
+ .replace(/✅/g, '')
191
+ .replace(/\bconclu[ií]d[ao]\b/gi, '')
192
+ .replace(/\*+/g, '')
193
+ .replace(/[\s—–:.-]+$/, '')
194
+ .trim()
195
+ .slice(0, 120);
196
+ return title || `Task ${sprint}.${number}`;
197
+ }
198
+ /**
199
+ * Tasks do plano antigo nos formatos encontrados nos projetos: `#### Task 1.1 — Título`, `### T1.1 — Título`
200
+ * e `- [x] **T1.1 — Título**`. Concluída quando marcada (✅, "concluída", `[x]`), quando a seção só tem caixas
201
+ * marcadas ou quando o plano inteiro não tem caixa aberta.
202
+ */
203
+ function parseTasks(body) {
204
+ const found = [];
205
+ let current = null;
206
+ for (const line of body.split('\n')) {
207
+ const heading = TASK_HEADING.exec(line);
208
+ if (heading) {
209
+ const titulo = heading[4] ?? '';
210
+ current = { level: (heading[1] ?? '').length, sprint: Number(heading[2]), number: Number(heading[3]), titulo, marked: DONE_MARK.test(titulo), open: 0, closed: 0 };
211
+ found.push(current);
212
+ continue;
213
+ }
214
+ const item = TASK_ITEM.exec(line);
215
+ if (item) {
216
+ const titulo = item[4] ?? '';
217
+ found.push({ level: 7, sprint: Number(item[2]), number: Number(item[3]), titulo, marked: item[1] !== ' ' || DONE_MARK.test(titulo), open: 0, closed: 0 });
218
+ continue;
219
+ }
220
+ const other = ANY_HEADING.exec(line);
221
+ if (other && current && (other[1] ?? '').length <= current.level)
222
+ current = null;
223
+ const box = BOX.exec(line);
224
+ if (box && current) {
225
+ if (box[1] === ' ')
226
+ current.open += 1;
227
+ else
228
+ current.closed += 1;
229
+ }
230
+ }
231
+ const planDone = !OPEN_BOX.test(body);
232
+ const ids = new Set();
233
+ const tasks = [];
234
+ for (const task of found) {
235
+ let number = task.number;
236
+ const idFor = (n) => `T-${String(task.sprint).padStart(2, '0')}.${String(n).padStart(2, '0')}`;
237
+ while (ids.has(idFor(number)))
238
+ number += 1;
239
+ if (task.sprint > 99 || number > 99)
240
+ continue;
241
+ ids.add(idFor(number));
242
+ tasks.push({
243
+ id: idFor(number),
244
+ titulo: cleanTitle(task.titulo, task.sprint, task.number),
245
+ status: task.marked || planDone || (task.open === 0 && task.closed > 0) ? 'concluida' : 'pendente',
246
+ depende_de: [],
247
+ paralelizavel: false,
248
+ testes_criticos: [],
249
+ });
250
+ }
251
+ return tasks;
252
+ }
253
+ function contractFrontmatter(content, move, today) {
254
+ const match = /^\s*---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(content);
255
+ const parsed = match ? parse(match[1] ?? '') : null;
256
+ const existing = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : {};
257
+ const body = match ? content.slice(match[0].length) : content;
258
+ const heading = /^#\s+(.+)$/m.exec(body)?.[1]?.trim();
259
+ const status = move.tipo === 'spec' ? 'aprovado' : move.tipo === 'plano' ? (OPEN_BOX.test(body) ? 'em-execucao' : 'concluido') : 'final';
260
+ const data = {
261
+ schema_versao: SCHEMA_VERSION,
262
+ tipo: move.tipo,
263
+ id: move.id,
264
+ trabalho_tipo: move.trabalhoTipo,
265
+ titulo: heading ?? move.id,
266
+ status,
267
+ criado_em: move.id.slice(0, 10),
268
+ atualizado_em: today,
269
+ arquivos: [],
270
+ migrado: true,
271
+ ...(move.tipo === 'plano'
272
+ ? {
273
+ condensado: false,
274
+ auditoria_plano: 'pendente',
275
+ causa_raiz: move.trabalhoTipo === 'bug' ? rootCauseOf(body) : null,
276
+ tasks: parseTasks(body),
277
+ }
278
+ : {}),
279
+ };
280
+ // O que já existia prevalece, exceto a identidade do artefato, que o contrato define.
281
+ const merged = { ...data, ...existing, schema_versao: SCHEMA_VERSION, tipo: move.tipo, id: move.id, trabalho_tipo: move.trabalhoTipo, migrado: true };
282
+ return `---\n${stringify(merged)}---\n${body.startsWith('\n') ? '' : '\n'}${body}`;
283
+ }
284
+ function safeDecode(path) {
285
+ try {
286
+ return decodeURI(path);
287
+ }
288
+ catch {
289
+ return path;
290
+ }
291
+ }
292
+ /**
293
+ * Links do documento migrado (D-36): o relativo é resolvido a partir do lugar antigo e reescrito a partir do
294
+ * novo, e o caminho de uma pasta antiga passa a apontar para o destino do documento citado.
295
+ */
296
+ function rewriteLinks(content, move, destinations) {
297
+ const fromDir = posix.dirname(move.from);
298
+ const toDir = posix.dirname(move.to);
299
+ const destinationOf = (path) => destinations.get(path.replace(/^Docs\//, 'docs/'));
300
+ const relinked = content.replace(/\]\(([^)\s#]+)(#[^)\s]*)?\)/g, (whole, target, anchor) => {
301
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('/'))
302
+ return whole;
303
+ const decoded = safeDecode(target);
304
+ if (/^[Dd]ocs\//.test(decoded)) {
305
+ const moved = destinationOf(decoded);
306
+ return moved ? `](${moved}${anchor ?? ''})` : whole;
307
+ }
308
+ const resolved = posix.normalize(posix.join(fromDir, decoded));
309
+ const destination = destinationOf(resolved) ?? resolved;
310
+ const relative = posix.relative(toDir, destination) || posix.basename(destination);
311
+ return `](${target.includes('%') ? encodeURI(relative) : relative}${anchor ?? ''})`;
312
+ });
313
+ return relinked.replace(/\b[Dd]ocs\/(?:specs|plans|bugs|investigacoes)\/[^\s)`'"\]<>]+?\.md\b/g, (path) => destinationOf(path) ?? path);
314
+ }
315
+ /** `Docs/` → `docs/` em dois passos: num disco que não distingue caixa, a troca direta não tem efeito. */
316
+ function renameDocsDir(root, from, git) {
317
+ const temp = `${from}-ai-harness-migrar`;
318
+ const move = (source, target) => {
319
+ if (git) {
320
+ try {
321
+ execFileSync('git', ['mv', source, target], { cwd: root, stdio: 'ignore' });
322
+ return;
323
+ }
324
+ catch {
325
+ // Pasta sem arquivo versionado: o git recusa, e a troca vai direto no disco.
326
+ }
327
+ }
328
+ renameSync(join(root, source), join(root, target));
329
+ };
330
+ move(from, temp);
331
+ move(temp, 'docs');
332
+ }
333
+ /** Pasta antiga vazia, ou só com `.gitkeep`, deixa de existir. */
334
+ function removeLegacyDir(root, rel, git) {
335
+ const dir = join(root, rel);
336
+ if (!existsSync(dir))
337
+ return;
338
+ const entries = readdirSync(dir).filter((name) => name !== '.DS_Store');
339
+ if (entries.some((name) => name !== '.gitkeep'))
340
+ return;
341
+ const keep = `${rel}/.gitkeep`;
342
+ if (entries.includes('.gitkeep') && git && isTracked(root, keep))
343
+ execFileSync('git', ['rm', '-q', keep], { cwd: root, stdio: 'ignore' });
344
+ rmSync(dir, { recursive: true, force: true });
345
+ }
346
+ /** Move os documentos e aplica o frontmatter; arquivos versionados vão com `git mv` para manter o histórico. */
347
+ export function applyMigration(root, plan) {
348
+ const git = isGitRepo(root);
349
+ if (plan.moves.length && plan.docsDir !== 'docs')
350
+ renameDocsDir(root, plan.docsDir, git);
351
+ for (const move of plan.moves) {
352
+ const target = join(root, move.to);
353
+ mkdirSync(dirname(target), { recursive: true });
354
+ if (git && isTracked(root, move.from))
355
+ execFileSync('git', ['mv', move.from, move.to], { cwd: root, stdio: 'ignore' });
356
+ else
357
+ renameSync(join(root, move.from), target);
358
+ }
359
+ // Links e frontmatter só depois de mover tudo: cada link precisa do destino final do documento citado.
360
+ const destinations = new Map(plan.moves.map((move) => [move.from, move.to]));
361
+ for (const move of plan.moves) {
362
+ const target = join(root, move.to);
363
+ const content = rewriteLinks(readFileSync(target, 'utf8'), move, destinations);
364
+ writeFileSync(target, move.tipo === 'anexo' ? content : contractFrontmatter(content, move, plan.today));
365
+ }
366
+ for (const dir of SOURCE_DIRS)
367
+ removeLegacyDir(root, `docs/${dir}`, git);
368
+ }
369
+ const MIGRATION_FLAGS = ['--yes', '-y', '--sim'];
370
+ const REFERENCES_SHOWN = 40;
371
+ /** `ai-harness migrar [caminho] [--yes]`; sem `--yes` só simula. */
372
+ export function runMigrationCommand(argv) {
373
+ let path;
374
+ let yes = false;
375
+ for (const token of argv) {
376
+ if (!token.startsWith('-')) {
377
+ if (path !== undefined)
378
+ throw new Error(`Argumento inesperado: ${token}`);
379
+ path = token;
380
+ }
381
+ else if (MIGRATION_FLAGS.includes(token)) {
382
+ yes = true;
383
+ }
384
+ else {
385
+ const suggestion = suggestFlag(token, MIGRATION_FLAGS);
386
+ throw new Error(`Flag desconhecida: ${token}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
387
+ }
388
+ }
389
+ const root = resolve(process.cwd(), path ?? '.');
390
+ if (!existsSync(root))
391
+ throw new Error(`Pasta não encontrada: ${root}`);
392
+ const plan = planMigration(root);
393
+ if (!plan.moves.length && !plan.conflicts.length) {
394
+ const ignored = plan.ignored.length ? `\nIgnorados: ${plan.ignored.join(', ')}` : '';
395
+ console.log(`Nada a migrar: o projeto não tem documentos em docs/specs, docs/plans, docs/bugs nem docs/investigacoes.${ignored}`);
396
+ return 0;
397
+ }
398
+ const lines = [`Documentos a migrar (${plan.moves.length}):`, ...plan.moves.map((move) => ` ${move.from} → ${move.to}`)];
399
+ if (plan.docsDir !== 'docs') {
400
+ lines.push('', `A pasta ${plan.docsDir}/ será renomeada para docs/ antes de migrar, com git mv quando versionada.`);
401
+ }
402
+ if (plan.ignored.length) {
403
+ lines.push('', `Ignorados, ficam onde estão (${plan.ignored.length}):`, ...plan.ignored.map((ignored) => ` - ${ignored}`));
404
+ }
405
+ if (plan.conflicts.length) {
406
+ lines.push('', `Conflitos, ficam onde estão e o restante migra (${plan.conflicts.length}):`, ...plan.conflicts.map((conflict) => ` ! ${conflict.destino} ← ${conflict.origens.join(', ')}`));
407
+ }
408
+ if (plan.references.length) {
409
+ lines.push('', `Citações às pastas antigas no CLAUDE.md e em .claude/, para revisar à mão (${plan.references.length}):`, ...plan.references.slice(0, REFERENCES_SHOWN).map((reference) => ` ${reference}`));
410
+ if (plan.references.length > REFERENCES_SHOWN)
411
+ lines.push(` … e mais ${plan.references.length - REFERENCES_SHOWN}`);
412
+ }
413
+ console.log(lines.join('\n'));
414
+ if (!plan.moves.length) {
415
+ console.error('\nNenhum documento pode ser migrado: resolva os conflitos acima.');
416
+ return 1;
417
+ }
418
+ if (!yes) {
419
+ console.log('\nSimulação — nada foi movido. Rode de novo com --yes para migrar.');
420
+ return 0;
421
+ }
422
+ applyMigration(root, plan);
423
+ console.log(`\nMigração concluída: ${plan.moves.length} documento(s) em docs/trabalhos/. ` +
424
+ 'Rode `npx @fontdata/ai-harness doctor` para conferir o contrato.');
425
+ return 0;
426
+ }
@@ -0,0 +1,46 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { templatesDir } from '../catalog.js';
4
+ import { suggestFlag } from '../install/flags.js';
5
+ /** O CLI grava pela mesma biblioteca dos hooks, para as duas formas produzirem o mesmo formato. */
6
+ export async function loadRastroLib() {
7
+ return (await import(pathToFileURL(join(templatesDir, 'hooks', 'lib', 'rastro.mjs')).href));
8
+ }
9
+ const FLAGS = ['--trabalho', '--task', '--nome', '--resultado', '--dados'];
10
+ /** `ai-harness evento <evento> [--trabalho id] [--task T-SS.NN] [--nome n] [--resultado r] [--dados json]` */
11
+ export async function runEventCommand(argv) {
12
+ const [evento, ...rest] = argv;
13
+ if (!evento || evento.startsWith('-')) {
14
+ throw new Error('Informe o evento. Ex.: npx @fontdata/ai-harness evento suite_verde --task T-01.02');
15
+ }
16
+ const fields = { evento, origem: 'cli', nome: 'ai-harness' };
17
+ for (let i = 0; i < rest.length; i++) {
18
+ const flag = rest[i] ?? '';
19
+ if (!FLAGS.includes(flag)) {
20
+ const suggestion = suggestFlag(flag, FLAGS);
21
+ throw new Error(`Flag desconhecida: ${flag}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
22
+ }
23
+ const value = rest[i + 1];
24
+ if (value === undefined)
25
+ throw new Error(`${flag} precisa de um valor.`);
26
+ i++;
27
+ if (flag === '--trabalho')
28
+ fields.trabalhoId = value;
29
+ else if (flag === '--task')
30
+ fields.taskId = value;
31
+ else if (flag === '--nome')
32
+ fields.nome = value;
33
+ else if (flag === '--resultado')
34
+ fields.resultado = value;
35
+ else {
36
+ try {
37
+ fields.dados = JSON.parse(value);
38
+ }
39
+ catch {
40
+ throw new Error('--dados precisa ser um JSON válido.');
41
+ }
42
+ }
43
+ }
44
+ (await loadRastroLib()).recordEvent(process.cwd(), fields);
45
+ return 0;
46
+ }
package/dist/run.js ADDED
@@ -0,0 +1,86 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { allCommands, loadCatalog } from './catalog.js';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ /** Raiz do pacote — é ela que vira o plugin carregado via `claude --plugin-dir`. */
8
+ export const pluginRoot = join(__dirname, '..');
9
+ /** Nome do plugin, lido do manifesto para não duplicar a string. */
10
+ export function pluginName() {
11
+ const manifest = JSON.parse(readFileSync(join(pluginRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
12
+ return manifest.name;
13
+ }
14
+ /**
15
+ * Ids executáveis: os comandos dos grupos mais o comando-mestre, que fica fora
16
+ * de `groups` no catálogo.
17
+ */
18
+ export function runnableIds(catalog = loadCatalog()) {
19
+ const ids = allCommands(catalog).map((cmd) => cmd.id);
20
+ if (catalog.master?.id)
21
+ ids.unshift(catalog.master.id);
22
+ return [...new Set(ids)];
23
+ }
24
+ /** Ids mais parecidos com o que foi digitado, para sugerir em caso de erro. */
25
+ export function suggestIds(input, ids) {
26
+ const needle = input.toLowerCase();
27
+ return ids
28
+ .filter((id) => id.includes(needle) || needle.includes(id) || id.startsWith(needle.slice(0, 4)))
29
+ .slice(0, 5);
30
+ }
31
+ const SYSTEM_NOTE = 'Os slash commands do harness Fontdata estão disponíveis nesta sessão com o prefixo ' +
32
+ '`/PLUGIN:` (ex.: `/PLUGIN:nova-tarefa`). Quando uma instrução referenciar um comando ' +
33
+ 'como `/nome`, entenda que se trata de `/PLUGIN:nome`.';
34
+ /**
35
+ * Executa um comando do catálogo abrindo o Claude Code na pasta atual, com o
36
+ * pacote carregado como plugin da sessão — sem instalar nada no projeto.
37
+ */
38
+ export function runCommand(id, args = [], claudeFlags = []) {
39
+ const catalog = loadCatalog();
40
+ const ids = runnableIds(catalog);
41
+ const wanted = id.replace(/^\//, '');
42
+ if (!ids.includes(wanted)) {
43
+ const near = suggestIds(wanted, ids);
44
+ console.error(`Comando desconhecido: ${wanted}`);
45
+ if (near.length)
46
+ console.error(`\nVocê quis dizer: ${near.join(', ')}`);
47
+ console.error('\nRode `npx @fontdata/ai-harness list` para ver o catálogo completo.');
48
+ return 1;
49
+ }
50
+ const name = pluginName();
51
+ const prompt = [`/${name}:${wanted}`, ...args].join(' ').trim();
52
+ const argv = [
53
+ '--plugin-dir',
54
+ pluginRoot,
55
+ '--append-system-prompt',
56
+ SYSTEM_NOTE.replaceAll('PLUGIN', name),
57
+ ...claudeFlags,
58
+ '--',
59
+ prompt,
60
+ ];
61
+ const result = spawn(argv);
62
+ const error = result.error;
63
+ if (error?.code === 'ENOENT') {
64
+ console.error('Claude Code não encontrado no PATH.\n' +
65
+ 'Instale com: npm install -g @anthropic-ai/claude-code');
66
+ return 127;
67
+ }
68
+ if (error) {
69
+ console.error('Erro ao executar o Claude Code:', error.message);
70
+ return 1;
71
+ }
72
+ return result.status ?? 0;
73
+ }
74
+ /**
75
+ * No Windows o `claude` costuma ser um `.cmd`, que o spawn sem shell não
76
+ * resolve; nesse caso repetimos a chamada pelo shell, com os argumentos citados.
77
+ */
78
+ function spawn(argv) {
79
+ const direct = spawnSync('claude', argv, { stdio: 'inherit', cwd: process.cwd() });
80
+ const error = direct.error;
81
+ if (error?.code === 'ENOENT' && process.platform === 'win32') {
82
+ const quoted = argv.map((arg) => `"${String(arg).replaceAll('"', '\\"')}"`).join(' ');
83
+ return spawnSync(`claude ${quoted}`, { stdio: 'inherit', cwd: process.cwd(), shell: true });
84
+ }
85
+ return direct;
86
+ }
@@ -0,0 +1,115 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { setTimeout as sleep } from 'node:timers/promises';
4
+ import { readArtifact } from '../contratos/artefatos.js';
5
+ import { suggestFlag } from '../install/flags.js';
6
+ import { loadRastroLib } from '../rastro/command.js';
7
+ const FINISHED = new Set(['concluido', 'cancelado']);
8
+ const normalize = (path) => path.trim().replace(/^\.\//, '').replace(/\/+$/, '');
9
+ /** Mesmo arquivo, ou um dos caminhos é uma pasta que contém o outro. */
10
+ function overlaps(a, b) {
11
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
12
+ }
13
+ function readPlan(root, workId) {
14
+ const rel = `docs/trabalhos/${workId}/PLAN.md`;
15
+ const file = join(root, rel);
16
+ if (!existsSync(file))
17
+ return null;
18
+ const { data } = readArtifact(rel, readFileSync(file, 'utf8'));
19
+ if (!data)
20
+ return null;
21
+ const listed = Array.isArray(data.arquivos) ? data.arquivos : [];
22
+ const arquivos = listed.filter((entry) => typeof entry === 'string').map(normalize).filter(Boolean);
23
+ return { status: String(data.status ?? ''), arquivos };
24
+ }
25
+ /**
26
+ * Trabalhos em execução (PLAN.md `em-execucao` ou listados no estado.json) que declaram arquivos em comum
27
+ * com o plano do trabalho informado. Várias sessões executam trabalhos na mesma pasta; dois trabalhos
28
+ * mexendo no mesmo código ao mesmo tempo quebram os testes um do outro.
29
+ */
30
+ export async function findConflicts(root, workId) {
31
+ const own = readPlan(root, workId);
32
+ if (!own)
33
+ throw new Error(`Não há docs/trabalhos/${workId}/PLAN.md com frontmatter legível.`);
34
+ const listed = new Set((await loadRastroLib()).findActiveWorks(root));
35
+ const report = { arquivos: own.arquivos, conflitos: [], semArquivos: [] };
36
+ for (const id of readdirSync(join(root, 'docs', 'trabalhos'))) {
37
+ if (id === workId)
38
+ continue;
39
+ const plan = readPlan(root, id);
40
+ if (!plan || FINISHED.has(plan.status) || (plan.status !== 'em-execucao' && !listed.has(id)))
41
+ continue;
42
+ if (!plan.arquivos.length) {
43
+ report.semArquivos.push(id);
44
+ continue;
45
+ }
46
+ const shared = own.arquivos.filter((a) => plan.arquivos.some((b) => overlaps(a, b)));
47
+ if (shared.length)
48
+ report.conflitos.push({ trabalho: id, arquivos: shared });
49
+ }
50
+ return report;
51
+ }
52
+ export function formatConflicts(workId, report) {
53
+ const lines = [];
54
+ if (!report.arquivos.length) {
55
+ lines.push(`O PLAN.md de ${workId} não declara arquivos em \`arquivos\`: sem eles não há como detectar conflito com outros trabalhos.`);
56
+ }
57
+ for (const id of report.semArquivos) {
58
+ lines.push(`O trabalho ${id} está em execução, mas o PLAN.md dele não declara arquivos: não há como comparar com ele.`);
59
+ }
60
+ if (!report.conflitos.length) {
61
+ lines.push(`Nenhum trabalho em execução mexe nos arquivos de ${workId}.`);
62
+ return lines.join('\n');
63
+ }
64
+ lines.push(`${workId} mexe em arquivos que estão em uso por outro trabalho em execução:`, ...report.conflitos.map((conflict) => ` - ${conflict.trabalho}: ${conflict.arquivos.join(', ')}`), `Aguarde a conclusão (npx @fontdata/ai-harness conflitos ${workId} --aguardar). Se um desses trabalhos foi ` +
65
+ 'abandonado, troque o status do PLAN.md dele para `cancelado`.');
66
+ return lines.join('\n');
67
+ }
68
+ /** Espera até nenhum trabalho em execução mexer nos mesmos arquivos. */
69
+ export async function waitForFreeFiles(root, workId, options) {
70
+ const wait = options.wait ?? sleep;
71
+ let previous = '';
72
+ for (;;) {
73
+ const report = await findConflicts(root, workId);
74
+ if (!report.conflitos.length)
75
+ return report;
76
+ const holders = report.conflitos.map((conflict) => conflict.trabalho).join(', ');
77
+ if (holders !== previous)
78
+ options.log(`Aguardando ${holders} concluir para liberar os arquivos de ${workId}...`);
79
+ previous = holders;
80
+ await wait(options.intervalMs);
81
+ }
82
+ }
83
+ const FLAGS = ['--aguardar', '--intervalo'];
84
+ /** `ai-harness conflitos <id> [--aguardar] [--intervalo <segundos>]` — sai com 1 quando há conflito sem --aguardar. */
85
+ export async function runConflictsCommand(argv, root = process.cwd()) {
86
+ const [workId, ...rest] = argv;
87
+ if (!workId || workId.startsWith('-')) {
88
+ throw new Error('Informe o id do trabalho. Ex.: npx @fontdata/ai-harness conflitos 2026-09-18-login --aguardar');
89
+ }
90
+ let waitMode = false;
91
+ let intervalMs = 30_000;
92
+ for (let i = 0; i < rest.length; i++) {
93
+ const flag = rest[i] ?? '';
94
+ if (flag === '--aguardar')
95
+ waitMode = true;
96
+ else if (flag === '--intervalo') {
97
+ const seconds = Number(rest[++i]);
98
+ if (!Number.isFinite(seconds) || seconds <= 0)
99
+ throw new Error('--intervalo precisa de um número de segundos maior que zero.');
100
+ intervalMs = seconds * 1000;
101
+ }
102
+ else {
103
+ const suggestion = suggestFlag(flag, FLAGS);
104
+ throw new Error(`Flag desconhecida: ${flag}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
105
+ }
106
+ }
107
+ if (!waitMode) {
108
+ const report = await findConflicts(root, workId);
109
+ console.log(formatConflicts(workId, report));
110
+ return report.conflitos.length ? 1 : 0;
111
+ }
112
+ const report = await waitForFreeFiles(root, workId, { intervalMs, log: (line) => console.log(line) });
113
+ console.log(formatConflicts(workId, report));
114
+ return 0;
115
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};