primocode 11.1.0 → 11.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.
@@ -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'
@@ -258,6 +263,14 @@ function limparParaOModelo(result) {
258
263
  return limpo;
259
264
  }
260
265
 
266
+ /* Os eventos do subagente, do diretor e do pipeline vão para o mesmo emissor
267
+ do pedido principal (vem no ctx). Sem ele, uma tela de terminal nova. */
268
+ function emitirDe(ctx) {
269
+ if (!ctx.emitir) ctx.emitir = require('../../motor/tela-terminal.js').criar();
270
+ return ctx.emitir;
271
+ }
272
+ const telaDe = (ctx, vista, dados) => emitirDe(ctx)(eventosMotor.montar(vista, { profundidade: ctx.profundidade, ...dados }));
273
+
261
274
  function logResult(kind, result, profundidade) {
262
275
  const p = fala.resultado(profundidade);
263
276
  if (!result) return;
@@ -920,7 +933,28 @@ async function runActInterno(userPrompt, opts) {
920
933
  : () => require('../../browser.js').aquecerRevisor(),
921
934
  /* Quem é este agente no livro de ações (auditoria.js). */
922
935
  rotulo = subagent ? 'subagente' : 'principal',
936
+ /* Para onde vão os eventos do turno (lib/motor/eventos.js). O motor
937
+ não escreve na tela: quem desenha assina. Sem ninguém de fora, nasce
938
+ uma tela de terminal (lib/motor/tela-terminal.js) — a saída de sempre. */
939
+ emitir: emitirDeFora = null,
923
940
  } = opts || {};
941
+ const emitir = emitirDeFora || require('../../motor/tela-terminal.js').criar();
942
+ const ev = (vista, dados) => emitir(eventosMotor.montar(vista, { profundidade, ...dados }));
943
+ /* Spinner por evento: a tela decide se gira (terminal) ou se mostra "trabalhando" (app). */
944
+ const girar = (rotulo) => {
945
+ const id = eventosMotor.novoId();
946
+ ev('spinner-inicia', { id, rotulo });
947
+ return {
948
+ set detalhe(v) { ev('spinner-detalhe', { id, detalhe: v }); },
949
+ stop() { ev('spinner-para', { id }); },
950
+ };
951
+ };
952
+ /* O texto que corre na tela conforme chega: mesma interface do fala.fluxo(). */
953
+ const abrirFluxo = (texto) => {
954
+ const id = eventosMotor.novoId();
955
+ ev('fluxo-inicia', { id, texto });
956
+ return { escrever: (t) => ev('fluxo-escreve', { id, texto: t }), fechar: () => ev('fluxo-fecha', { id }) };
957
+ };
924
958
 
925
959
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
926
960
  // Antes ela nascia na primeira tool qualquer — pedir "abre o Chrome" criava
@@ -968,7 +1002,7 @@ async function runActInterno(userPrompt, opts) {
968
1002
  const t = Date.now();
969
1003
  // Com spinner: eram segundos de tela parada entre o último arquivo e o
970
1004
  // "pronto", e parecia que tinha travado ali.
971
- const sp = new Spinner('abrindo a página escondido para conferir').start();
1005
+ const sp = girar('abrindo a página escondido para conferir');
972
1006
  try {
973
1007
  medida = await medirPagina(pastaAtual);
974
1008
  } catch { medida = null; } finally { sp.stop(); }
@@ -977,7 +1011,7 @@ async function runActInterno(userPrompt, opts) {
977
1011
  if (!medida || !medida.testou) return null;
978
1012
  avisoDaPagina = medida.limpo ? ''
979
1013
  : 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; }
1014
+ if (medida.limpo) { ev('pensamento', { texto: 'a página abriu sem erro' }); return null; }
981
1015
  return conferencias >= 2 ? null : medida.relato;
982
1016
  };
983
1017
  /* ── RODAR OS TESTES ANTES DE DIZER "PRONTO" (lib/conferir.js) ──────
@@ -1000,7 +1034,7 @@ async function runActInterno(userPrompt, opts) {
1000
1034
  const achados = K.procurar([...arquivosDaTarefa], { raiz: pastaAtual });
1001
1035
  avisoDaChave = K.relato(achados);
1002
1036
  if (!avisoDaChave) return null;
1003
- console.log(c.warn(` ⚠ ${avisoDaChave}`));
1037
+ ev('linha', { texto: ` ⚠ ${avisoDaChave}`, nivel: 'aviso' });
1004
1038
  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
1039
  };
1006
1040
  const conferirTestes = async () => {
@@ -1022,14 +1056,14 @@ async function runActInterno(userPrompt, opts) {
1022
1056
  if (r.ok) {
1023
1057
  avisoDosTestes = '';
1024
1058
  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})`));
1059
+ ev('linha', { texto: ` ✓ ${ex.titulo} (${como.comando})`, nivel: 'ok' });
1026
1060
  try { checkpoint.marcarFuncionando(pastaAtual, `${como.comando}, pelo agente`); } catch { /* sem ponto pra marcar */ }
1027
1061
  return null;
1028
1062
  }
1029
1063
  avisoDosTestes = `${ex.titulo} ${ex.linhas.slice(0, 2).join(' ')}`.trim();
1030
1064
  // a frase que explica (a do Error:, se houver), não a do stack: é a que a pessoa entende
1031
1065
  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})`));
1066
+ ev('linha', { texto: ` ✗ ${ex.titulo} (${como.comando})`, nivel: 'aviso' });
1033
1067
  return rodadasDeTeste >= 2 ? null : `Rodei \`${como.comando}\` e falhou:\n${r.saida}`;
1034
1068
  };
1035
1069
  /* O ARQUIVO QUE O PEDIDO MANDA GRAVAR (lib/plugin/entregaveis.js): "grave o
@@ -1098,7 +1132,7 @@ async function runActInterno(userPrompt, opts) {
1098
1132
  if (decisao !== 'recusada' && decisao !== 'negada') require('../../plugin/voz-ponte').ferramenta(tc.name, tc.arguments);
1099
1133
  const id = auditoria.antes({ agente: rotulo, ferramenta: tc.name, args: tc.arguments, decisao, motivo });
1100
1134
  const falhou = auditoria.problema();
1101
- if (falhou) console.log(c.warn(`⚠ ${falhou}`));
1135
+ if (falhou) ev('linha', { texto: `⚠ ${falhou}`, nivel: 'aviso' });
1102
1136
  return id;
1103
1137
  };
1104
1138
  // O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
@@ -1339,10 +1373,10 @@ async function runActInterno(userPrompt, opts) {
1339
1373
  }
1340
1374
  // Recado digitado durante o passo anterior entra AGORA, antes da ida
1341
1375
  // ao modelo — o resultado das ferramentas já está na conversa.
1342
- if (!subagent) entregarRecados(messages, profundidade);
1376
+ if (!subagent) entregarRecados(messages, profundidade, emitir);
1343
1377
  // Tarefa em segundo plano que acabou desde a última volta (lib/fundo.js): o modelo fica sabendo.
1344
1378
  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();
1379
+ const sp = girar(subagent ? 'subagente trabalhando' : PENSANDO);
1346
1380
 
1347
1381
  const toolCalls = [];
1348
1382
  let assistantText = '';
@@ -1375,7 +1409,7 @@ async function runActInterno(userPrompt, opts) {
1375
1409
  resumir: ({ resumo, mensagens }, todos) => comp.resumirComFatos({ resumo, mensagens },
1376
1410
  (p) => motor.requestResumo(server, { ...p, token }).then((x) => x && x.resumo), todos),
1377
1411
  });
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)`));
1412
+ 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
1413
  }
1380
1414
  } catch { /* resumo é economia, não requisito */ }
1381
1415
  }
@@ -1396,8 +1430,7 @@ async function runActInterno(userPrompt, opts) {
1396
1430
  if (!subagent && !toolCalls.length) {
1397
1431
  if (!fluxo && assistantText.length > fala.LIMIAR_PENSAMENTO) {
1398
1432
  sp.stop();
1399
- fluxo = fala.fluxo();
1400
- fluxo.escrever(assistantText);
1433
+ fluxo = abrirFluxo(assistantText);
1401
1434
  } else if (fluxo) fluxo.escrever(evt.delta);
1402
1435
  }
1403
1436
  }
@@ -1429,14 +1462,14 @@ async function runActInterno(userPrompt, opts) {
1429
1462
  }
1430
1463
  }
1431
1464
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${fala.frase(e)}` } };
1432
- fala.problema(e);
1465
+ ev('problema', { erro: e });
1433
1466
  throw e;
1434
1467
  }
1435
1468
  sp.stop();
1436
1469
  emVoo = null;
1437
1470
  if (!subagent && !avisouReserva && modeloDaVolta && modeloDaVolta.reserva) {
1438
1471
  avisouReserva = true;
1439
- console.log(c.dim(` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`));
1472
+ ev('linha', { texto: ` ⎿ ${linhaDaReserva(modeloDaVolta.modelo)}`, nivel: 'dim' });
1440
1473
  }
1441
1474
  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
1475
  tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
@@ -1519,7 +1552,7 @@ async function runActInterno(userPrompt, opts) {
1519
1552
  if (fluxo) fluxo.fechar();
1520
1553
  const amigavel = fala.frase(streamError);
1521
1554
  if (subagent) return { assistantMessage: { role: 'assistant', content: `Subagente falhou: ${amigavel}` }, concluiu: false };
1522
- fala.problema(streamError);
1555
+ ev('problema', { erro: streamError });
1523
1556
  // concluiu: false — o plano de passos não pode tratar um erro de
1524
1557
  // provedor como resultado e passá-lo adiante como "fato".
1525
1558
  return { assistantMessage: { role: 'assistant', content: amigavel }, concluiu: false };
@@ -1561,7 +1594,7 @@ async function runActInterno(userPrompt, opts) {
1561
1594
  if (pendentes) cobrouPlano = true; // este empurrão já cobrou o plano aberto
1562
1595
  if (!subagent) {
1563
1596
  if (fluxo) fluxo.fechar();
1564
- else fala.pensamento(finalText, profundidade);
1597
+ else ev('pensamento', { texto: finalText });
1565
1598
  }
1566
1599
  messages.push({ role: 'assistant', content: finalText });
1567
1600
  messages.push({
@@ -1582,7 +1615,7 @@ async function runActInterno(userPrompt, opts) {
1582
1615
  try { const K = require('../../plugin/kits.js'); if (K.continuaVideo(userPrompt)) falta = K.batidasFaltando(pastaAtual); } catch { /* sem kits */ }
1583
1616
  if (falta.length) {
1584
1617
  empurroesDoKit++;
1585
- if (fluxo) fluxo.fechar(); else fala.pensamento(finalText, profundidade);
1618
+ if (fluxo) fluxo.fechar(); else ev('pensamento', { texto: finalText });
1586
1619
  messages.push({ role: 'assistant', content: finalText });
1587
1620
  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
1621
  finalText = '';
@@ -1645,7 +1678,7 @@ async function runActInterno(userPrompt, opts) {
1645
1678
  }
1646
1679
  if (!subagent) {
1647
1680
  if (fluxo) fluxo.fechar();
1648
- else fala.resposta(finalText);
1681
+ else ev('resposta', { texto: finalText });
1649
1682
  }
1650
1683
  /* Ia terminar, mas chegou recado no meio: a tarefa não acaba com
1651
1684
  a pessoa esperando resposta. A volta seguinte entrega o recado. */
@@ -1661,7 +1694,7 @@ async function runActInterno(userPrompt, opts) {
1661
1694
  // O que o modelo disse ANTES de agir é pensamento, e sai como tal.
1662
1695
  if (!subagent) {
1663
1696
  if (fluxo) fluxo.fechar();
1664
- else if (assistantText.trim()) fala.pensamento(assistantText, profundidade);
1697
+ else if (assistantText.trim()) ev('pensamento', { texto: assistantText });
1665
1698
  }
1666
1699
 
1667
1700
  // Registra o turno do assistente COM os tool_calls. A API exige que
@@ -1694,14 +1727,14 @@ async function runActInterno(userPrompt, opts) {
1694
1727
  /* O plano decide quantos subagentes rodam JUNTOS (lib/config/limites.js; grátis e
1695
1728
  Primo: um de cada vez, o resto continua em fila). Nunca recusa: só diz o porquê. */
1696
1729
  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}`); } }
1730
+ if (par) { const av = par.aviso(spawns.length); if (av && !avisouParalelo) { avisouParalelo = true; ev('linha', { texto: ` ${av}` }); } }
1698
1731
  if (spawns.length > 1 && par.max > 1 && !PARAR) {
1699
1732
  const juntos = spawns.slice(0, par.max);
1700
- fala.paralelo(juntos.length, profundidade);
1733
+ ev('paralelo', { quantos: juntos.length });
1701
1734
  const ids = juntos.map((tc) => anotar(tc, 'permitida'));
1702
1735
  const rs = await Promise.all(juntos.map((tc) =>
1703
1736
  runSubagent(tc.arguments || {}, {
1704
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1737
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir,
1705
1738
  }).catch((e) => `Subagente falhou: ${e && e.message}`)));
1706
1739
  juntos.forEach((tc, i) => { resultadosSpawn.set(tc.id, rs[i]); auditoria.depois(ids[i], { ok: true }); });
1707
1740
  }
@@ -1824,7 +1857,7 @@ async function runActInterno(userPrompt, opts) {
1824
1857
  usuário (o bin). Sem a resposta dele, não troca. */
1825
1858
  if (tc.name === 'mudar_pasta') {
1826
1859
  algoRodou = true;
1827
- fala.acao(tc.name, tc.arguments, profundidade);
1860
+ ev('acao', { nome: tc.name, args: tc.arguments });
1828
1861
  const idA = anotar(tc, 'permitida');
1829
1862
  const r = await require('../../outra-pasta.js').pedirTroca(tc.arguments || {}, {
1830
1863
  atual: pastaAtual, subagent, trocar: mudarPasta,
@@ -1834,7 +1867,7 @@ async function runActInterno(userPrompt, opts) {
1834
1867
  criarPasta = null;
1835
1868
  temPasta = true;
1836
1869
  }
1837
- logResult(tc.name, r, profundidade);
1870
+ ev('resultado', { nome: tc.name, resultado: r });
1838
1871
  auditoria.depois(idA, r);
1839
1872
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1840
1873
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
@@ -1847,7 +1880,7 @@ async function runActInterno(userPrompt, opts) {
1847
1880
  const r = resultadosSpawn.has(tc.id)
1848
1881
  ? resultadosSpawn.get(tc.id)
1849
1882
  : await runSubagent(tc.arguments || {}, {
1850
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1883
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir,
1851
1884
  });
1852
1885
  if (idA) auditoria.depois(idA, { ok: true });
1853
1886
  repeticao.executou(jaChamado, tc.name, tc.arguments);
@@ -1859,12 +1892,12 @@ async function runActInterno(userPrompt, opts) {
1859
1892
  nova, e quem sabe abrir conversa é este arquivo. */
1860
1893
  if (tc.name === 'planejar_producao') {
1861
1894
  algoRodou = true;
1862
- fala.acao(tc.name, tc.arguments, profundidade);
1895
+ ev('acao', { nome: tc.name, args: tc.arguments });
1863
1896
  const idA = anotar(tc, 'permitida');
1864
1897
  const r = await rodarDiretor(tc.arguments || {}, {
1865
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1898
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir,
1866
1899
  });
1867
- logResult(tc.name, r, profundidade);
1900
+ ev('resultado', { nome: tc.name, resultado: r });
1868
1901
  auditoria.depois(idA, r);
1869
1902
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
1870
1903
  continue;
@@ -1879,12 +1912,12 @@ async function runActInterno(userPrompt, opts) {
1879
1912
  if (tc.name === 'construir_app') {
1880
1913
  algoRodou = true;
1881
1914
  garantirPasta();
1882
- fala.acao(tc.name, tc.arguments, profundidade);
1915
+ ev('acao', { nome: tc.name, args: tc.arguments });
1883
1916
  const idA = anotar(tc, 'permitida');
1884
1917
  const r = await rodarPipeline(tc.arguments || {}, {
1885
- server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, livro, autorizar, controle: portaoControle,
1918
+ server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, emitir, livro, autorizar, controle: portaoControle,
1886
1919
  });
1887
- logResult(tc.name, r, profundidade);
1920
+ ev('resultado', { nome: tc.name, resultado: r });
1888
1921
  auditoria.depois(idA, r);
1889
1922
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1890
1923
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
@@ -1957,8 +1990,8 @@ async function runActInterno(userPrompt, opts) {
1957
1990
  igual = !!e && voltas - e.volta <= 1 && require('fs').readFileSync(abs, 'utf8') === e.texto;
1958
1991
  } catch { igual = false; }
1959
1992
  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'));
1993
+ ev('acao', { nome: tc.name, args: tc.arguments });
1994
+ ev('resultado-linha', { texto: 'igual ao que ele escreveu — não precisou ler' });
1962
1995
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({
1963
1996
  ok: true, igualAoQueVoceEscreveu: true,
1964
1997
  aviso: 'Este arquivo está EXATAMENTE como você o escreveu nesta tarefa — o conteúdo é o da sua '
@@ -1990,8 +2023,8 @@ async function runActInterno(userPrompt, opts) {
1990
2023
  const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
1991
2024
  if (!porta.ok) {
1992
2025
  anotar(tc, 'recusada', porta.error);
1993
- fala.acao(tc.name, tc.arguments, profundidade);
1994
- console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
2026
+ ev('acao', { nome: tc.name, args: tc.arguments });
2027
+ ev('resultado-linha', { texto: 'não autorizado', nivel: 'aviso' });
1995
2028
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
1996
2029
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
1997
2030
  continue;
@@ -2008,8 +2041,8 @@ async function runActInterno(userPrompt, opts) {
2008
2041
  });
2009
2042
  if (!risco.ok) {
2010
2043
  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}`));
2044
+ ev('acao', { nome: tc.name, args: tc.arguments });
2045
+ ev('resultado-linha', { texto: `não autorizado — ${risco.motivo}`, nivel: 'aviso' });
2013
2046
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
2014
2047
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
2015
2048
  continue;
@@ -2019,8 +2052,8 @@ async function runActInterno(userPrompt, opts) {
2019
2052
  const gancho = hooks.rodar('PreToolUse', { nome: tc.name, args: tc.arguments, projectDir: pastaAtual });
2020
2053
  if (gancho.bloqueado) {
2021
2054
  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)}`));
2055
+ ev('acao', { nome: tc.name, args: tc.arguments });
2056
+ ev('resultado-linha', { texto: `bloqueado por hook — ${gancho.motivo.slice(0, 80)}`, nivel: 'aviso' });
2024
2057
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: gancho.motivo });
2025
2058
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: `Bloqueado por hook: ${gancho.motivo}` }) });
2026
2059
  continue;
@@ -2031,13 +2064,13 @@ async function runActInterno(userPrompt, opts) {
2031
2064
  if (['run_shell', 'run_in_new_terminal', 'run_background'].includes(tc.name) && perigo.apagaBancoNoComando((tc.arguments || {}).command)) {
2032
2065
  try {
2033
2066
  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`));
2067
+ if (copia) ev('linha', { texto: ` ⎿ fiz uma cópia do banco antes (${copia.arquivos.join(', ')}) · /undo devolve`, nivel: 'ok' });
2035
2068
  } catch { /* sem cópia o comando segue: ela é rede de segurança, não condição */ }
2036
2069
  }
2037
2070
 
2038
2071
  algoRodou = true;
2039
2072
  rodouTool = true;
2040
- fala.acao(tc.name, tc.arguments, profundidade);
2073
+ ev('acao', { nome: tc.name, args: tc.arguments });
2041
2074
  /* Antes de mexer no arquivo, guarda o original — é o que o /desfazer
2042
2075
  devolve. Caminho inválido não é problema nosso: a tool recusa. */
2043
2076
  let antesDaEdicao = null, arquivoDaEdicao = null; // pro alarme do arquivo que encolheu (lib/session/encolheu.js)
@@ -2052,7 +2085,7 @@ async function runActInterno(userPrompt, opts) {
2052
2085
  catch (e) {
2053
2086
  // Caminho inválido a tool recusa sozinha. O resto (disco cheio, sem permissão)
2054
2087
  // 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})`));
2088
+ 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
2089
  }
2057
2090
  }
2058
2091
  /* O servidor, o token e o cliente da API vão junto: a legenda do
@@ -2061,7 +2094,9 @@ async function runActInterno(userPrompt, opts) {
2061
2094
  traco(`executando ${tc.name}`);
2062
2095
  const idAcao = idsAdiantados.get(tc.id) || anotar(tc, 'permitida');
2063
2096
  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 });
2097
+ : await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api, escolher: subagent ? undefined : escolher,
2098
+ // estudio_criar conta o andamento: a tela desenha a barra do item `estudio`.
2099
+ progresso: tc.name === 'estudio_criar' ? (pct, etapa) => ev('estudio-progresso', { pct, etapa }) : undefined });
2065
2100
  auditoria.depois(idAcao, result);
2066
2101
  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
2102
  if (antesDaEdicao != null && result && result.ok !== false) {
@@ -2069,19 +2104,21 @@ async function runActInterno(userPrompt, opts) {
2069
2104
  try { depois = require('fs').readFileSync(arquivoDaEdicao, 'utf8'); } catch { depois = null; }
2070
2105
  const encolheu = require('../encolheu.js').julgar(antesDaEdicao, depois);
2071
2106
  if (encolheu) {
2072
- console.log(c.warn(` ⚠ ${tc.arguments.path}: ${encolheu.texto}. Se sumiu código sem querer, /undo volta.`));
2107
+ ev('linha', { texto: ` ⚠ ${tc.arguments.path}: ${encolheu.texto}. Se sumiu código sem querer, /undo volta.`, nivel: 'aviso' });
2073
2108
  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
2109
  }
2075
2110
  }
2076
- for (const aviso of hooks.rodar('PostToolUse', { nome: tc.name, args: tc.arguments, resultado: result, projectDir: pastaAtual }).avisos) console.log(c.dim(` ⎿ ${aviso}`));
2111
+ 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
2112
  traco(`${tc.name} devolveu ok=${result && result.ok}`);
2078
2113
  if (/^estudio_|^planejar_producao$/.test(tc.name) && result && result.ok !== false) {
2079
2114
  const id = result.id || (result.projeto && result.projeto.id) || (tc.arguments && tc.arguments.id);
2080
2115
  if (typeof id === 'string' && id) estudioDaTarefa = id;
2081
2116
  }
2082
- logResult(tc.name, result, profundidade);
2117
+ ev('resultado', { nome: tc.name, resultado: result });
2118
+ // Render é lento e roda solto: ao fim, o sino avisa (vira notice/created no app).
2119
+ if (tc.name === 'estudio_criar' && result && result.ok !== false && result.arquivo) ev('aviso', { texto: `${result.resumo || 'Pronto'} ${require('path').basename(result.arquivo)}` });
2083
2120
  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}`));
2121
+ for (const l of require('../../plugin/estudio/conferencia.js').linhasPraPessoa(result.conferencia)) ev('linha', { texto: ` ⎿ ${l}`, nivel: 'aviso' });
2085
2122
  }
2086
2123
  repeticao.executou(jaChamado, tc.name, tc.arguments);
2087
2124
  if (RELEITURA.has(tc.name) && result && result.ok !== false) {
@@ -2202,7 +2239,7 @@ async function runActInterno(userPrompt, opts) {
2202
2239
 
2203
2240
  if (finished) {
2204
2241
  finalText = finished;
2205
- if (!subagent) fala.resposta(finished);
2242
+ if (!subagent) ev('resposta', { texto: finished });
2206
2243
  // Recado chegou enquanto ele fechava: entrega e segue.
2207
2244
  if (!subagent && CAIXA.length && !PARAR) { finalText = ''; continue; }
2208
2245
  break;
@@ -2220,14 +2257,14 @@ async function runActInterno(userPrompt, opts) {
2220
2257
  let passagemDaParada = null;
2221
2258
  if (paradoPorTravar && !finalText) {
2222
2259
  finalText = paradoPorTravar;
2223
- if (!subagent) fala.aviso(finalText);
2260
+ if (!subagent) ev('aviso', { texto: finalText });
2224
2261
  // Parou sem terminar: a memória leva o estado, não só "parei" (lib/passagem.js).
2225
2262
  passagemDaParada = require('../../passagem').montar({ pedido: userPrompt, motivo: finalText, registro: livro, falhasPendentes });
2226
2263
  }
2227
2264
 
2228
2265
  if (PARAR && !finalText) {
2229
2266
  finalText = 'Parado por você (Esc Esc). O que já foi feito, ficou feito.';
2230
- if (!subagent) fala.aviso(finalText);
2267
+ if (!subagent) ev('aviso', { texto: finalText });
2231
2268
  }
2232
2269
 
2233
2270
  // Nunca terminar em silêncio. Quando o modelo devolvia resposta vazia sem
@@ -2236,7 +2273,7 @@ async function runActInterno(userPrompt, opts) {
2236
2273
  // depois de a primeira tentativa de seletor falhar.
2237
2274
  if (!finalText) {
2238
2275
  finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS, teto });
2239
- if (!subagent) fala.aviso(finalText);
2276
+ if (!subagent) ev('aviso', { texto: finalText });
2240
2277
  passagemDaParada = require('../../passagem').montar({ pedido: userPrompt, motivo: finalText, registro: livro, falhasPendentes });
2241
2278
  }
2242
2279
 
@@ -2299,11 +2336,13 @@ async function runSubagent(args, ctx) {
2299
2336
  // O ofício: o modelo diz quem faz (`especialista`), ou a tarefa sugere. O
2300
2337
  // briefing vai na frente da tarefa, na mesma mensagem — ver especialistas.js.
2301
2338
  // 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);
2339
+ // `agente`: um agente da pessoa (~/.primocode/agentes, lib/agentes-pessoais.js) ganha do catálogo e do legado.
2340
+ const meu = args.agente ? require('../../agentes-pessoais').ler(args.agente) : null;
2341
+ const novos = meu ? [] : require('../../agentes').refinar(args.especialista, args.prompt); // o catálogo confere o chute do modelo
2342
+ const esp = meu ? require('../../agentes-pessoais').comoEspecialista(meu) : novos.length ? null : especialistas.escolher(args.especialista, args.prompt);
2304
2343
  const agentes = require('../../agentes');
2305
- const rot = novos.length ? agentes.rotulo(novos) : especialistas.rotulo(esp);
2306
- fala.agente(rot, descricao, profundidade);
2344
+ const rot = meu ? meu.nome : novos.length ? agentes.rotulo(novos) : especialistas.rotulo(esp);
2345
+ telaDe(ctx, 'agente', { rotulo: rot, descricao });
2307
2346
 
2308
2347
  // modelo_sugerido do briefing vale quando o placar ainda não tem dado desse tipo de tarefa.
2309
2348
  let modeloDoAgente = model;
@@ -2322,6 +2361,7 @@ async function runSubagent(args, ctx) {
2322
2361
  subagent: true,
2323
2362
  motor,
2324
2363
  permissoes,
2364
+ emitir: emitirDe(ctx),
2325
2365
  autorizar: ctx.autorizar,
2326
2366
  controle: ctx.controle, // o mesmo objeto: a resposta vale para todos
2327
2367
  rotulo: `subagente: ${String(descricao).slice(0, 60)}`,
@@ -2334,7 +2374,7 @@ async function runSubagent(args, ctx) {
2334
2374
  if (ctx.livro && r.registro) registro.juntar(ctx.livro, r.registro);
2335
2375
 
2336
2376
  const resumo = r.assistantMessage.content || 'sem resultado';
2337
- fala.agenteVoltou(resumo, profundidade);
2377
+ telaDe(ctx, 'agente-voltou', { texto: resumo });
2338
2378
  return entregaDoAgente(rot, resumo, r.registro, { pasta: projectDir });
2339
2379
  }
2340
2380
 
@@ -2417,10 +2457,10 @@ async function rodarDiretor(args, ctx) {
2417
2457
  const diretor = require('../../plugin/estudio/diretor.js');
2418
2458
  const correr = async (prompt, rotulo) => {
2419
2459
  if (PARAR) return '';
2420
- fala.agente('Diretor', rotulo, profundidade);
2460
+ telaDe(ctx, 'agente', { rotulo: 'Diretor', descricao: rotulo });
2421
2461
  const r = await runAct(prompt, {
2422
2462
  server, model, token, projectDir, projectName,
2423
- history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
2463
+ history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes, emitir: emitirDe(ctx),
2424
2464
  });
2425
2465
  return (r && r.assistantMessage && r.assistantMessage.content) || '';
2426
2466
  };
@@ -2431,7 +2471,7 @@ async function rodarDiretor(args, ctx) {
2431
2471
  formato: args.formato === 'apresentacao' ? 'apresentacao' : 'video',
2432
2472
  contexto: args.contexto || '',
2433
2473
  correr,
2434
- mostrar: (t) => fala.agente('Diretor', t, profundidade),
2474
+ mostrar: (t) => telaDe(ctx, 'agente', { rotulo: 'Diretor', descricao: t }),
2435
2475
  });
2436
2476
  if (!r.ok) return r;
2437
2477
 
@@ -2473,10 +2513,10 @@ async function rodarPipeline(args, ctx) {
2473
2513
 
2474
2514
  const correr = async (prompt, rotulo) => {
2475
2515
  if (PARAR) return '';
2476
- fala.agente('Agente', rotulo, profundidade);
2516
+ telaDe(ctx, 'agente', { rotulo: 'Agente', descricao: rotulo });
2477
2517
  const r = await runAct(prompt, {
2478
2518
  server, model, token, projectDir: pasta, projectName,
2479
- history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes,
2519
+ history: [], profundidade: profundidade + 1, subagent: true, motor, permissoes, emitir: emitirDe(ctx),
2480
2520
  });
2481
2521
  return (r && r.assistantMessage && r.assistantMessage.content) || '';
2482
2522
  };
@@ -2486,7 +2526,7 @@ async function rodarPipeline(args, ctx) {
2486
2526
  pasta,
2487
2527
  rodadas: args.rodadas,
2488
2528
  correr,
2489
- mostrar: (linha) => console.log(fala.resultado(profundidade) + c.muted(linha)),
2529
+ mostrar: (linha) => telaDe(ctx, 'resultado-linha', { texto: linha }),
2490
2530
  abrir: tools.openInBrowser,
2491
2531
  });
2492
2532
  }
@@ -2518,5 +2558,5 @@ function novoPortaoDeProva() {
2518
2558
  };
2519
2559
  }
2520
2560
 
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,
2561
+ 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
2562
  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
@@ -943,6 +943,12 @@ async function despachar(name, args, ctx) {
943
943
  return estudio.ferramentaMidia(args);
944
944
  case 'estudio_ae':
945
945
  return estudio.ferramentaAE(args);
946
+ case 'estudio_criar':
947
+ // A porta simples: abre o projeto e chama plano, motion e render por dentro. ctx.progresso alimenta a tela.
948
+ return estudio.ferramentaCriar(args, {
949
+ ...ctx, projectDir,
950
+ abrir: (arquivo) => { try { openInBrowser(arquivo); } catch {} },
951
+ });
946
952
  case 'estudio_motion':
947
953
  return estudio.ferramentaMotion(args);
948
954
  case 'estudio_editar':
package/lib/tui/ui.js CHANGED
@@ -566,7 +566,7 @@ const ROTULOS = {
566
566
  estudio_site:'Estúdio·marca',
567
567
  desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
568
568
  desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
569
- estudio_midia:'Estúdio·mídia', estudio_ae:'Estúdio·After Effects', estudio_motion:'Estúdio·Motion', estudio_editar:'Estúdio·Editar', construir_app:'Construir',
569
+ estudio_midia:'Estúdio·mídia', estudio_ae:'Estúdio·After Effects', estudio_motion:'Estúdio·Motion', estudio_editar:'Estúdio·Editar', estudio_criar:'Estúdio·Criar', construir_app:'Construir',
570
570
  remember_general:'Lembrar', lembrar:'Memória',
571
571
  mcp_procurar:'MCP·procurar', mcp_conectar:'MCP·conectar', mcp_usar:'MCP',
572
572
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "11.1.0",
3
+ "version": "11.2.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -10,7 +10,11 @@
10
10
  "start": "node bin/primocode.js",
11
11
  "postinstall": "node lib/preparar.js || true",
12
12
  "test:server": "cd server && npm test",
13
- "test": "node scripts/testes.js"
13
+ "test": "node scripts/testes.js",
14
+ "test:app": "node test/app-fluxo.test.mjs && node test/app-primeira.test.mjs && node test/app-entrar.test.mjs",
15
+ "app:build": "cd app && npm run dist",
16
+ "app:dev": "cd app && npm start",
17
+ "mobile:build": "cd mobile && npm run build"
14
18
  },
15
19
  "engines": {
16
20
  "node": ">=18.17.0"