primocode 8.42.2 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +42 -195
  7. package/lib/catalogo.js +100 -61
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +576 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. package/studio/web/view.html +0 -27
@@ -0,0 +1,137 @@
1
+ /**
2
+ * permissao.js — o portão do controle do computador.
3
+ *
4
+ * "Implementar controle do computador do usuário. Sempre pedir permissão
5
+ * antes: aparece o pedido, o usuário autoriza, e só então o agente age. O
6
+ * usuário assiste o agente mexendo na máquina em tempo real."
7
+ *
8
+ * ── O QUE FALTAVA ────────────────────────────────────────────────────────
9
+ * O controle do computador já existia inteiro (lib/desktop.js): mouse,
10
+ * teclado, janelas, clique em botão pelo nome. O que não existia era o
11
+ * pedido. O agente decidia mexer e mexia — o ponteiro saía andando sozinho
12
+ * sem ninguém ter dito que podia.
13
+ *
14
+ * E a diferença entre isso e as outras ferramentas é real, não cerimônia:
15
+ * escrever arquivo acontece DENTRO da pasta do projeto, e o que der errado se
16
+ * desfaz. Mover o mouse e apertar teclas acontece no computador inteiro, no
17
+ * programa que estiver na frente — e um `alt+tab` no momento errado digita o
18
+ * que o agente ia escrever dentro da sua conversa aberta no WhatsApp.
19
+ *
20
+ * ── AS TRÊS RESPOSTAS ────────────────────────────────────────────────────
21
+ * "Sim, agora" · "Sim, esta sessão inteira" · "Não".
22
+ *
23
+ * A do meio existe porque uma tarefa de desktop são dez, vinte ações. Pedir
24
+ * a cada clique transformaria a permissão num botão que a pessoa aperta sem
25
+ * ler — que é o mesmo que não pedir, com uma etapa a mais. Pedir UMA vez, com
26
+ * o que ele vai fazer escrito, é a pergunta que ainda é lida.
27
+ *
28
+ * A sessão é a sessão: fechar o PrimoCode esquece tudo. Não há arquivo de
29
+ * "sempre permitir" de propósito — uma autorização permanente é exatamente o
30
+ * que ninguém lembra de ter dado.
31
+ *
32
+ * ── O QUE NÃO PASSA POR AQUI ─────────────────────────────────────────────
33
+ * Ler a tela (`desktop_screenshot`, `desktop_windows`, `desktop_elements`,
34
+ * `desktop_size`) não mexe em nada — é o agente ABRINDO O OLHO, e é o que ele
35
+ * precisa fazer para saber onde clicar. Exigir autorização para olhar
36
+ * obrigaria a pessoa a responder duas vezes a mesma pergunta: uma para ele
37
+ * ver, outra para ele agir.
38
+ *
39
+ * O print merece uma linha à parte: ele vê a sua tela inteira, e isso é
40
+ * sério. Mas quem decide se o conteúdo da tela sai da máquina é o modelo, e
41
+ * modelo de texto não enxerga imagem — o print vira arquivo no disco, para o
42
+ * agente medir onde as coisas estão. Ler é olhar; o portão é para AGIR.
43
+ */
44
+
45
+ 'use strict';
46
+
47
+ // Ferramentas que MEXEM na máquina. Todo o resto do `desktop_*` é olhar.
48
+ const AGEM = new Set([
49
+ 'desktop_click', 'desktop_double_click', 'desktop_click_element',
50
+ 'desktop_move', 'desktop_drag', 'desktop_type', 'desktop_key',
51
+ 'desktop_scroll', 'desktop_open_app',
52
+ ]);
53
+
54
+ /* O que cada uma faz, em português de gente. Vai na pergunta: "autoriza
55
+ desktop_click_element" não é uma pergunta que alguém consiga responder. */
56
+ const EM_PORTUGUES = {
57
+ desktop_click: 'clicar na sua tela',
58
+ desktop_double_click: 'dar um clique duplo na sua tela',
59
+ desktop_click_element: 'clicar num botão da janela aberta',
60
+ desktop_move: 'mover o seu mouse',
61
+ desktop_drag: 'arrastar com o seu mouse',
62
+ desktop_type: 'digitar no programa que está na frente',
63
+ desktop_key: 'apertar teclas no programa que está na frente',
64
+ desktop_scroll: 'rolar a tela',
65
+ desktop_open_app: 'abrir um programa no seu computador',
66
+ };
67
+
68
+ function novo() {
69
+ return { liberado: false, negado: false, pedidos: 0 };
70
+ }
71
+
72
+ /** Esta ferramenta mexe na máquina? */
73
+ function precisa(nome) { return AGEM.has(nome); }
74
+
75
+ /** A frase do que ele quer fazer, com o alvo quando há um. */
76
+ function descrever(nome, args) {
77
+ const a = args || {};
78
+ const base = EM_PORTUGUES[nome] || nome;
79
+ const alvo = a.name || a.app || a.window || a.text || a.key
80
+ || (a.x != null ? `${a.x},${a.y}` : '');
81
+ const curto = String(alvo || '').replace(/\s+/g, ' ').trim();
82
+ return curto ? `${base} — ${curto.length > 60 ? curto.slice(0, 59) + '…' : curto}` : base;
83
+ }
84
+
85
+ /**
86
+ * O portão.
87
+ *
88
+ * @param {object} estado o estado da sessão (de `novo()`)
89
+ * @param {string} nome a ferramenta
90
+ * @param {object} args os argumentos dela
91
+ * @param {function} perguntar async (frase) => 'agora' | 'sessao' | 'nao'
92
+ * @returns {Promise<{ok: boolean, error?: string, primeira?: boolean}>}
93
+ */
94
+ async function pedir(estado, nome, args, perguntar) {
95
+ if (!precisa(nome)) return { ok: true };
96
+ if (estado.liberado) return { ok: true };
97
+
98
+ /* Negar uma vez vale para a sessão. Sem isto, quem disse não leva a mesma
99
+ pergunta na ação seguinte, e na seguinte — o agente tem vinte ações
100
+ para tentar, e a pessoa acabaria dizendo sim de cansaço. */
101
+ if (estado.negado) {
102
+ return {
103
+ ok: false,
104
+ error: 'O usuário NÃO autorizou o controle do computador nesta sessão. '
105
+ + 'Não tente de novo com outra ferramenta de desktop: a resposta é a mesma. '
106
+ + 'Resolva por outro caminho (arquivos, terminal, navegador) ou diga a ele '
107
+ + 'o que precisaria ser feito à mão.',
108
+ };
109
+ }
110
+
111
+ // Sem quem responda (pipe, CI, subagente), a resposta é NÃO. O padrão
112
+ // seguro de uma pergunta que ninguém ouviu não é "pode".
113
+ if (typeof perguntar !== 'function') {
114
+ return {
115
+ ok: false,
116
+ error: 'Controle do computador precisa de autorização, e não há ninguém '
117
+ + 'para autorizar neste modo. Use outro caminho.',
118
+ };
119
+ }
120
+
121
+ const primeira = estado.pedidos === 0;
122
+ estado.pedidos++;
123
+ const resposta = await perguntar(descrever(nome, args), { primeira });
124
+
125
+ if (resposta === 'sessao') { estado.liberado = true; return { ok: true, primeira }; }
126
+ if (resposta === 'agora') return { ok: true, primeira };
127
+
128
+ estado.negado = true;
129
+ return {
130
+ ok: false,
131
+ error: 'O usuário recusou o controle do computador. Não insista com outra '
132
+ + 'ferramenta de desktop — resolva por outro caminho ou explique a ele '
133
+ + 'o que precisaria ser feito à mão.',
134
+ };
135
+ }
136
+
137
+ module.exports = { novo, precisa, pedir, descrever, AGEM, EM_PORTUGUES };
@@ -0,0 +1,229 @@
1
+ /**
2
+ * registro.js — o que este pedido fez, para dizer no fim.
3
+ *
4
+ * "Precisa existir um pipeline visível de execução: o que está fazendo, o
5
+ * que já fez, o que falhou. Exibir sempre: arquivos alterados, arquivos
6
+ * criados, links abertos, comandos rodados, resultados dos testes."
7
+ *
8
+ * ── O QUE FALTAVA ────────────────────────────────────────────────────────
9
+ * Tudo isso JÁ aparecia — uma linha por vez, conforme acontecia. O problema é
10
+ * que uma tarefa de verdade são trinta ações, e no fim delas a primeira já
11
+ * saiu da tela. Quem acompanhou viu tudo; quem voltou do café viu o fim de um
12
+ * rolo de texto e não sabe se o agente criou três arquivos ou trinta, nem se
13
+ * o teste que ele rodou passou.
14
+ *
15
+ * Então, além de narrar, ele ANOTA. No fim sai um balanço do pedido inteiro:
16
+ * o que nasceu, o que mudou, o que rodou, o que abriu, o que falhou.
17
+ *
18
+ * ── POR QUE ANOTAR AQUI E NÃO OLHAR O DISCO ──────────────────────────────
19
+ * A tentação é perguntar ao git no fim. Não serve: a pasta pode não ser um
20
+ * repositório, pode ter mudanças anteriores ao pedido, e o `git status` não
21
+ * sabe qual comando o agente rodou nem qual link ele abriu. O que interessa é
22
+ * o que o AGENTE fez neste pedido, e quem sabe isso é quem executou.
23
+ *
24
+ * ── O QUE CONTA COMO TESTE ───────────────────────────────────────────────
25
+ * Um `run_shell` cujo comando parece rodar teste. A distinção importa porque
26
+ * "rodei o teste" é a frase que mais se diz sem prova: com o registro, o que
27
+ * sai na tela é o comando REAL e o código de saída REAL dele. Quando a saída
28
+ * traz contagem reconhecível ("12 passed, 1 failed"), ela entra também — mas
29
+ * o código de saída é o que decide, porque é o que não dá para escrever por
30
+ * engano.
31
+ */
32
+
33
+ 'use strict';
34
+
35
+ const { c, truncate, padRight } = require('./ui.js');
36
+
37
+ /* Comando que é teste. Deliberadamente restrito: `npm run build` não é
38
+ teste, e chamar de teste o que não é seria pior que não contar nada. */
39
+ const EH_TESTE = /(^|\s|&&|\|\|)(npm\s+(run\s+)?tests?\b|npm\s+t\b|yarn\s+tests?\b|pnpm\s+tests?\b|pytest\b|python\s+-m\s+pytest\b|jest\b|vitest\b|mocha\b|go\s+test\b|cargo\s+test\b|mvn\s+test\b|rspec\b|phpunit\b)/i;
40
+
41
+ /* As contagens que os corredores de teste escrevem. Nenhuma é obrigatória:
42
+ quando nada casa, vale o código de saída, que é o que não mente. */
43
+ const CONTAGENS = [
44
+ /(\d+)\s+pass(?:ed|ing)\b/i,
45
+ /(\d+)\s+fail(?:ed|ing|ures?)\b/i,
46
+ ];
47
+
48
+ function novo() {
49
+ return {
50
+ criados: [], // caminhos que nasceram neste pedido
51
+ alterados: [], // caminhos que já existiam e mudaram
52
+ comandos: [], // { comando, ok, code }
53
+ testes: [], // { comando, ok, code, resumo }
54
+ links: [], // URLs e arquivos abertos no navegador
55
+ falhas: [], // { acao, motivo }
56
+ pesquisas: [], // consultas que ele foi olhar antes de escrever
57
+ };
58
+ }
59
+
60
+ const jaTem = (lista, valor) => lista.includes(valor);
61
+
62
+ /** Guarda sem repetir, e sem deixar a lista crescer sem fim. */
63
+ function guardar(lista, valor, teto = 60) {
64
+ if (!valor || jaTem(lista, valor)) return;
65
+ if (lista.length < teto) lista.push(valor);
66
+ }
67
+
68
+ /** A linha de resumo de um teste, quando dá para ler a contagem da saída. */
69
+ function resumoDeTeste(saida) {
70
+ const texto = String(saida || '');
71
+ const passou = (texto.match(CONTAGENS[0]) || [])[1];
72
+ const falhou = (texto.match(CONTAGENS[1]) || [])[1];
73
+ if (!passou && !falhou) return '';
74
+ const partes = [];
75
+ if (passou) partes.push(`${passou} passaram`);
76
+ if (falhou && Number(falhou) > 0) partes.push(`${falhou} falharam`);
77
+ return partes.join(', ');
78
+ }
79
+
80
+ /**
81
+ * Anota uma ação que acabou de acontecer.
82
+ *
83
+ * @param {object} reg o registro deste pedido
84
+ * @param {string} nome o nome da tool
85
+ * @param {object} args os argumentos com que ela foi chamada
86
+ * @param {object} r o resultado que ela devolveu
87
+ */
88
+ function anotar(reg, nome, args, r) {
89
+ if (!reg || !r) return reg;
90
+ const a = args || {};
91
+
92
+ // Ação que falhou entra sempre, seja qual for a tool: é justamente o que
93
+ // a pessoa precisa ver, e o que some mais rápido do rolo de texto.
94
+ if (r.ok === false) {
95
+ const motivo = String(r.error || `saiu com ${r.code}`).replace(/\s+/g, ' ').trim();
96
+ /* QUAL arquivo, QUAL comando. Sem isso a linha vira
97
+ "run_shell: saiu com 1" — que diz que algo falhou e não diz o quê,
98
+ que é o mesmo que não dizer nada. */
99
+ const alvo = String(a.path || a.command || a.target || a.url || a.consulta || '')
100
+ .replace(/\s+/g, ' ').trim();
101
+ if (reg.falhas.length < 20) reg.falhas.push({ acao: nome, alvo, motivo });
102
+ // Uma tool que falhou não criou nem alterou nada. Continuar daqui
103
+ // faria um write_file recusado aparecer na lista de criados.
104
+ return reg;
105
+ }
106
+
107
+ switch (nome) {
108
+ case 'write_file':
109
+ // `created` vem do próprio tools.js: ele sabe se o arquivo existia.
110
+ guardar(r.created ? reg.criados : reg.alterados, r.path || a.path);
111
+ return reg;
112
+
113
+ case 'edit_file':
114
+ case 'multi_edit':
115
+ guardar(reg.alterados, r.path || a.path);
116
+ return reg;
117
+
118
+ case 'run_shell': {
119
+ const comando = String(a.command || '').replace(/\s+/g, ' ').trim();
120
+ if (!comando) return reg;
121
+ const item = { comando, ok: r.ok !== false, code: r.code ?? 0 };
122
+ if (EH_TESTE.test(comando)) {
123
+ item.resumo = resumoDeTeste(r.stdout || r.saida || r.output || '');
124
+ if (reg.testes.length < 20) reg.testes.push(item);
125
+ } else if (reg.comandos.length < 40) {
126
+ reg.comandos.push(item);
127
+ }
128
+ return reg;
129
+ }
130
+
131
+ case 'run_in_new_terminal':
132
+ if (reg.comandos.length < 40) {
133
+ reg.comandos.push({ comando: String(a.command || '').trim(), ok: true, code: 0, solto: true });
134
+ }
135
+ return reg;
136
+
137
+ case 'open_in_browser':
138
+ case 'browser_open':
139
+ guardar(reg.links, r.url || a.target || a.url);
140
+ return reg;
141
+
142
+ case 'pesquisar_referencia':
143
+ guardar(reg.pesquisas, a.consulta || a.query, 12);
144
+ return reg;
145
+
146
+ default:
147
+ return reg;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Junta o livro de um subagente no de quem o chamou.
153
+ *
154
+ * Sem isto o balanço mentiria por omissão: o especialista de frontend
155
+ * escreveria cinco arquivos e a tela diria "nenhum arquivo". As conversas
156
+ * seguem separadas — o que se junta é o que aconteceu no disco.
157
+ */
158
+ function juntar(destino, outro) {
159
+ if (!destino || !outro) return destino;
160
+ for (const campo of ['criados', 'alterados', 'links', 'pesquisas']) {
161
+ for (const v of outro[campo] || []) guardar(destino[campo], v);
162
+ }
163
+ for (const campo of ['comandos', 'testes', 'falhas']) {
164
+ for (const v of outro[campo] || []) {
165
+ if (destino[campo].length < 40) destino[campo].push(v);
166
+ }
167
+ }
168
+ return destino;
169
+ }
170
+
171
+ /** Alguma coisa aconteceu? Um pedido de conversa pura não tem balanço. */
172
+ function vazio(reg) {
173
+ if (!reg) return true;
174
+ return !reg.criados.length && !reg.alterados.length && !reg.comandos.length
175
+ && !reg.testes.length && !reg.links.length && !reg.falhas.length;
176
+ }
177
+
178
+ /**
179
+ * O balanço, em texto pronto para a tela.
180
+ *
181
+ * Ordem deliberada: primeiro o que mudou no computador da pessoa (arquivos),
182
+ * depois o que ele rodou, depois a prova (testes), e por último o que falhou
183
+ * — que é o que ela precisa ler ANTES de ir embora, então fica onde o olho
184
+ * para, no fim.
185
+ */
186
+ function resumo(reg, { colunas = 80 } = {}) {
187
+ if (vazio(reg)) return '';
188
+ const larg = Math.max(40, Math.min(colunas, 120));
189
+ const out = [];
190
+
191
+ const secao = (rotulo, itens, pinta) => {
192
+ if (!itens.length) return;
193
+ out.push(' ' + c.dim(padRight(rotulo, 12)) + pinta(itens[0]));
194
+ for (const item of itens.slice(1)) out.push(' ' + ' '.repeat(12) + pinta(item));
195
+ };
196
+
197
+ out.push('');
198
+ out.push(' ' + c.dim('─'.repeat(larg - 4)));
199
+
200
+ secao('criados', reg.criados, (f) => c.ok('+ ') + c.white(f));
201
+ secao('alterados', reg.alterados, (f) => c.brandDim('~ ') + c.white(f));
202
+ secao('comandos', reg.comandos.map((x) => x), (x) => {
203
+ const marca = x.solto ? c.dim('▸ ') : x.ok ? c.dim('$ ') : c.err('$ ');
204
+ return marca + truncate(x.ok ? c.muted(x.comando) : c.warn(x.comando), larg - 18);
205
+ });
206
+ secao('links', reg.links, (u) => c.dim('↗ ') + c.brandDim(truncate(u, larg - 18)));
207
+
208
+ // Os testes ganham seção própria porque são a PROVA. "Rodei os testes" é
209
+ // a frase que mais se diz sem prova; aqui sai o comando real e o código
210
+ // de saída real, que é o que não dá para escrever por engano.
211
+ if (reg.testes.length) {
212
+ secao('testes', reg.testes, (t) => {
213
+ const marca = t.ok ? c.ok('✔ ') : c.err('✗ ');
214
+ const cauda = t.resumo ? c.muted(' ' + t.resumo)
215
+ : t.ok ? c.dim(' passou') : c.warn(` saiu com ${t.code}`);
216
+ return marca + c.white(truncate(t.comando, larg - 30)) + cauda;
217
+ });
218
+ }
219
+
220
+ if (reg.falhas.length) {
221
+ secao('falhou', reg.falhas, (f) => c.warn('! ')
222
+ + c.muted(truncate(`${f.alvo || f.acao}${f.alvo ? '' : ''} — ${f.motivo}`, larg - 18)));
223
+ }
224
+
225
+ out.push('');
226
+ return out.join('\n');
227
+ }
228
+
229
+ module.exports = { novo, anotar, juntar, resumo, vazio, resumoDeTeste, EH_TESTE };