primocode 11.1.0 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +16 -2
  2. package/bin/primocode-rpc.js +100 -0
  3. package/lib/agentes-pessoais.js +120 -0
  4. package/lib/app-server/cli.js +64 -0
  5. package/lib/app-server/enderecos.js +43 -0
  6. package/lib/app-server/index.js +3 -0
  7. package/lib/app-server/pareamento.js +61 -0
  8. package/lib/app-server/protocolo.js +86 -0
  9. package/lib/app-server/servidor.js +805 -0
  10. package/lib/app-server/websocket.js +87 -0
  11. package/lib/avisos.js +65 -0
  12. package/lib/cli/sessao.js +202 -17
  13. package/lib/cli-nova.js +237 -0
  14. package/lib/config/config.js +22 -2
  15. package/lib/diff.js +12 -2
  16. package/lib/motor/aprovacao.js +114 -0
  17. package/lib/motor/eventos.js +77 -0
  18. package/lib/motor/index.js +8 -0
  19. package/lib/motor/tela-terminal.js +64 -0
  20. package/lib/motor/transcricao.js +57 -0
  21. package/lib/motor/turno.js +43 -0
  22. package/lib/motor/voz.js +65 -0
  23. package/lib/piloto.js +39 -0
  24. package/lib/plugin/entregaveis/nucleo.js +2 -2
  25. package/lib/plugin/estudio/criar.js +113 -0
  26. package/lib/plugin/estudio/gosto.js +3 -0
  27. package/lib/plugin/estudio/historico.js +60 -0
  28. package/lib/plugin/estudio/index.js +14 -1
  29. package/lib/plugin/estudio/kits/tela.html +3 -1
  30. package/lib/plugin/estudio/miniatura.js +56 -0
  31. package/lib/plugin/video/auto.js +1 -1
  32. package/lib/plugin/video/catalogo.js +20 -0
  33. package/lib/plugin/video/mutar.js +28 -0
  34. package/lib/plugin/video/ops-fx.js +6 -6
  35. package/lib/plugin/video/ops.js +2 -13
  36. package/lib/plugin/video/pedido.js +1 -1
  37. package/lib/plugin/video/zoom.js +1 -1
  38. package/lib/projetos-meta.js +107 -0
  39. package/lib/qr.js +209 -0
  40. package/lib/session/runner/act.js +127 -68
  41. package/lib/skill/catalogo.js +3 -1
  42. package/lib/tool/tools.js +7 -0
  43. package/lib/tui/entrada.js +143 -18
  44. package/lib/tui/tela.js +312 -49
  45. package/lib/tui/ui.js +78 -4
  46. package/package.json +10 -2
  47. package/voz/primo_voz/transcrever.py +12 -0
@@ -40,6 +40,7 @@ const tempos = require('../../tempos');
40
40
  const preLeitura = require('../../pre-leitura');
41
41
  const travado = require('../../travado');
42
42
  const auditoria = require('../../auditoria');
43
+ const eventosMotor = require('../../motor/eventos');
43
44
  const chamadaTorta = require('../../chamada-torta');
44
45
  const idioma = require('../../idioma');
45
46
  const entregaveis = require('../../plugin/entregaveis');
@@ -103,10 +104,14 @@ function trabalhando() { return TRABALHANDO; }
103
104
  /** O que ficou na caixa sem ser lido — o terminal roda como pedido novo. */
104
105
  function sobrasDaCaixa() { return CAIXA.splice(0); }
105
106
  /* Esvazia a caixa dentro da conversa. Devolve quantos recados entraram. */
106
- function entregarRecados(messages, profundidade) {
107
+ function entregarRecados(messages, profundidade, emitir) {
107
108
  if (!CAIXA.length) return 0;
108
109
  const recados = CAIXA.splice(0);
109
- for (const r of recados) fala.pensamento(`recado recebido — entrando na tarefa: ${r}`, profundidade);
110
+ for (const r of recados) {
111
+ const texto = `recado recebido — entrando na tarefa: ${r}`;
112
+ if (emitir) emitir(eventosMotor.montar('pensamento', { profundidade, texto }));
113
+ else fala.pensamento(texto, profundidade);
114
+ }
110
115
  messages.push({
111
116
  role: 'user',
112
117
  content: (recados.length === 1 ? 'MENSAGEM NOVA do usuário, enviada enquanto você trabalhava:\n'
@@ -241,6 +246,17 @@ function ident(profundidade) {
241
246
  * duas versões do texto, que o `limparParaOModelo` apaga antes de a conversa
242
247
  * seguir. Sem essa limpeza, cada edição mandaria o arquivo inteiro de volta
243
248
  * ao modelo — que acabou de escrevê-lo e não aprende nada relendo. */
249
+ /* +N −M de uma escrita ou edição: o diff de verdade (linhas que entraram e saíram). Arquivo
250
+ novo conta todas as linhas como entradas. Sem as duas versões nem a contagem, null. */
251
+ function contarMudanca(result) {
252
+ if (!result) return null;
253
+ if (typeof result._antes === 'string' && typeof result._depois === 'string') {
254
+ try { return diff.contar(diff.operacoes(result._antes, result._depois)); } catch { return null; }
255
+ }
256
+ if (result.created && Number.isFinite(result.linhas)) return { mais: result.linhas, menos: 0 };
257
+ return null;
258
+ }
259
+
244
260
  function mostrarDiff(result, profundidade) {
245
261
  if (!result || typeof result._antes !== 'string' || typeof result._depois !== 'string') return;
246
262
  const bloco = diff.desenhar(result._antes, result._depois, {
@@ -258,6 +274,14 @@ function limparParaOModelo(result) {
258
274
  return limpo;
259
275
  }
260
276
 
277
+ /* Os eventos do subagente, do diretor e do pipeline vão para o mesmo emissor
278
+ do pedido principal (vem no ctx). Sem ele, uma tela de terminal nova. */
279
+ function emitirDe(ctx) {
280
+ if (!ctx.emitir) ctx.emitir = require('../../motor/tela-terminal.js').criar();
281
+ return ctx.emitir;
282
+ }
283
+ const telaDe = (ctx, vista, dados) => emitirDe(ctx)(eventosMotor.montar(vista, { profundidade: ctx.profundidade, ...dados }));
284
+
261
285
  function logResult(kind, result, profundidade) {
262
286
  const p = fala.resultado(profundidade);
263
287
  if (!result) return;
@@ -274,17 +298,24 @@ function logResult(kind, result, profundidade) {
274
298
  }
275
299
 
276
300
  switch (kind) {
277
- case 'write_file':
278
- console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`));
301
+ case 'write_file': {
302
+ const k = contarMudanca(result);
303
+ if (k) require('../../tui/ui').registrarArquivo(result.path, k.mais, k.menos);
304
+ console.log(p + c.muted(`${result.created ? 'criado' : 'atualizado'} · ${tools.fmtBytes(result.bytes)}`)
305
+ + (k ? c.muted(' · ') + c.ok(`+${k.mais}`) + ' ' + c.err(`−${k.menos}`) : ''));
279
306
  // Reescrever arquivo que já existia apaga o que estava lá. O
280
307
  // número de bytes não conta isso; o bloco embaixo conta.
281
308
  mostrarDiff(result, profundidade);
282
309
  return;
310
+ }
283
311
  case 'edit_file':
284
312
  case 'multi_edit': {
285
313
  const delta = result.linhasDepois - result.linhasAntes;
286
314
  const sinal = delta > 0 ? c.ok(`+${delta}`) : delta < 0 ? c.err(`${delta}`) : c.muted('±0');
287
- console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas'));
315
+ const k = contarMudanca(result);
316
+ if (k) require('../../tui/ui').registrarArquivo(result.path, k.mais, k.menos);
317
+ console.log(p + c.muted(`${result.substituicoes} troca${result.substituicoes > 1 ? 's' : ''} · `) + sinal + c.muted(' linhas')
318
+ + (k ? c.muted(' · ') + c.ok(`+${k.mais}`) + ' ' + c.err(`−${k.menos}`) : ''));
288
319
  mostrarDiff(result, profundidade);
289
320
  return;
290
321
  }
@@ -329,6 +360,7 @@ function logResult(kind, result, profundidade) {
329
360
  return;
330
361
  }
331
362
  case 'write_todos': {
363
+ require('../../tui/ui').estadoSessao.todos = result.todos || [];
332
364
  const marca = { completed: c.ok('✔'), in_progress: c.brand('▶'), pending: c.dim('○') };
333
365
  for (const t of result.todos || []) {
334
366
  const texto = t.status === 'completed' ? c.dim(t.content) : c.white(t.content);
@@ -920,7 +952,28 @@ async function runActInterno(userPrompt, opts) {
920
952
  : () => require('../../browser.js').aquecerRevisor(),
921
953
  /* Quem é este agente no livro de ações (auditoria.js). */
922
954
  rotulo = subagent ? 'subagente' : 'principal',
955
+ /* Para onde vão os eventos do turno (lib/motor/eventos.js). O motor
956
+ não escreve na tela: quem desenha assina. Sem ninguém de fora, nasce
957
+ uma tela de terminal (lib/motor/tela-terminal.js) — a saída de sempre. */
958
+ emitir: emitirDeFora = null,
923
959
  } = opts || {};
960
+ const emitir = emitirDeFora || require('../../motor/tela-terminal.js').criar();
961
+ const ev = (vista, dados) => emitir(eventosMotor.montar(vista, { profundidade, ...dados }));
962
+ /* Spinner por evento: a tela decide se gira (terminal) ou se mostra "trabalhando" (app). */
963
+ const girar = (rotulo) => {
964
+ const id = eventosMotor.novoId();
965
+ ev('spinner-inicia', { id, rotulo });
966
+ return {
967
+ set detalhe(v) { ev('spinner-detalhe', { id, detalhe: v }); },
968
+ stop() { ev('spinner-para', { id }); },
969
+ };
970
+ };
971
+ /* O texto que corre na tela conforme chega: mesma interface do fala.fluxo(). */
972
+ const abrirFluxo = (texto) => {
973
+ const id = eventosMotor.novoId();
974
+ ev('fluxo-inicia', { id, texto });
975
+ return { escrever: (t) => ev('fluxo-escreve', { id, texto: t }), fechar: () => ev('fluxo-fecha', { id }) };
976
+ };
924
977
 
925
978
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
926
979
  // Antes ela nascia na primeira tool qualquer — pedir "abre o Chrome" criava
@@ -968,7 +1021,7 @@ async function runActInterno(userPrompt, opts) {
968
1021
  const t = Date.now();
969
1022
  // Com spinner: eram segundos de tela parada entre o último arquivo e o
970
1023
  // "pronto", e parecia que tinha travado ali.
971
- const sp = new Spinner('abrindo a página escondido para conferir').start();
1024
+ const sp = girar('abrindo a página escondido para conferir');
972
1025
  try {
973
1026
  medida = await medirPagina(pastaAtual);
974
1027
  } catch { medida = null; } finally { sp.stop(); }
@@ -977,7 +1030,7 @@ async function runActInterno(userPrompt, opts) {
977
1030
  if (!medida || !medida.testou) return null;
978
1031
  avisoDaPagina = medida.limpo ? ''
979
1032
  : String(medida.relato || '').split('\n').filter((l) => /·|BRANCO/.test(l)).slice(0, 3).join(' ');
980
- if (medida.limpo) { fala.pensamento('a página abriu sem erro', profundidade); return null; }
1033
+ if (medida.limpo) { ev('pensamento', { texto: 'a página abriu sem erro' }); return null; }
981
1034
  return conferencias >= 2 ? null : medida.relato;
982
1035
  };
983
1036
  /* ── RODAR OS TESTES ANTES DE DIZER "PRONTO" (lib/conferir.js) ──────
@@ -1000,7 +1053,7 @@ async function runActInterno(userPrompt, opts) {
1000
1053
  const achados = K.procurar([...arquivosDaTarefa], { raiz: pastaAtual });
1001
1054
  avisoDaChave = K.relato(achados);
1002
1055
  if (!avisoDaChave) return null;
1003
- console.log(c.warn(` ⚠ ${avisoDaChave}`));
1056
+ ev('linha', { texto: ` ⚠ ${avisoDaChave}`, nivel: 'aviso' });
1004
1057
  return conferiuChaves >= 2 ? null : `${avisoDaChave} Tire a chave do código: leia de um .env (process.env) ou do servidor, e ponha o .env no .gitignore.`;
1005
1058
  };
1006
1059
  const conferirTestes = async () => {
@@ -1022,14 +1075,14 @@ async function runActInterno(userPrompt, opts) {
1022
1075
  if (r.ok) {
1023
1076
  avisoDosTestes = '';
1024
1077
  if (erroQueHavia) notaDoConserto = `O que estava errado: os testes falhavam (${erroQueHavia}); depois do conserto, passaram.`;
1025
- console.log(c.ok(` ✓ ${ex.titulo}`) + c.dim(` (${como.comando})`));
1078
+ ev('linha', { texto: ` ✓ ${ex.titulo} (${como.comando})`, nivel: 'ok' });
1026
1079
  try { checkpoint.marcarFuncionando(pastaAtual, `${como.comando}, pelo agente`); } catch { /* sem ponto pra marcar */ }
1027
1080
  return null;
1028
1081
  }
1029
1082
  avisoDosTestes = `${ex.titulo} ${ex.linhas.slice(0, 2).join(' ')}`.trim();
1030
1083
  // a frase que explica (a do Error:, se houver), não a do stack: é a que a pessoa entende
1031
1084
  if (!erroQueHavia) erroQueHavia = String(ex.linhas.find((l) => /error|erro|assert|fail|falh/i.test(l)) || ex.linhas[0] || '').trim().slice(0, 140);
1032
- console.log(c.warn(` ✗ ${ex.titulo}`) + c.dim(` (${como.comando})`));
1085
+ ev('linha', { texto: ` ✗ ${ex.titulo} (${como.comando})`, nivel: 'aviso' });
1033
1086
  return rodadasDeTeste >= 2 ? null : `Rodei \`${como.comando}\` e falhou:\n${r.saida}`;
1034
1087
  };
1035
1088
  /* O ARQUIVO QUE O PEDIDO MANDA GRAVAR (lib/plugin/entregaveis.js): "grave o
@@ -1098,7 +1151,7 @@ async function runActInterno(userPrompt, opts) {
1098
1151
  if (decisao !== 'recusada' && decisao !== 'negada') require('../../plugin/voz-ponte').ferramenta(tc.name, tc.arguments);
1099
1152
  const id = auditoria.antes({ agente: rotulo, ferramenta: tc.name, args: tc.arguments, decisao, motivo });
1100
1153
  const falhou = auditoria.problema();
1101
- if (falhou) console.log(c.warn(`⚠ ${falhou}`));
1154
+ if (falhou) ev('linha', { texto: `⚠ ${falhou}`, nivel: 'aviso' });
1102
1155
  return id;
1103
1156
  };
1104
1157
  // O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
@@ -1339,10 +1392,10 @@ async function runActInterno(userPrompt, opts) {
1339
1392
  }
1340
1393
  // Recado digitado durante o passo anterior entra AGORA, antes da ida
1341
1394
  // ao modelo — o resultado das ferramentas já está na conversa.
1342
- if (!subagent) entregarRecados(messages, profundidade);
1395
+ if (!subagent) entregarRecados(messages, profundidade, emitir);
1343
1396
  // Tarefa em segundo plano que acabou desde a última volta (lib/fundo.js): o modelo fica sabendo.
1344
1397
  if (!subagent) for (const av of require('../../fundo').pegarAvisos()) messages.push({ role: 'user', content: av.texto });
1345
- const sp = new Spinner(subagent ? 'subagente trabalhando' : PENSANDO).start();
1398
+ const sp = girar(subagent ? 'subagente trabalhando' : PENSANDO);
1346
1399
 
1347
1400
  const toolCalls = [];
1348
1401
  let assistantText = '';
@@ -1375,7 +1428,7 @@ async function runActInterno(userPrompt, opts) {
1375
1428
  resumir: ({ resumo, mensagens }, todos) => comp.resumirComFatos({ resumo, mensagens },
1376
1429
  (p) => motor.requestResumo(server, { ...p, token }).then((x) => x && x.resumo), todos),
1377
1430
  });
1378
- if (r.ok) console.log(ident(profundidade) + c.dim(` ⎿ conversa passou de 80% da janela: ${r.resumidas} mensagens viraram resumo (${r.antes} → ${r.depois} tokens)`));
1431
+ if (r.ok) ev('linha', { texto: ` ⎿ conversa passou de 80% da janela: ${r.resumidas} mensagens viraram resumo (${r.antes} → ${r.depois} tokens)`, nivel: 'dim' });
1379
1432
  }
1380
1433
  } catch { /* resumo é economia, não requisito */ }
1381
1434
  }
@@ -1396,8 +1449,7 @@ async function runActInterno(userPrompt, opts) {
1396
1449
  if (!subagent && !toolCalls.length) {
1397
1450
  if (!fluxo && assistantText.length > fala.LIMIAR_PENSAMENTO) {
1398
1451
  sp.stop();
1399
- fluxo = fala.fluxo();
1400
- fluxo.escrever(assistantText);
1452
+ fluxo = abrirFluxo(assistantText);
1401
1453
  } else if (fluxo) fluxo.escrever(evt.delta);
1402
1454
  }
1403
1455
  }
@@ -1429,14 +1481,14 @@ async function runActInterno(userPrompt, opts) {
1429
1481
  }
1430
1482
  }
1431
1483
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${fala.frase(e)}` } };
1432
- fala.problema(e);
1484
+ ev('problema', { erro: e });
1433
1485
  throw e;
1434
1486
  }
1435
1487
  sp.stop();
1436
1488
  emVoo = null;
1437
1489
  if (!subagent && !avisouReserva && modeloDaVolta && modeloDaVolta.reserva) {
1438
1490
  avisouReserva = true;
1439
- console.log(c.dim(` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`));
1491
+ ev('linha', { texto: ` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`, nivel: 'dim' });
1440
1492
  }
1441
1493
  traco(`volta ${voltas} voltou em ${Math.round((Date.now() - tRodada) / 1000)}s · ${toolCalls.map((t) => t.name).join(', ') || 'texto'}${streamError ? ' · erro: ' + String(streamError).slice(0, 160) : ''}`);
1442
1494
  tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
@@ -1519,7 +1571,7 @@ async function runActInterno(userPrompt, opts) {
1519
1571
  if (fluxo) fluxo.fechar();
1520
1572
  const amigavel = fala.frase(streamError);
1521
1573
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` }, concluiu: false };
1522
- fala.problema(streamError);
1574
+ ev('problema', { erro: streamError });
1523
1575
  // concluiu: false — o plano de passos não pode tratar um erro de
1524
1576
  // provedor como resultado e passá-lo adiante como "fato".
1525
1577
  return { assistantMessage: { role: 'assistant', content: amigavel }, concluiu: false };
@@ -1561,7 +1613,7 @@ async function runActInterno(userPrompt, opts) {
1561
1613
  if (pendentes) cobrouPlano = true; // este empurrão já cobrou o plano aberto
1562
1614
  if (!subagent) {
1563
1615
  if (fluxo) fluxo.fechar();
1564
- else fala.pensamento(finalText, profundidade);
1616
+ else ev('pensamento', { texto: finalText });
1565
1617
  }
1566
1618
  messages.push({ role: 'assistant', content: finalText });
1567
1619
  messages.push({
@@ -1582,7 +1634,7 @@ async function runActInterno(userPrompt, opts) {
1582
1634
  try { const K = require('../../plugin/kits.js'); if (K.continuaVideo(userPrompt)) falta = K.batidasFaltando(pastaAtual); } catch { /* sem kits */ }
1583
1635
  if (falta.length) {
1584
1636
  empurroesDoKit++;
1585
- if (fluxo) fluxo.fechar(); else fala.pensamento(finalText, profundidade);
1637
+ if (fluxo) fluxo.fechar(); else ev('pensamento', { texto: finalText });
1586
1638
  messages.push({ role: 'assistant', content: finalText });
1587
1639
  messages.push({ role: 'user', content: `O vídeo ainda não está montado: faltam as batidas ${falta.join(', ')} em motion/plano.json. Não explique — mande agora a batida ${falta[0]}: run_shell "primocode kit batida ${falta[0]} --camadas '[…]'" (o formulário: "primocode kit batida ${falta[0]}"), e siga até o cartão (primocode kit plano).` });
1588
1640
  finalText = '';
@@ -1645,7 +1697,7 @@ async function runActInterno(userPrompt, opts) {
1645
1697
  }
1646
1698
  if (!subagent) {
1647
1699
  if (fluxo) fluxo.fechar();
1648
- else fala.resposta(finalText);
1700
+ else ev('resposta', { texto: finalText });
1649
1701
  }
1650
1702
  /* Ia terminar, mas chegou recado no meio: a tarefa não acaba com
1651
1703
  a pessoa esperando resposta. A volta seguinte entrega o recado. */
@@ -1661,7 +1713,7 @@ async function runActInterno(userPrompt, opts) {
1661
1713
  // O que o modelo disse ANTES de agir é pensamento, e sai como tal.
1662
1714
  if (!subagent) {
1663
1715
  if (fluxo) fluxo.fechar();
1664
- else if (assistantText.trim()) fala.pensamento(assistantText, profundidade);
1716
+ else if (assistantText.trim()) ev('pensamento', { texto: assistantText });
1665
1717
  }
1666
1718
 
1667
1719
  // Registra o turno do assistente COM os tool_calls. A API exige que
@@ -1694,14 +1746,14 @@ async function runActInterno(userPrompt, opts) {
1694
1746
  /* O plano decide quantos subagentes rodam JUNTOS (lib/config/limites.js; grátis e
1695
1747
  Primo: um de cada vez, o resto continua em fila). Nunca recusa: só diz o porquê. */
1696
1748
  const par = spawns.length > 1 ? await limites.paralelo() : null;
1697
- if (par) { const av = par.aviso(spawns.length); if (av && !avisouParalelo) { avisouParalelo = true; console.log(` ${av}`); } }
1749
+ if (par) { const av = par.aviso(spawns.length); if (av && !avisouParalelo) { avisouParalelo = true; ev('linha', { texto: ` ${av}` }); } }
1698
1750
  if (spawns.length > 1 && par.max > 1 && !PARAR) {
1699
1751
  const juntos = spawns.slice(0, par.max);
1700
- fala.paralelo(juntos.length, profundidade);
1752
+ ev('paralelo', { quantos: juntos.length });
1701
1753
  const ids = juntos.map((tc) => anotar(tc, 'permitida'));
1702
1754
  const rs = await Promise.all(juntos.map((tc) =>
1703
1755
  runSubagent(tc.arguments || {}, {
1704
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1756
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir,
1705
1757
  }).catch((e) => `Subagente falhou: ${e && e.message}`)));
1706
1758
  juntos.forEach((tc, i) => { resultadosSpawn.set(tc.id, rs[i]); auditoria.depois(ids[i], { ok: true }); });
1707
1759
  }
@@ -1824,7 +1876,7 @@ async function runActInterno(userPrompt, opts) {
1824
1876
  usuário (o bin). Sem a resposta dele, não troca. */
1825
1877
  if (tc.name === 'mudar_pasta') {
1826
1878
  algoRodou = true;
1827
- fala.acao(tc.name, tc.arguments, profundidade);
1879
+ ev('acao', { nome: tc.name, args: tc.arguments });
1828
1880
  const idA = anotar(tc, 'permitida');
1829
1881
  const r = await require('../../outra-pasta.js').pedirTroca(tc.arguments || {}, {
1830
1882
  atual: pastaAtual, subagent, trocar: mudarPasta,
@@ -1834,7 +1886,7 @@ async function runActInterno(userPrompt, opts) {
1834
1886
  criarPasta = null;
1835
1887
  temPasta = true;
1836
1888
  }
1837
- logResult(tc.name, r, profundidade);
1889
+ ev('resultado', { nome: tc.name, resultado: r });
1838
1890
  auditoria.depois(idA, r);
1839
1891
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1840
1892
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
@@ -1847,7 +1899,7 @@ async function runActInterno(userPrompt, opts) {
1847
1899
  const r = resultadosSpawn.has(tc.id)
1848
1900
  ? resultadosSpawn.get(tc.id)
1849
1901
  : await runSubagent(tc.arguments || {}, {
1850
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1902
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir,
1851
1903
  });
1852
1904
  if (idA) auditoria.depois(idA, { ok: true });
1853
1905
  repeticao.executou(jaChamado, tc.name, tc.arguments);
@@ -1859,12 +1911,12 @@ async function runActInterno(userPrompt, opts) {
1859
1911
  nova, e quem sabe abrir conversa é este arquivo. */
1860
1912
  if (tc.name === 'planejar_producao') {
1861
1913
  algoRodou = true;
1862
- fala.acao(tc.name, tc.arguments, profundidade);
1914
+ ev('acao', { nome: tc.name, args: tc.arguments });
1863
1915
  const idA = anotar(tc, 'permitida');
1864
1916
  const r = await rodarDiretor(tc.arguments || {}, {
1865
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1917
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir,
1866
1918
  });
1867
- logResult(tc.name, r, profundidade);
1919
+ ev('resultado', { nome: tc.name, resultado: r });
1868
1920
  auditoria.depois(idA, r);
1869
1921
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
1870
1922
  continue;
@@ -1879,12 +1931,12 @@ async function runActInterno(userPrompt, opts) {
1879
1931
  if (tc.name === 'construir_app') {
1880
1932
  algoRodou = true;
1881
1933
  garantirPasta();
1882
- fala.acao(tc.name, tc.arguments, profundidade);
1934
+ ev('acao', { nome: tc.name, args: tc.arguments });
1883
1935
  const idA = anotar(tc, 'permitida');
1884
1936
  const r = await rodarPipeline(tc.arguments || {}, {
1885
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, livro, autorizar, controle: portaoControle,
1937
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir, livro, autorizar, controle: portaoControle,
1886
1938
  });
1887
- logResult(tc.name, r, profundidade);
1939
+ ev('resultado', { nome: tc.name, resultado: r });
1888
1940
  auditoria.depois(idA, r);
1889
1941
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1890
1942
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
@@ -1957,8 +2009,8 @@ async function runActInterno(userPrompt, opts) {
1957
2009
  igual = !!e && voltas - e.volta <= 1 && require('fs').readFileSync(abs, 'utf8') === e.texto;
1958
2010
  } catch { igual = false; }
1959
2011
  if (igual) {
1960
- fala.acao(tc.name, tc.arguments, profundidade);
1961
- console.log(fala.resultado(profundidade) + c.muted('igual ao que ele escreveu — não precisou ler'));
2012
+ ev('acao', { nome: tc.name, args: tc.arguments });
2013
+ ev('resultado-linha', { texto: 'igual ao que ele escreveu — não precisou ler' });
1962
2014
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({
1963
2015
  ok: true, igualAoQueVoceEscreveu: true,
1964
2016
  aviso: 'Este arquivo está EXATAMENTE como você o escreveu nesta tarefa — o conteúdo é o da sua '
@@ -1990,8 +2042,8 @@ async function runActInterno(userPrompt, opts) {
1990
2042
  const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
1991
2043
  if (!porta.ok) {
1992
2044
  anotar(tc, 'recusada', porta.error);
1993
- fala.acao(tc.name, tc.arguments, profundidade);
1994
- console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
2045
+ ev('acao', { nome: tc.name, args: tc.arguments });
2046
+ ev('resultado-linha', { texto: 'não autorizado', nivel: 'aviso' });
1995
2047
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
1996
2048
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
1997
2049
  continue;
@@ -2008,8 +2060,8 @@ async function runActInterno(userPrompt, opts) {
2008
2060
  });
2009
2061
  if (!risco.ok) {
2010
2062
  anotar(tc, 'recusada', risco.motivo || risco.error);
2011
- fala.acao(tc.name, tc.arguments, profundidade);
2012
- console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
2063
+ ev('acao', { nome: tc.name, args: tc.arguments });
2064
+ ev('resultado-linha', { texto: `não autorizado — ${risco.motivo}`, nivel: 'aviso' });
2013
2065
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
2014
2066
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
2015
2067
  continue;
@@ -2019,8 +2071,8 @@ async function runActInterno(userPrompt, opts) {
2019
2071
  const gancho = hooks.rodar('PreToolUse', { nome: tc.name, args: tc.arguments, projectDir: pastaAtual });
2020
2072
  if (gancho.bloqueado) {
2021
2073
  anotar(tc, 'recusada', gancho.motivo);
2022
- fala.acao(tc.name, tc.arguments, profundidade);
2023
- console.log(fala.resultado(profundidade) + c.warn(`bloqueado por hook — ${gancho.motivo.slice(0, 80)}`));
2074
+ ev('acao', { nome: tc.name, args: tc.arguments });
2075
+ ev('resultado-linha', { texto: `bloqueado por hook — ${gancho.motivo.slice(0, 80)}`, nivel: 'aviso' });
2024
2076
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: gancho.motivo });
2025
2077
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: `Bloqueado por hook: ${gancho.motivo}` }) });
2026
2078
  continue;
@@ -2031,13 +2083,13 @@ async function runActInterno(userPrompt, opts) {
2031
2083
  if (['run_shell', 'run_in_new_terminal', 'run_background'].includes(tc.name) && perigo.apagaBancoNoComando((tc.arguments || {}).command)) {
2032
2084
  try {
2033
2085
  const copia = require('../../backup-banco.js').fazer(pastaAtual);
2034
- if (copia) console.log(c.ok(' ⎿ fiz uma cópia do banco antes') + c.dim(` (${copia.arquivos.join(', ')}) · /undo devolve`));
2086
+ if (copia) ev('linha', { texto: ` ⎿ fiz uma cópia do banco antes (${copia.arquivos.join(', ')}) · /undo devolve`, nivel: 'ok' });
2035
2087
  } catch { /* sem cópia o comando segue: ela é rede de segurança, não condição */ }
2036
2088
  }
2037
2089
 
2038
2090
  algoRodou = true;
2039
2091
  rodouTool = true;
2040
- fala.acao(tc.name, tc.arguments, profundidade);
2092
+ ev('acao', { nome: tc.name, args: tc.arguments });
2041
2093
  /* Antes de mexer no arquivo, guarda o original — é o que o /desfazer
2042
2094
  devolve. Caminho inválido não é problema nosso: a tool recusa. */
2043
2095
  let antesDaEdicao = null, arquivoDaEdicao = null; // pro alarme do arquivo que encolheu (lib/session/encolheu.js)
@@ -2052,7 +2104,7 @@ async function runActInterno(userPrompt, opts) {
2052
2104
  catch (e) {
2053
2105
  // Caminho inválido a tool recusa sozinha. O resto (disco cheio, sem permissão)
2054
2106
  // quer dizer que o /desfazer não vai devolver este arquivo: a pessoa tem que saber.
2055
- if (tc.arguments && tc.arguments.path) console.log(c.warn(` ⚠ não guardei o original de ${tc.arguments.path}: o /undo não vai voltar este arquivo (${e.message})`));
2107
+ if (tc.arguments && tc.arguments.path) ev('linha', { texto: ` ⚠ não guardei o original de ${tc.arguments.path}: o /undo não vai voltar este arquivo (${e.message})`, nivel: 'aviso' });
2056
2108
  }
2057
2109
  }
2058
2110
  /* O servidor, o token e o cliente da API vão junto: a legenda do
@@ -2061,7 +2113,9 @@ async function runActInterno(userPrompt, opts) {
2061
2113
  traco(`executando ${tc.name}`);
2062
2114
  const idAcao = idsAdiantados.get(tc.id) || anotar(tc, 'permitida');
2063
2115
  const result = adiantadas.has(tc.id) ? adiantadas.get(tc.id)
2064
- : await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api, escolher: subagent ? undefined : escolher });
2116
+ : await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api, escolher: subagent ? undefined : escolher,
2117
+ // estudio_criar conta o andamento: a tela desenha a barra do item `estudio`.
2118
+ progresso: tc.name === 'estudio_criar' ? (pct, etapa) => ev('estudio-progresso', { pct, etapa }) : undefined });
2065
2119
  auditoria.depois(idAcao, result);
2066
2120
  if (sinais && result && result.ok && /^(write_file|edit_file|multi_edit)$/.test(tc.name)) sinais.diagSujo = !!result.diagnosticos; // diagnóstico depois da edição
2067
2121
  if (antesDaEdicao != null && result && result.ok !== false) {
@@ -2069,19 +2123,21 @@ async function runActInterno(userPrompt, opts) {
2069
2123
  try { depois = require('fs').readFileSync(arquivoDaEdicao, 'utf8'); } catch { depois = null; }
2070
2124
  const encolheu = require('../encolheu.js').julgar(antesDaEdicao, depois);
2071
2125
  if (encolheu) {
2072
- console.log(c.warn(` ⚠ ${tc.arguments.path}: ${encolheu.texto}. Se sumiu código sem querer, /undo volta.`));
2126
+ ev('linha', { texto: ` ⚠ ${tc.arguments.path}: ${encolheu.texto}. Se sumiu código sem querer, /undo volta.`, nivel: 'aviso' });
2073
2127
  result.aviso_encolheu = `ATENÇÃO: ${tc.arguments.path} ${encolheu.texto}. Confira se você não apagou código sem querer; se apagou, refaça a edição sem cortar nada.`;
2074
2128
  }
2075
2129
  }
2076
- for (const aviso of hooks.rodar('PostToolUse', { nome: tc.name, args: tc.arguments, resultado: result, projectDir: pastaAtual }).avisos) console.log(c.dim(` ⎿ ${aviso}`));
2130
+ for (const aviso of hooks.rodar('PostToolUse', { nome: tc.name, args: tc.arguments, resultado: result, projectDir: pastaAtual }).avisos) ev('linha', { texto: ` ⎿ ${aviso}`, nivel: 'dim' });
2077
2131
  traco(`${tc.name} devolveu ok=${result && result.ok}`);
2078
2132
  if (/^estudio_|^planejar_producao$/.test(tc.name) && result && result.ok !== false) {
2079
2133
  const id = result.id || (result.projeto && result.projeto.id) || (tc.arguments && tc.arguments.id);
2080
2134
  if (typeof id === 'string' && id) estudioDaTarefa = id;
2081
2135
  }
2082
- logResult(tc.name, result, profundidade);
2136
+ ev('resultado', { nome: tc.name, resultado: result });
2137
+ // Render é lento e roda solto: ao fim, o sino avisa (vira notice/created no app).
2138
+ if (tc.name === 'estudio_criar' && result && result.ok !== false && result.arquivo) ev('aviso', { texto: `${result.resumo || 'Pronto'} ${require('path').basename(result.arquivo)}` });
2083
2139
  if (/^estudio_render/.test(tc.name) && result && result.conferencia && !subagent) {
2084
- for (const l of require('../../plugin/estudio/conferencia.js').linhasPraPessoa(result.conferencia)) console.log(c.warn(` ⎿ ${l}`));
2140
+ for (const l of require('../../plugin/estudio/conferencia.js').linhasPraPessoa(result.conferencia)) ev('linha', { texto: ` ⎿ ${l}`, nivel: 'aviso' });
2085
2141
  }
2086
2142
  repeticao.executou(jaChamado, tc.name, tc.arguments);
2087
2143
  if (RELEITURA.has(tc.name) && result && result.ok !== false) {
@@ -2202,7 +2258,7 @@ async function runActInterno(userPrompt, opts) {
2202
2258
 
2203
2259
  if (finished) {
2204
2260
  finalText = finished;
2205
- if (!subagent) fala.resposta(finished);
2261
+ if (!subagent) ev('resposta', { texto: finished });
2206
2262
  // Recado chegou enquanto ele fechava: entrega e segue.
2207
2263
  if (!subagent && CAIXA.length && !PARAR) { finalText = ''; continue; }
2208
2264
  break;
@@ -2220,14 +2276,14 @@ async function runActInterno(userPrompt, opts) {
2220
2276
  let passagemDaParada = null;
2221
2277
  if (paradoPorTravar && !finalText) {
2222
2278
  finalText = paradoPorTravar;
2223
- if (!subagent) fala.aviso(finalText);
2279
+ if (!subagent) ev('aviso', { texto: finalText });
2224
2280
  // Parou sem terminar: a memória leva o estado, não só "parei" (lib/passagem.js).
2225
2281
  passagemDaParada = require('../../passagem').montar({ pedido: userPrompt, motivo: finalText, registro: livro, falhasPendentes });
2226
2282
  }
2227
2283
 
2228
2284
  if (PARAR && !finalText) {
2229
2285
  finalText = 'Parado por você (Esc Esc). O que já foi feito, ficou feito.';
2230
- if (!subagent) fala.aviso(finalText);
2286
+ if (!subagent) ev('aviso', { texto: finalText });
2231
2287
  }
2232
2288
 
2233
2289
  // Nunca terminar em silêncio. Quando o modelo devolvia resposta vazia sem
@@ -2236,7 +2292,7 @@ async function runActInterno(userPrompt, opts) {
2236
2292
  // depois de a primeira tentativa de seletor falhar.
2237
2293
  if (!finalText) {
2238
2294
  finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS, teto });
2239
- if (!subagent) fala.aviso(finalText);
2295
+ if (!subagent) ev('aviso', { texto: finalText });
2240
2296
  passagemDaParada = require('../../passagem').montar({ pedido: userPrompt, motivo: finalText, registro: livro, falhasPendentes });
2241
2297
  }
2242
2298
 
@@ -2250,7 +2306,7 @@ async function runActInterno(userPrompt, opts) {
2250
2306
  // arquivo de verdade — a memória era descartada nesse caso, sempre.
2251
2307
  // A raiz de trabalho não é projeto: um memory.md solto em ~/primocode
2252
2308
  // era o que um pedido de vídeo (que trabalha na raiz) deixava para trás.
2253
- if (!subagent && temPasta && require('path').resolve(pastaAtual) !== require('path').resolve(cfg.ROOT_DIR)) {
2309
+ if (!subagent && temPasta && pastaAtual && require('path').resolve(pastaAtual) !== require('path').resolve(cfg.ROOT_DIR)) {
2254
2310
  cfg.appendProjectMemory(pastaAtual, passagemDaParada || `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
2255
2311
  }
2256
2312
 
@@ -2299,11 +2355,13 @@ async function runSubagent(args, ctx) {
2299
2355
  // O ofício: o modelo diz quem faz (`especialista`), ou a tarefa sugere. O
2300
2356
  // briefing vai na frente da tarefa, na mesma mensagem — ver especialistas.js.
2301
2357
  // Novo catálogo (agentes/): "id" ou "id1,id2" (até 3 olhares na mesma tarefa); o que não existe cai no legado.
2302
- const novos = require('../../agentes').refinar(args.especialista, args.prompt); // o catálogo confere o chute do modelo
2303
- const esp = novos.length ? null : especialistas.escolher(args.especialista, args.prompt);
2358
+ // `agente`: um agente da pessoa (~/.primocode/agentes, lib/agentes-pessoais.js) ganha do catálogo e do legado.
2359
+ const meu = args.agente ? require('../../agentes-pessoais').ler(args.agente) : null;
2360
+ const novos = meu ? [] : require('../../agentes').refinar(args.especialista, args.prompt); // o catálogo confere o chute do modelo
2361
+ const esp = meu ? require('../../agentes-pessoais').comoEspecialista(meu) : novos.length ? null : especialistas.escolher(args.especialista, args.prompt);
2304
2362
  const agentes = require('../../agentes');
2305
- const rot = novos.length ? agentes.rotulo(novos) : especialistas.rotulo(esp);
2306
- fala.agente(rot, descricao, profundidade);
2363
+ const rot = meu ? meu.nome : novos.length ? agentes.rotulo(novos) : especialistas.rotulo(esp);
2364
+ telaDe(ctx, 'agente', { rotulo: rot, descricao });
2307
2365
 
2308
2366
  // modelo_sugerido do briefing vale quando o placar ainda não tem dado desse tipo de tarefa.
2309
2367
  let modeloDoAgente = model;
@@ -2322,6 +2380,7 @@ async function runSubagent(args, ctx) {
2322
2380
  subagent: true,
2323
2381
  motor,
2324
2382
  permissoes,
2383
+ emitir: emitirDe(ctx),
2325
2384
  autorizar: ctx.autorizar,
2326
2385
  controle: ctx.controle, // o mesmo objeto: a resposta vale para todos
2327
2386
  rotulo: `subagente: ${String(descricao).slice(0, 60)}`,
@@ -2334,7 +2393,7 @@ async function runSubagent(args, ctx) {
2334
2393
  if (ctx.livro && r.registro) registro.juntar(ctx.livro, r.registro);
2335
2394
 
2336
2395
  const resumo = r.assistantMessage.content || 'sem resultado';
2337
- fala.agenteVoltou(resumo, profundidade);
2396
+ telaDe(ctx, 'agente-voltou', { texto: resumo });
2338
2397
  return entregaDoAgente(rot, resumo, r.registro, { pasta: projectDir });
2339
2398
  }
2340
2399
 
@@ -2417,10 +2476,10 @@ async function rodarDiretor(args, ctx) {
2417
2476
  const diretor = require('../../plugin/estudio/diretor.js');
2418
2477
  const correr = async (prompt, rotulo) => {
2419
2478
  if (PARAR) return '';
2420
- fala.agente('Diretor', rotulo, profundidade);
2479
+ telaDe(ctx, 'agente', { rotulo: 'Diretor', descricao: rotulo });
2421
2480
  const r = await runAct(prompt, {
2422
2481
  server, model, token, projectDir, projectName,
2423
- history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
2482
+ history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes, emitir: emitirDe(ctx),
2424
2483
  });
2425
2484
  return (r && r.assistantMessage && r.assistantMessage.content) || '';
2426
2485
  };
@@ -2431,7 +2490,7 @@ async function rodarDiretor(args, ctx) {
2431
2490
  formato: args.formato === 'apresentacao' ? 'apresentacao' : 'video',
2432
2491
  contexto: args.contexto || '',
2433
2492
  correr,
2434
- mostrar: (t) => fala.agente('Diretor', t, profundidade),
2493
+ mostrar: (t) => telaDe(ctx, 'agente', { rotulo: 'Diretor', descricao: t }),
2435
2494
  });
2436
2495
  if (!r.ok) return r;
2437
2496
 
@@ -2473,10 +2532,10 @@ async function rodarPipeline(args, ctx) {
2473
2532
 
2474
2533
  const correr = async (prompt, rotulo) => {
2475
2534
  if (PARAR) return '';
2476
- fala.agente('Agente', rotulo, profundidade);
2535
+ telaDe(ctx, 'agente', { rotulo: 'Agente', descricao: rotulo });
2477
2536
  const r = await runAct(prompt, {
2478
2537
  server, model, token, projectDir: pasta, projectName,
2479
- history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
2538
+ history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes, emitir: emitirDe(ctx),
2480
2539
  });
2481
2540
  return (r && r.assistantMessage && r.assistantMessage.content) || '';
2482
2541
  };
@@ -2486,7 +2545,7 @@ async function rodarPipeline(args, ctx) {
2486
2545
  pasta,
2487
2546
  rodadas: args.rodadas,
2488
2547
  correr,
2489
- mostrar: (linha) => console.log(fala.resultado(profundidade) + c.muted(linha)),
2548
+ mostrar: (linha) => telaDe(ctx, 'resultado-linha', { texto: linha }),
2490
2549
  abrir: tools.openInBrowser,
2491
2550
  });
2492
2551
  }
@@ -2518,5 +2577,5 @@ function novoPortaoDeProva() {
2518
2577
  };
2519
2578
  }
2520
2579
 
2521
- module.exports = { summarizeForModel, mesmoComando, EDICAO_DE_VIDEO_FALADO, novoPortaoDeProva, runAct, erroDaCorrente, linhaDaReserva, videoForaDeEdicao, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, interrompido, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
2580
+ module.exports = { summarizeForModel, mesmoComando, EDICAO_DE_VIDEO_FALADO, novoPortaoDeProva, runAct, logResult, interrompido: () => PARAR, erroDaCorrente, linhaDaReserva, videoForaDeEdicao, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, interrompido, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
2522
2581
  anotacaoDoQueJaFoi, resumoDoQueJaFoi, encolherParaContinuar, recadoDoPlanoAberto, MAX_ITERATIONS };
@@ -82,6 +82,8 @@ const GRUPOS = [
82
82
  nome: 'Estúdio',
83
83
  resumo: 'vídeo, motion, slides e arte — a IA faz, e no fim abre o resultado pronto',
84
84
  itens: [
85
+ { tool: 'estudio_criar', escreve: true, faz: 'a porta simples: cria slides, vídeo, motion, anúncio ou abertura do pedido ao arquivo, e abre o resultado',
86
+ args: 'tipo (slides|video|motion|anuncio|abertura). briefing (uma frase). duracao (s, opcional). formato (9:16|16:9|1:1, opcional). Para controle fino use as outras estudio_*.' },
85
87
  { tool: 'estudio_projeto', escreve: true, faz: 'abre, acha e lista os projetos do estúdio — cada peça é uma pasta que não some',
86
88
  args: 'acao: criar | abrir | procurar | listar | anotar | editar | assinatura. '
87
89
  + 'assinatura: quando o usuário diz o que achou de um vídeo ("adorei a virada", "texto demais"), grave com texto (e o id) — o próximo vídeo parte disso; sem texto, lê as faixas e as notas. '
@@ -282,7 +284,7 @@ const GRUPOS = [
282
284
  + 'Para só LER lá não precisa trocar: read_file/list_dir com o caminho absoluto, glob/grep com padrão absoluto.' },
283
285
  // Estas duas o loop do agente atende (lib/session/runner/act.js), não o tools.execute.
284
286
  { tool: 'spawn_agent', escreve: false, faz: 'chama um subagente especialista para uma parte da tarefa', noLoop: true,
285
- args: 'description: 3 a 5 palavras, aparece na tela do usuário. prompt: instrução completa e autossuficiente — o subagente não vê esta conversa. especialista (opcional): id EXATO que agentes_buscar devolveu — não chute o nome (o mais específico vence: css-layout, pagamentos, kubernetes, regex…, não frontend/backend/devops) ou dois separados por vírgula — cada um chega com o briefing da profissão (lib/skill/especialistas.js); sem ele, a tarefa sugere.' },
287
+ args: 'description: 3 a 5 palavras, aparece na tela do usuário. prompt: instrução completa e autossuficiente — o subagente não vê esta conversa. especialista (opcional): id EXATO que agentes_buscar devolveu — não chute o nome (o mais específico vence: css-layout, pagamentos, kubernetes, regex…, não frontend/backend/devops) ou dois separados por vírgula — cada um chega com o briefing da profissão (lib/skill/especialistas.js); sem ele, a tarefa sugere. agente (opcional): nome de um agente da pessoa (@nome); ganha do especialista.' },
286
288
  { tool: 'construir_app', escreve: true, noLoop: true,
287
289
  faz: 'constrói um app ou site inteiro com quatro papéis: engenheiro de prompt → protótipo → MVP → revisor que testa escondido, em laço até passar',
288
290
  args: 'pedido: o que o usuário quer, com as palavras dele. pasta (opcional): onde construir — sem ela, uma pasta nova com o nome do pedido. rodadas (opcional, 1 a 4, padrão 2): quantas voltas de correção. '
package/lib/tool/tools.js CHANGED
@@ -688,6 +688,7 @@ async function despachar(name, args, ctx) {
688
688
  ok: true, created,
689
689
  path: path.relative(projectDir, filePath).replace(/\\/g, '/'),
690
690
  bytes: content.length,
691
+ linhas: content.split('\n').length,
691
692
  ...(created ? {} : { _antes: antes, _depois: content }),
692
693
  };
693
694
  }
@@ -943,6 +944,12 @@ async function despachar(name, args, ctx) {
943
944
  return estudio.ferramentaMidia(args);
944
945
  case 'estudio_ae':
945
946
  return estudio.ferramentaAE(args);
947
+ case 'estudio_criar':
948
+ // A porta simples: abre o projeto e chama plano, motion e render por dentro. ctx.progresso alimenta a tela.
949
+ return estudio.ferramentaCriar(args, {
950
+ ...ctx, projectDir,
951
+ abrir: (arquivo) => { try { openInBrowser(arquivo); } catch {} },
952
+ });
946
953
  case 'estudio_motion':
947
954
  return estudio.ferramentaMotion(args);
948
955
  case 'estudio_editar':