primocode 9.10.0 → 10.0.9

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 (89) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +169 -342
  3. package/lib/act.js +360 -39
  4. package/lib/browser.js +105 -4
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/claude-engine.js +38 -17
  8. package/lib/codex-engine.js +27 -9
  9. package/lib/config.js +10 -3
  10. package/lib/cursor.js +10 -1
  11. package/lib/entrada.js +77 -11
  12. package/lib/estudio/cinema.js +27 -9
  13. package/lib/estudio/diretor.js +21 -1
  14. package/lib/estudio/ende.js +16 -5
  15. package/lib/estudio/estilo.js +66 -0
  16. package/lib/estudio/gerativo.js +1453 -0
  17. package/lib/estudio/glifos.js +215 -0
  18. package/lib/estudio/hibrido.js +165 -0
  19. package/lib/estudio/index.js +284 -53
  20. package/lib/estudio/kit/base.jsx +285 -0
  21. package/lib/estudio/kit/gerativo.jsx +980 -0
  22. package/lib/estudio/kit/index.jsx +13 -0
  23. package/lib/estudio/kit/marca.jsx +741 -0
  24. package/lib/estudio/kit/tres.jsx +308 -0
  25. package/lib/estudio/kit/vox.jsx +357 -0
  26. package/lib/estudio/kit.js +78 -0
  27. package/lib/estudio/lancamento.js +441 -0
  28. package/lib/estudio/motion.js +2 -5
  29. package/lib/estudio/plano.js +28 -3
  30. package/lib/estudio/referencias.js +105 -0
  31. package/lib/estudio/remotion.js +139 -51
  32. package/lib/estudio/ritmo.js +77 -0
  33. package/lib/estudio/sons-leves.js +38 -1
  34. package/lib/estudio/variedade.js +105 -0
  35. package/lib/estudio/vox.js +414 -0
  36. package/lib/fala.js +1 -94
  37. package/lib/mcp.js +156 -0
  38. package/lib/personas.js +67 -0
  39. package/lib/pipeline.js +55 -56
  40. package/lib/repeticao.js +3 -1
  41. package/lib/skills.js +44 -2
  42. package/lib/snapshot.js +111 -0
  43. package/lib/tools.js +100 -22
  44. package/lib/ui.js +50 -8
  45. package/lib/video/analise.js +89 -0
  46. package/lib/video/auto.js +329 -0
  47. package/lib/video/camadas.js +207 -0
  48. package/lib/video/cli.js +74 -0
  49. package/lib/video/delta.js +90 -0
  50. package/lib/video/edits.js +379 -0
  51. package/lib/video/filters.js +280 -0
  52. package/lib/video/fx3d/Camadas.jsx +64 -0
  53. package/lib/video/fx3d/Objetos.jsx +128 -0
  54. package/lib/video/fx3d/Root.jsx +18 -0
  55. package/lib/video/fx3d/modelos.jsx +304 -0
  56. package/lib/video/fx3d.js +108 -0
  57. package/lib/video/gerados.js +119 -0
  58. package/lib/video/objetos.js +111 -0
  59. package/lib/video/ops-fx.js +453 -0
  60. package/lib/video/ops.js +347 -0
  61. package/lib/video/paralelo.js +50 -0
  62. package/lib/video/pedido.js +191 -0
  63. package/lib/video/projeto.js +133 -0
  64. package/lib/video/rastrear.js +95 -0
  65. package/lib/video/rastro.js +276 -0
  66. package/lib/video/render.js +274 -0
  67. package/lib/video/texto.js +144 -0
  68. package/lib/video/timeline.js +318 -0
  69. package/lib/video/transcript.js +124 -0
  70. package/lib/voz.js +75 -223
  71. package/package.json +2 -3
  72. package/skills/motion-de-primeira/SKILL.md +5 -5
  73. package/studio/narrar.py +10 -7
  74. package/studio/rastrear.py +399 -0
  75. package/studio/texto.py +208 -0
  76. package/voz/primo_voz/__main__.py +96 -236
  77. package/voz/primo_voz/transcrever.py +31 -1
  78. package/app/ansi.js +0 -55
  79. package/app/app.css +0 -500
  80. package/app/app.js +0 -1017
  81. package/app/icone.svg +0 -6
  82. package/app/index.html +0 -220
  83. package/app/primo-marca.js +0 -192
  84. package/lib/app.js +0 -615
  85. package/lib/icone.js +0 -263
  86. package/lib/instalar-app.js +0 -231
  87. package/lib/painel.js +0 -291
  88. package/voz/primo_voz/desperta.py +0 -195
  89. package/voz/primo_voz/orbe.py +0 -406
package/lib/act.js CHANGED
@@ -31,6 +31,7 @@ const preLeitura = require('./pre-leitura');
31
31
  // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
32
32
  // de um vídeo de dez cenas com pesquisa, e a pessoa lia "gastei as voltas".
33
33
  const MAX_ITERATIONS = Number(process.env.PRIMOCODE_MAX_VOLTAS || 40);
34
+ const MAX_VOLTAS_AGENTE = Number(process.env.PRIMOCODE_MAX_VOLTAS_AGENTE || 20);
34
35
  // Quantas vezes o agente é EMPURRADO quando para no meio para narrar
35
36
  // ("agora vou criar o arquivo…") em vez de fazer. Ver `pareceParadaNoMeio`.
36
37
  const MAX_EMPURROES = 3;
@@ -39,6 +40,10 @@ const MAX_RODADAS_PARADAS = 3;
39
40
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
40
41
  // As leituras que, repetidas sem mudança no meio, devolvem o guardado.
41
42
  const RELEITURA = new Set(['read_file', 'list_dir']);
43
+ /* O que só lê: pode rodar junto com as outras leituras da mesma rodada. */
44
+ const SO_LEITURA = new Set(['read_file', 'list_dir', 'glob', 'grep', 'pesquisar_referencia', 'estudio_site']);
45
+ const ehLeitura = (tc) => SO_LEITURA.has(tc.name)
46
+ || (tc.name === 'run_shell' && /^\s*primocode\s+(skill|mcp\s+list|video\s+(help|get_timeline|get_transcript))\b/.test(String((tc.arguments && tc.arguments.command) || '')));
42
47
 
43
48
  // ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
44
49
  // O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
@@ -55,6 +60,44 @@ function interromper() {
55
60
  if (emVoo) { try { emVoo.abort(); } catch {} }
56
61
  }
57
62
 
63
+ /* ── A CAIXA DE ENTRADA: o que a pessoa digita enquanto ele trabalha ────
64
+ * Antes, a linha digitada durante uma tarefa ia para a fila do terminal e só
65
+ * rodava DEPOIS — como um pedido novo, que não sabia nada do que estava em
66
+ * curso. "Mesmo quando ela estiver trabalhando poderá enviar mensagem; ela
67
+ * vai à fila e, quando o PrimoCode termina um passo, recebe a mensagem e
68
+ * integra na tarefa." É o que o Claude Code e o Codex fazem: o recado entra
69
+ * na conversa na fronteira seguinte — depois do resultado das ferramentas,
70
+ * antes da próxima ida ao modelo. Nenhuma ferramenta é cortada no meio.
71
+ *
72
+ * Só o pedido PRINCIPAL lê a caixa (o subagente trabalha para ele). O que
73
+ * sobrar quando a tarefa acabar volta para o terminal como pedido normal. */
74
+ const CAIXA = [];
75
+ let TRABALHANDO = false;
76
+ function mandarDuranteATarefa(texto) {
77
+ const t = String(texto || '').trim();
78
+ if (!t || !TRABALHANDO) return false;
79
+ CAIXA.push(t);
80
+ return true;
81
+ }
82
+ function trabalhando() { return TRABALHANDO; }
83
+ /** O que ficou na caixa sem ser lido — o terminal roda como pedido novo. */
84
+ function sobrasDaCaixa() { return CAIXA.splice(0); }
85
+ /* Esvazia a caixa dentro da conversa. Devolve quantos recados entraram. */
86
+ function entregarRecados(messages, profundidade) {
87
+ if (!CAIXA.length) return 0;
88
+ const recados = CAIXA.splice(0);
89
+ for (const r of recados) fala.pensamento(`recado recebido — entrando na tarefa: ${r}`, profundidade);
90
+ messages.push({
91
+ role: 'user',
92
+ content: (recados.length === 1 ? 'MENSAGEM NOVA do usuário, enviada enquanto você trabalhava:\n'
93
+ : 'MENSAGENS NOVAS do usuário, enviadas enquanto você trabalhava:\n')
94
+ + recados.map((r) => `- ${r}`).join('\n')
95
+ + '\n\nIntegre isto na tarefa em curso: se é um ajuste, aplique; se muda o rumo, siga o novo rumo; '
96
+ + 'se é uma pergunta, responda em uma frase e continue. Não recomece o que já está feito.',
97
+ });
98
+ return recados.length;
99
+ }
100
+
58
101
  /**
59
102
  * Este erro é "não coube no minuto"?
60
103
  *
@@ -138,9 +181,6 @@ function limparParaOModelo(result) {
138
181
 
139
182
  function logResult(kind, result, profundidade) {
140
183
  const p = fala.resultado(profundidade);
141
- // A linha da janela (lib/app.js) fecha aqui: o mesmo resultado que o
142
- // terminal desenha embaixo, resumido. Ver o canal em lib/fala.js.
143
- fala.avisar('resultado', fala.resumoDoResultado(kind, result));
144
184
  if (!result) return;
145
185
  // Uma tool que falhou é informação para o AGENTE, que vai consertar na
146
186
  // volta seguinte — não é o erro do usuário. Âmbar, uma linha, sem o
@@ -221,9 +261,12 @@ function logResult(kind, result, profundidade) {
221
261
  case 'browser_open':
222
262
  console.log(p + c.muted(result.url || 'aberto'));
223
263
  return;
224
- case 'browser_read':
225
- console.log(p + c.muted(`${(result.text || '').length} caracteres lidos da página`));
264
+ case 'browser_read': {
265
+ const refs = (String(result.pagina || '').match(/^@e\d+/gm) || []).length;
266
+ console.log(p + c.muted(refs ? `${refs} elementos com ref · ${(result.text || '').length} caracteres de texto`
267
+ : `${(result.text || '').length} caracteres lidos da página`));
226
268
  return;
269
+ }
227
270
  case 'pesquisar_referencia': {
228
271
  console.log(p + c.muted(`${result.fontes} fonte${result.fontes === 1 ? '' : 's'} lida${result.fontes === 1 ? '' : 's'} sobre "${result.consulta}"`));
229
272
  for (const r of (result.referencias || []).slice(0, 4)) {
@@ -298,6 +341,50 @@ const RESTO_DO_RESULTADO = 300;
298
341
  const RESTO_DA_PESQUISA = 1500;
299
342
  const MATERIA_PRIMA = new Set(['pesquisar_referencia']);
300
343
 
344
+ /* ── O ARQUIVO JÁ GRAVADO NÃO VIAJA DE NOVO ─────────────────────────────
345
+ * O `compactar` encolhia o resultado das ferramentas e deixava intacto o que
346
+ * o MODELO escreveu — e o que ele escreve num site é o arquivo inteiro, dentro
347
+ * da chamada write_file. Medido em 29/09 ("crie uma landing de barbearia"):
348
+ * depois do index.html (7,6 KB) e do style.css (15 KB), cada ida ao modelo
349
+ * levava os dois de novo, passava do teto de tokens por minuto do plano
350
+ * grátis e ficava esperando a janela virar, em silêncio — "escreve dois
351
+ * arquivos e trava". O conteúdo já está no disco: nas voltas antigas ele vira
352
+ * um ponteiro. As duas últimas ficam inteiras (é nelas que o modelo decide o
353
+ * próximo passo, e é delas que sai a releitura sem disco — ver read_file). */
354
+ const ESCRITAS_INTEIRAS = 2;
355
+ const RESTO_DA_ESCRITA = 400;
356
+ function assistentesAntigas(messages) {
357
+ const idx = [];
358
+ for (let i = 0; i < messages.length; i++) {
359
+ const m = messages[i];
360
+ if (m && m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) idx.push(i);
361
+ }
362
+ return new Set(idx.slice(0, -ESCRITAS_INTEIRAS));
363
+ }
364
+ function encolherEscritas(m) {
365
+ let mudou = false;
366
+ const tool_calls = m.tool_calls.map((tc) => {
367
+ const f = tc && tc.function;
368
+ if (!f || !/^(write_file|edit_file|multi_edit)$/.test(f.name)) return tc;
369
+ const bruto = String(f.arguments || '');
370
+ if (bruto.length <= RESTO_DA_ESCRITA) return tc;
371
+ let a;
372
+ try { a = JSON.parse(bruto); } catch { return tc; }
373
+ const ponteiro = (n) => `(${n} caracteres — já gravado no disco; para mudar, leia com read_file e use edit_file)`;
374
+ if (f.name === 'write_file' && typeof a.content === 'string' && a.content.length > RESTO_DA_ESCRITA) a.content = ponteiro(a.content.length);
375
+ else if (f.name === 'edit_file' && typeof a.new_string === 'string' && a.new_string.length > RESTO_DA_ESCRITA) {
376
+ a.new_string = ponteiro(a.new_string.length);
377
+ if (typeof a.old_string === 'string' && a.old_string.length > 200) a.old_string = a.old_string.slice(0, 200) + '…';
378
+ } else if (f.name === 'multi_edit' && Array.isArray(a.edits)) {
379
+ a.edits = a.edits.map((e) => (e && typeof e.new_string === 'string' && e.new_string.length > RESTO_DA_ESCRITA
380
+ ? { ...e, new_string: ponteiro(e.new_string.length), old_string: String(e.old_string || '').slice(0, 200) } : e));
381
+ } else return tc;
382
+ mudou = true;
383
+ return { ...tc, function: { ...f, arguments: JSON.stringify(a) } };
384
+ });
385
+ return mudou ? { ...m, tool_calls } : m;
386
+ }
387
+
301
388
  /**
302
389
  * Encolhe os resultados de tool ANTIGOS antes de mandar a conversa.
303
390
  *
@@ -322,11 +409,13 @@ function compactar(messages) {
322
409
 
323
410
  const indices = [];
324
411
  for (let i = 0; i < messages.length; i++) if (messages[i].role === 'tool') indices.push(i);
325
- if (indices.length <= RESULTADOS_INTEIROS && !duplicadas.size) return messages;
412
+ if (indices.length <= RESULTADOS_INTEIROS && !duplicadas.size && !assistentesAntigas(messages).size) return messages;
326
413
 
327
414
  const chamadas = repeticao.chamadasPorId(messages);
328
415
  const encolher = new Set(indices.slice(0, -RESULTADOS_INTEIROS));
416
+ const antigas = assistentesAntigas(messages);
329
417
  return messages.map((m, i) => {
418
+ if (antigas.has(i)) return encolherEscritas(m);
330
419
  if (duplicadas.has(i)) {
331
420
  return { ...m, content: '(a mesma referência aparece inteira mais adiante nesta conversa — use aquela)' };
332
421
  }
@@ -383,10 +472,21 @@ function esperaDoErro(e, tentativa = 0) {
383
472
  return Math.min(20, 3 * 2 ** Math.max(0, tentativa - 1)) * 1000;
384
473
  }
385
474
 
386
- /** Espera com o spinner de PENSAR girando — nunca um de "limite" ou
387
- * "continuando em Ns". Quem olha vê o Primo pensando um pouco mais, e só. */
475
+ /** Espera a vez no modelo — À VISTA, com os segundos, também no subagente.
476
+ * Antes girava o spinner de "pensando" (e no subagente, nada): eram minutos
477
+ * de tela parada que pareciam trava. "As coisas estão acontecendo, só não
478
+ * estão sendo mostradas." Dizer que é a vez do modelo, sem alarme, é a
479
+ * diferença entre esperar e desistir. */
480
+ /* PRIMOCODE_TRACO=1: uma linha por ida ao modelo no stderr — o tamanho da
481
+ conversa, o erro e a espera. É o que diz ONDE uma tarefa travou quando a
482
+ tela (ou um log sem terminal) só mostra o último arquivo escrito. */
483
+ function traco(texto) {
484
+ if (process.env.PRIMOCODE_TRACO === '1') process.stderr.write(` [traço ${new Date().toTimeString().slice(0, 8)}] ${texto}\n`);
485
+ }
486
+
388
487
  async function pausar(ms, subagent) {
389
- const sp = subagent ? null : new Spinner(PENSANDO).start();
488
+ traco(`esperando ${Math.round(ms / 1000)}s pela vez no modelo${subagent ? ' (agente)' : ''}`);
489
+ const sp = new Spinner(subagent ? 'o agente espera a vez no modelo' : 'esperando a vez no modelo').start();
390
490
  await new Promise((r) => setTimeout(r, ms));
391
491
  if (sp) sp.stop();
392
492
  tempos.somar('espera de cota/rede', ms);
@@ -431,7 +531,7 @@ const TROCA_DE_ROTA = 'Você ficou três voltas sem conseguir rodar nada, repeti
431
531
  + '(write_todos) e faça AGORA o primeiro passo, por um caminho diferente: outra ferramenta, outro arquivo, '
432
532
  + 'outro comando. Se não houver caminho, chame finish e diga em uma frase o que impediu.';
433
533
 
434
- function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
534
+ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou, teto = MAX_ITERATIONS }) {
435
535
  if (falhasPendentes.length) {
436
536
  return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
437
537
  }
@@ -439,11 +539,11 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
439
539
  return 'Fiquei preso: tentei dois caminhos e os dois voltaram ao mesmo lugar, então parei antes de gastar sua cota à toa. '
440
540
  + 'O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
441
541
  }
442
- if (voltas >= MAX_ITERATIONS) {
542
+ if (voltas >= teto) {
443
543
  const emFalso = voltasEmFalso
444
544
  ? ` ${voltasEmFalso} ${voltasEmFalso === 1 ? 'chamada foi repetida' : 'chamadas foram repetidas'} e não rodaram de novo.`
445
545
  : '';
446
- return `Usei as ${MAX_ITERATIONS} voltas deste pedido sem chegar ao fim.${emFalso}`
546
+ return `Usei as ${teto} voltas deste pedido sem chegar ao fim.${emFalso}`
447
547
  + ' O que já fiz está salvo. Diga "continua" que eu retomo daqui.';
448
548
  }
449
549
  return 'Parei sem concluir e sem explicação do modelo. Diga "continua" que eu tento de novo daqui.';
@@ -487,6 +587,69 @@ function mapaDoProjeto(dir, limite = 80, achados = arquivosDoProjeto(dir, limite
487
587
  return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
488
588
  }
489
589
 
590
+ /* O comando de shell que só LÊ um arquivo — `cat x`, `sed -n '10,40p' x`,
591
+ `head -50 x`, `tail x`, `nl x`, com ou sem `cd pasta &&` na frente. Medido
592
+ num pedido de site (29/09): depois de escrever o script.js, o agente o
593
+ releu em quatro fatias de `sed -n`, uma volta de modelo cada. Devolve o
594
+ caminho lido (absoluto quando houve cd), ou null. */
595
+ function leituraPeloShell(comando) {
596
+ const m = /^\s*(?:cd\s+("[^"]+"|'[^']+'|\S+)\s*&&\s*)?(?:cat|nl|sed\s+-n\s+(?:'[^']*'|"[^"]*"|\S+)|head(?:\s+-n?\s*\d+)?|tail(?:\s+-n?\s*\d+)?)\s+("[^"]+"|'[^']+'|[^\s|;&><]+)\s*$/.exec(String(comando || ''));
597
+ if (!m) return null;
598
+ const tira = (x) => String(x || '').replace(/^["']|["']$/g, '');
599
+ const arquivo = tira(m[2]);
600
+ return m[1] ? require('path').resolve(tira(m[1]), arquivo) : arquivo;
601
+ }
602
+
603
+ /* ── O HISTÓRICO QUE VIAJA ─────────────────────────────────────────────
604
+ * Vinte mensagens INTEIRAS iam em toda volta do agente principal. Medido em
605
+ * 29/09: num pedido de site, 28 mil dos 38 mil caracteres da primeira ida
606
+ * eram respostas de pedidos anteriores — sozinhas, acima do teto por minuto
607
+ * do plano grátis. O que a conversa precisa para continuar é o fio (o que
608
+ * foi pedido, o que ficou pronto), não o texto inteiro: cada mensagem entra
609
+ * até um teto, e o total também, das mais novas para as mais velhas. */
610
+ const HISTORICO_POR_MENSAGEM = 1200;
611
+ const HISTORICO_TOTAL = 6000;
612
+ function historicoEnxuto(history) {
613
+ const fora = [];
614
+ let total = 0;
615
+ for (const m of (history || []).slice(-20).reverse()) {
616
+ if (!m || !m.role || !m.content) continue;
617
+ let t = String(m.content);
618
+ if (t.length > HISTORICO_POR_MENSAGEM) t = t.slice(0, HISTORICO_POR_MENSAGEM) + '…(encurtado)';
619
+ if (total + t.length > HISTORICO_TOTAL && fora.length) break;
620
+ total += t.length;
621
+ fora.unshift({ role: m.role, content: t });
622
+ }
623
+ // A API quer a conversa começando pelo usuário.
624
+ while (fora.length && fora[0].role !== 'user') fora.shift();
625
+ return fora;
626
+ }
627
+
628
+ /* ── O RITMO ─────────────────────────────────────────────────────────
629
+ * "A potência não deve vir do modelo, deve vir do sistema." O prompt do
630
+ * servidor manda "leia antes de editar" e "confira antes de dizer que
631
+ * terminou" — certo para editar código dos outros, e a receita do laço
632
+ * quando o arquivo foi ele mesmo que escreveu um minuto antes. Estas regras
633
+ * vêm do que o opencode e o Codex põem nos prompts deles para o modelo não
634
+ * reler o que fez; entram em TODO pedido, curtas, porque o plano conta token.
635
+ * A conferência de verdade é nossa (a página aberta no Chrome escondido). */
636
+ const RITMO = '[ritmo]\n'
637
+ + '- O resultado de write_file/edit_file é a verdade. Não releia um arquivo que você acabou de escrever para conferir (nem com cat/sed/head): a conferência é do PrimoCode.\n'
638
+ + '- Várias ações independentes vão na MESMA resposta (ex.: escrever index.html, style.css e script.js juntos).\n'
639
+ + '- Leia um arquivo inteiro uma vez, não em pedaços pequenos; não leia de novo o que não mudou.\n'
640
+ + '- Não narre o próximo passo: faça. Termine com finish("o que foi feito").\n'
641
+ + '- VOCÊ DECIDE O CAMINHO. Pedido simples: faça direto. Pedido grande com partes independentes: write_todos com as partes, '
642
+ + 'depois spawn_agent — um por parte, até 8 NA MESMA resposta (rodam juntos), cada um com o especialista e tudo que ele precisa saber. '
643
+ + 'Cada um volta com uma ENTREGA: confira; o que falhou volta para conserto; a parte que depende de outra recebe a entrega dela no prompt.\n'
644
+ + '- Antes de dizer pronto, PROVE: rode o teste ou o comando, abra o resultado, olhe a imagem. Sem prova, não está pronto.';
645
+
646
+ /* O ritmo do subagente: o mesmo, sem a parte de orquestrar (ele não abre outro agente). */
647
+ const RITMO_AGENTE = '[ritmo]\n'
648
+ + '- O resultado de write_file/edit_file é a verdade. Não releia o que acabou de escrever.\n'
649
+ + '- Leia um arquivo INTEIRO uma vez (read_file sem offset). Não leia aos pedaços, não leia de novo o que não mudou.\n'
650
+ + '- Várias ações independentes na MESMA resposta.\n'
651
+ + '- Faça o que a sua parte pede, prove (teste ou comando) e chame finish. Polimento além do pedido não é sua parte.';
652
+
490
653
  /* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
491
654
  entrar já lido (ver lib/pre-leitura.js). */
492
655
  function buildMemoryContext(projectDir, projectName, pedido = '') {
@@ -505,6 +668,12 @@ function buildMemoryContext(projectDir, projectName, pedido = '') {
505
668
  if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
506
669
  const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
507
670
  if (lidos) parts.push(lidos);
671
+ parts.push(RITMO);
672
+ // Os servidores MCP configurados, numa linha (lib/mcp.js).
673
+ try { const m = require('./mcp.js').dica(projectDir); if (m) parts.push(m); } catch { /* sem mcp */ }
674
+ // A equipe calada (lib/personas.js): o olhar do especialista que o pedido pede.
675
+ const equipe = require('./personas.js').bloco(pedido);
676
+ if (equipe) parts.push(equipe);
508
677
  return parts.join('\n\n');
509
678
  }
510
679
 
@@ -584,10 +753,12 @@ async function runAct(userPrompt, opts) {
584
753
  mexeuNaPagina = false;
585
754
  let medida = null;
586
755
  const t = Date.now();
756
+ // Com spinner: eram segundos de tela parada entre o último arquivo e o
757
+ // "pronto", e parecia que tinha travado ali.
758
+ const sp = new Spinner('abrindo a página escondido para conferir').start();
587
759
  try {
588
- fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
589
760
  medida = await medirPagina(pastaAtual);
590
- } catch { medida = null; }
761
+ } catch { medida = null; } finally { sp.stop(); }
591
762
  msConferindo += Date.now() - t;
592
763
  tempos.somar('conferir página', Date.now() - t);
593
764
  if (!medida || !medida.testou) return null;
@@ -602,6 +773,9 @@ async function runAct(userPrompt, opts) {
602
773
  const jaChamado = repeticao.novo();
603
774
  // O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
604
775
  const leiturasGuardadas = new Map();
776
+ // O que ELE escreveu inteiro nesta tarefa, por caminho absoluto — para
777
+ // não gastar uma volta relendo o próprio texto (ver read_file abaixo).
778
+ const escritosNaTarefa = new Map();
605
779
  /* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
606
780
  no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
607
781
  não se mistura com o do principal — mas o que ele fez também precisa
@@ -625,12 +799,9 @@ async function runAct(userPrompt, opts) {
625
799
  * não é uma opção num plano limitado por token. */
626
800
  let pedido = userPrompt;
627
801
  if (!subagent) {
628
- const comSkills = skills.montar(userPrompt);
802
+ const comSkills = skills.cardapio(userPrompt);
629
803
  pedido = comSkills.texto;
630
804
  if (instrucao) pedido = `${instrucao}\n\nPEDIDO: ${pedido}`;
631
- if (comSkills.skills.length) {
632
- fala.pensamento(`seguindo: ${comSkills.skills.map((s) => s.titulo).join(', ')}`, profundidade);
633
- }
634
805
  }
635
806
  let voltasEmFalso = 0;
636
807
  // Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
@@ -653,9 +824,10 @@ async function runAct(userPrompt, opts) {
653
824
  if (!subagent) {
654
825
  const memory = buildMemoryContext(projectDir, projectName, userPrompt);
655
826
  if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
656
- for (const m of (history || []).slice(-20)) {
657
- if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
658
- }
827
+ for (const m of historicoEnxuto(history)) messages.push(m);
828
+ } else {
829
+ // O subagente também trabalha no ritmo: sem isto ele relia o mesmo arquivo aos pedaços.
830
+ messages.push({ role: 'system', content: RITMO_AGENTE });
659
831
  }
660
832
  /* O que VAI para o modelo é `pedido` (com as skills na frente); o que
661
833
  fica guardado na memória e no histórico é `userPrompt`, o que a pessoa
@@ -676,8 +848,20 @@ async function runAct(userPrompt, opts) {
676
848
  * Num subagente não entra: ele já está fazendo uma parte de um plano, e
677
849
  * um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
678
850
  if (!subagent && profundidade === 0) {
679
- const producao = require('./estudio/diretor.js').precisaDeProducao(pedido);
680
- if (producao.producao) {
851
+ const diretor = require('./estudio/diretor.js');
852
+ const producao = diretor.precisaDeProducao(pedido);
853
+ if (diretor.precisaDeEdicao(pedido)) {
854
+ // A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
855
+ messages.push({ role: 'system', content: 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
856
+ + 'SE A PESSOA SÓ DISSE "edite esse vídeo" (sem dizer o quê), O VÍDEO É O PEDIDO: `primocode video import --json \'{"path":"<arquivo>"}\'`, '
857
+ + 'depois `primocode video auto --json \'{}\'` (lê o que ela FALA na gravação e aplica cada efeito no momento em que diz), inspect, export e abra o mp4. '
858
+ + 'Senão: '
859
+ + 'rode `primocode video help` UMA vez (lista as operações), depois import → remove_silence/remove_words → add_captions → inspect (olhe o png) → export. '
860
+ + 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.\n'
861
+ + 'EFEITOS que seguem a pessoa: separate_layers (recorte + momento 3D), replace_background (fundo novo no estalo), '
862
+ + 'attach_3d (logo 3D na mão, segue a mão), merge_on_join (mãos juntas → fusão → cérebro), cut_silence_at (silêncio depois do estalo), '
863
+ + 'style_captions (cor, legenda em cima da cabeça) — detalhe em `primocode video help efeitos`. Atalho: `primocode video do --json \'{"pedido":"<o pedido em português>"}\'`.' });
864
+ } else if (producao.producao) {
681
865
  /* "SE ESTIVER NA SUA LISTA" não é hesitação — é a diferença entre
682
866
  a tarefa seguir e a tarefa morrer.
683
867
 
@@ -697,8 +881,14 @@ async function runAct(userPrompt, opts) {
697
881
  com as primitivas" — o modelo escrevia posição em pixel e o
698
882
  vídeo saía quebrado, além de custar três ou quatro rodadas a
699
883
  mais de modelo (medido em 25/09). */
884
+ /* A IA DECIDE (01/10): "pedi um roteiro de vídeo e ela já ia fazer o
885
+ motion". A regra só sabe que a palavra "vídeo" apareceu; quem sabe
886
+ se a pessoa quer o vídeo PRONTO ou só o roteiro, a ideia ou uma
887
+ resposta é o modelo. Então isto é o caminho SE for produzir, e não
888
+ mais uma ordem de produzir. */
700
889
  messages.push({ role: 'system', content:
701
- `ESTE PEDIDO É DE PRODUÇÃO (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"). Faça em três passos: `
890
+ 'VOCÊ DECIDE o que entregar, pelo que a pessoa pediu: se ela quer só o roteiro, a ideia, um texto ou uma resposta, ESCREVA isso e pare — não produza vídeo. '
891
+ + `SE ela quer a peça pronta (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"), faça em três passos: `
702
892
  + `estudio_projeto acao:"criar" com formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}" → `
703
893
  + 'estudio_plano com "roteiro" (partes com tipo e fala; o PrimoCode faz layout, voz, ícones, música e sons) → '
704
894
  + 'estudio_render. Se a peça for de uma marca de verdade, estudio_site antes, e use as cores e a logo em roteiro.marca. '
@@ -749,8 +939,25 @@ async function runAct(userPrompt, opts) {
749
939
  const MAX_ESPERAS_REDE = Number(process.env.PRIMOCODE_MAX_ESPERAS_REDE || 6);
750
940
 
751
941
  let voltas = 0;
752
- for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
942
+ const teto = subagent ? MAX_VOLTAS_AGENTE : MAX_ITERATIONS;
943
+ let avisouTeto = false;
944
+ if (!subagent) TRABALHANDO = true;
945
+ try {
946
+ /* O AGENTE TEM TETO PRÓPRIO. No teste da loja (01/10) o "fechar
947
+ pendências do painel" passou de 38 voltas lendo o mesmo app.js aos
948
+ pedaços — a conversa dele inchou para 45 mil caracteres e uma resposta
949
+ levou 22 minutos. Uma parte de um plano cabe em 20 voltas; na 15ª ele
950
+ é avisado para fechar, e no teto volta com a entrega do que fez. */
951
+ for (let iteration = 0; iteration < teto && !PARAR; iteration++) {
753
952
  voltas++;
953
+ if (subagent && !avisouTeto && voltas >= teto - 5) {
954
+ avisouTeto = true;
955
+ messages.push({ role: 'user', content: `Faltam ${teto - voltas + 1} voltas para o seu limite. Feche o essencial do que foi pedido agora, `
956
+ + 'prove (rode o teste ou o comando) e chame finish dizendo o que ficou de fora. Nada de polimento extra.' });
957
+ }
958
+ // Recado digitado durante o passo anterior entra AGORA, antes da ida
959
+ // ao modelo — o resultado das ferramentas já está na conversa.
960
+ if (!subagent) entregarRecados(messages, profundidade);
754
961
  const sp = new Spinner(subagent ? 'subagente trabalhando' : PENSANDO).start();
755
962
 
756
963
  const toolCalls = [];
@@ -769,11 +976,19 @@ async function runAct(userPrompt, opts) {
769
976
  emVoo = new AbortController();
770
977
  const tRodada = Date.now();
771
978
  try {
772
- await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
979
+ const enviadas = compactar(messages);
980
+ traco(`volta ${voltas}${subagent ? ' (agente)' : ''} · ${enviadas.length} mensagens · ${JSON.stringify(enviadas).length} caracteres`);
981
+ await motor.requestAct(server, { messages: enviadas, model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
773
982
  if (evt.erro) { streamError = evt.erro; return; }
774
- if (evt.tool_call) { toolCalls.push(evt.tool_call); return; }
983
+ if (evt.tool_call) {
984
+ toolCalls.push(evt.tool_call);
985
+ const a = evt.tool_call.arguments || {};
986
+ sp.detalhe = `${toolCalls.length} ${toolCalls.length > 1 ? 'ações prontas' : 'ação pronta'} · ${evt.tool_call.name}${a.path ? ' ' + a.path : ''}`;
987
+ return;
988
+ }
775
989
  if (evt.delta) {
776
990
  assistantText += evt.delta;
991
+ sp.detalhe = assistantText.length > 200 ? `${Math.round(assistantText.length / 100) / 10} mil caracteres` : '';
777
992
  if (!subagent && !toolCalls.length) {
778
993
  if (!fluxo && assistantText.length > fala.LIMIAR_PENSAMENTO) {
779
994
  sp.stop();
@@ -787,6 +1002,7 @@ async function runAct(userPrompt, opts) {
787
1002
  } catch (e) {
788
1003
  sp.stop();
789
1004
  emVoo = null;
1005
+ traco(`erro na volta ${voltas}: ${String((e && e.message) || e).slice(0, 200)}`);
790
1006
  if (fluxo) fluxo.fechar();
791
1007
  // Estourou o minuto: encolhe o que já foi feito e CONTINUA, em vez
792
1008
  // de devolver erro e jogar fora o trabalho. O usuário não precisa
@@ -814,6 +1030,7 @@ async function runAct(userPrompt, opts) {
814
1030
  }
815
1031
  sp.stop();
816
1032
  emVoo = null;
1033
+ 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) : ''}`);
817
1034
  tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
818
1035
  if (PARAR) { if (fluxo) fluxo.fechar(); break; }
819
1036
 
@@ -956,7 +1173,7 @@ async function runAct(userPrompt, opts) {
956
1173
  messages.push({
957
1174
  role: 'user',
958
1175
  content: `AINDA NÃO. Abri a página para conferir e ela tem problema:\n${relato}\n\n`
959
- + 'Conserte (leia o arquivo antes de editar) e finalize de novo.',
1176
+ + 'Conserte SÓ o que está apontado, com edit_file no arquivo do erro (leia só ele, se precisar), e finalize de novo.',
960
1177
  });
961
1178
  finalText = '';
962
1179
  continue;
@@ -967,6 +1184,13 @@ async function runAct(userPrompt, opts) {
967
1184
  if (fluxo) fluxo.fechar();
968
1185
  else fala.resposta(finalText);
969
1186
  }
1187
+ /* Ia terminar, mas chegou recado no meio: a tarefa não acaba com
1188
+ a pessoa esperando resposta. A volta seguinte entrega o recado. */
1189
+ if (!subagent && CAIXA.length && !PARAR) {
1190
+ messages.push({ role: 'assistant', content: finalText });
1191
+ finalText = '';
1192
+ continue;
1193
+ }
970
1194
  break;
971
1195
  }
972
1196
  respostasVazias = 0;
@@ -999,13 +1223,13 @@ async function runAct(userPrompt, opts) {
999
1223
  // fila — cada subagente só começava quando o anterior terminava, e a
1000
1224
  // rodada custava a soma dos três. Agora as chamadas spawn_agent da
1001
1225
  // rodada disparam de uma vez (Promise.all) e cada resultado volta na
1002
- // posição certa da conversa. Limite de 4 por rodada: acima disso é
1003
- // sinal de decomposição ruim, e o excedente roda em série mesmo.
1226
+ // posição certa da conversa. Limite de 8 por rodada (era 4: o laço do
1227
+ // Qwen despacha várias direções de uma vez); o excedente roda em série.
1004
1228
  const spawns = toolCalls.filter((tc) => tc.name === 'spawn_agent');
1005
1229
  const resultadosSpawn = new Map();
1006
1230
  if (spawns.length && !PARAR) garantirPasta();
1007
1231
  if (spawns.length > 1 && !PARAR) {
1008
- const juntos = spawns.slice(0, 4);
1232
+ const juntos = spawns.slice(0, 8);
1009
1233
  fala.paralelo(juntos.length, profundidade);
1010
1234
  const rs = await Promise.all(juntos.map((tc) =>
1011
1235
  runSubagent(tc.arguments || {}, {
@@ -1014,6 +1238,22 @@ async function runAct(userPrompt, opts) {
1014
1238
  juntos.forEach((tc, i) => resultadosSpawn.set(tc.id, rs[i]));
1015
1239
  }
1016
1240
 
1241
+ /* LEITURAS JUNTAS. O RITMO pede ao modelo para juntar as ações
1242
+ independentes numa resposta só — e ele junta: três read_file, um
1243
+ grep e uma pesquisa. Só que o laço abaixo rodava uma de cada vez, e
1244
+ a rodada custava a soma. O que só LÊ (não muda arquivo, tela nem
1245
+ conta) roda tudo de uma vez aqui; o laço usa o resultado pronto,
1246
+ passando pelos mesmos freios de sempre (repetição, registro). */
1247
+ const adiantadas = new Map();
1248
+ const leituras = toolCalls.filter(ehLeitura);
1249
+ if (leituras.length > 1 && !PARAR) {
1250
+ const rs = await Promise.all(leituras.slice(0, 8).map((tc) =>
1251
+ tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api })
1252
+ .catch((e) => ({ ok: false, error: e && e.message }))));
1253
+ leituras.slice(0, 8).forEach((tc, i) => adiantadas.set(tc.id, rs[i]));
1254
+ traco(`${adiantadas.size} leituras em paralelo`);
1255
+ }
1256
+
1017
1257
  for (const tc of toolCalls) {
1018
1258
  if (PARAR) break;
1019
1259
  if (tc.name === 'finish') {
@@ -1037,7 +1277,7 @@ async function runAct(userPrompt, opts) {
1037
1277
  role: 'tool',
1038
1278
  tool_call_id: tc.id,
1039
1279
  content: `NÃO terminou. Estas tools falharam e você não corrigiu: ${falhasPendentes.map((f) => f.texto).join('; ')}. `
1040
- + 'Conserte agora (leia o arquivo antes de editar) ou, se for impossível, diga claramente ao usuário o que falhou.',
1280
+ + 'Conserte agora com edit_file no arquivo que falhou ou, se for impossível, diga claramente ao usuário o que falhou.',
1041
1281
  });
1042
1282
  continue;
1043
1283
  }
@@ -1055,7 +1295,7 @@ async function runAct(userPrompt, opts) {
1055
1295
  server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1056
1296
  });
1057
1297
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1058
- messages.push({ role: 'tool', tool_call_id: tc.id, content: r });
1298
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: r + lembreteDeParalelo(todosDaTarefa, spawns.length) });
1059
1299
  continue;
1060
1300
  }
1061
1301
 
@@ -1117,6 +1357,38 @@ async function runAct(userPrompt, opts) {
1117
1357
 
1118
1358
  if (criarPasta && catalogo.criaProjeto(tc.name, tc.arguments)) garantirPasta();
1119
1359
 
1360
+ /* RELER O QUE ACABOU DE ESCREVER. O modelo grátis escreve o
1361
+ index.html e, na volta seguinte, pede para lê-lo "para
1362
+ conferir" — uma ida ao servidor para receber o texto que ele
1363
+ mesmo mandou, e que continua inteiro na chamada write_file dele
1364
+ (o `compactar` só encolhe resultado de ferramenta, nunca o que
1365
+ o modelo escreveu). Se o arquivo está byte a byte como ele
1366
+ deixou, a resposta é essa, sem abrir nada. Depois de um
1367
+ edit_file não vale: aí o texto na conversa está em pedaços. */
1368
+ const relido = tc.name === 'read_file' && tc.arguments && tc.arguments.path
1369
+ ? tc.arguments.path
1370
+ : tc.name === 'run_shell' ? leituraPeloShell(tc.arguments && tc.arguments.command) : null;
1371
+ if (relido && escritosNaTarefa.size) {
1372
+ let igual = false;
1373
+ try {
1374
+ const abs = require('path').isAbsolute(relido) ? relido : tools.resolveInProject(relido, pastaAtual);
1375
+ const e = escritosNaTarefa.get(abs);
1376
+ // Só enquanto a escrita ainda viaja inteira na conversa
1377
+ // (as duas últimas voltas — ver ESCRITAS_INTEIRAS).
1378
+ igual = !!e && voltas - e.volta <= 1 && require('fs').readFileSync(abs, 'utf8') === e.texto;
1379
+ } catch { igual = false; }
1380
+ if (igual) {
1381
+ fala.acao(tc.name, tc.arguments, profundidade);
1382
+ console.log(fala.resultado(profundidade) + c.muted('igual ao que ele escreveu — não precisou ler'));
1383
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({
1384
+ ok: true, igualAoQueVoceEscreveu: true,
1385
+ aviso: 'Este arquivo está EXATAMENTE como você o escreveu nesta tarefa — o conteúdo é o da sua '
1386
+ + 'chamada write_file acima. Não precisa ler. Para mudar, use edit_file direto; se terminou, siga.',
1387
+ }) });
1388
+ continue;
1389
+ }
1390
+ }
1391
+
1120
1392
  if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
1121
1393
  voltasEmFalso++;
1122
1394
  messages.push({
@@ -1138,7 +1410,6 @@ async function runAct(userPrompt, opts) {
1138
1410
  const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
1139
1411
  if (!porta.ok) {
1140
1412
  fala.acao(tc.name, tc.arguments, profundidade);
1141
- fala.avisar('resultado', { nome: tc.name, ok: false, resumo: 'não autorizado' });
1142
1413
  console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
1143
1414
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
1144
1415
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: porta.error }) });
@@ -1156,7 +1427,6 @@ async function runAct(userPrompt, opts) {
1156
1427
  });
1157
1428
  if (!risco.ok) {
1158
1429
  fala.acao(tc.name, tc.arguments, profundidade);
1159
- fala.avisar('resultado', { nome: tc.name, ok: false, resumo: `não autorizado — ${risco.motivo}` });
1160
1430
  console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
1161
1431
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
1162
1432
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ ok: false, error: risco.error }) });
@@ -1174,13 +1444,24 @@ async function runAct(userPrompt, opts) {
1174
1444
  /* O servidor, o token e o cliente da API vão junto: a legenda do
1175
1445
  estudio_video transcreve no servidor, e sem eles a ferramenta
1176
1446
  respondia "a transcrição precisa do servidor". */
1177
- const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api });
1447
+ traco(`executando ${tc.name}`);
1448
+ const result = adiantadas.has(tc.id) ? adiantadas.get(tc.id)
1449
+ : await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api });
1450
+ traco(`${tc.name} devolveu ok=${result && result.ok}`);
1178
1451
  logResult(tc.name, result, profundidade);
1179
1452
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1180
1453
  if (RELEITURA.has(tc.name) && result && result.ok !== false) {
1181
1454
  leiturasGuardadas.set(eco.chave, summarizeForModel(result));
1182
1455
  }
1183
1456
  if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
1457
+ if (/^(write_file|edit_file|multi_edit)$/.test(tc.name) && tc.arguments && tc.arguments.path) {
1458
+ try {
1459
+ const abs = tools.resolveInProject(tc.arguments.path, pastaAtual);
1460
+ if (tc.name === 'write_file' && result && result.ok !== false && typeof tc.arguments.content === 'string') {
1461
+ escritosNaTarefa.set(abs, { texto: tc.arguments.content, volta: voltas });
1462
+ } else escritosNaTarefa.delete(abs);
1463
+ } catch { /* caminho inválido: a tool já recusou */ }
1464
+ }
1184
1465
  // Além de narrar, ANOTA: no fim do pedido sai o balanço do que
1185
1466
  // nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
1186
1467
  // tarefa de trinta ações, a primeira já saiu da tela.
@@ -1270,9 +1551,14 @@ async function runAct(userPrompt, opts) {
1270
1551
  if (finished) {
1271
1552
  finalText = finished;
1272
1553
  if (!subagent) fala.resposta(finished);
1554
+ // Recado chegou enquanto ele fechava: entrega e segue.
1555
+ if (!subagent && CAIXA.length && !PARAR) { finalText = ''; continue; }
1273
1556
  break;
1274
1557
  }
1275
1558
  }
1559
+ } finally {
1560
+ if (!subagent) TRABALHANDO = false;
1561
+ }
1276
1562
 
1277
1563
  // O que o modelo entregou por vontade própria, ANTES dos textos de
1278
1564
  // desistência. É esta a diferença que o plano de passos precisa: um passo
@@ -1289,7 +1575,7 @@ async function runAct(userPrompt, opts) {
1289
1575
  // não dá nem para saber que acabou. Medido: um pedido de navegador morria assim
1290
1576
  // depois de a primeira tentativa de seletor falhar.
1291
1577
  if (!finalText) {
1292
- finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS });
1578
+ finalText = motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou: rodadasParadas >= MAX_RODADAS_PARADAS, teto });
1293
1579
  if (!subagent) fala.aviso(finalText);
1294
1580
  }
1295
1581
 
@@ -1367,7 +1653,42 @@ async function runSubagent(args, ctx) {
1367
1653
 
1368
1654
  const resumo = r.assistantMessage.content || 'sem resultado';
1369
1655
  fala.agenteVoltou(resumo, profundidade);
1370
- return resumo;
1656
+ return entregaDoAgente(especialistas.rotulo(esp), resumo, r.registro);
1657
+ }
1658
+
1659
+ /* UM AGENTE DE CADA VEZ é o padrão do modelo grátis: no teste da loja
1660
+ (API + site + painel, 01/10) ele despachou os três em fila — 6 + 8 + 5
1661
+ minutos — quando o site e o painel só dependiam do contrato, que já
1662
+ existia. Quem decide continua sendo ele; a volta só lembra que as partes
1663
+ que faltam podem ir JUNTAS. Pura. */
1664
+ function lembreteDeParalelo(todos, naRodada) {
1665
+ const faltam = (todos || []).filter((t) => t && t.status !== 'completed' && t.status !== 'concluida');
1666
+ if (naRodada > 1 || faltam.length < 2) return '';
1667
+ return `\n\n(Faltam ${faltam.length} partes do plano. As que não dependem uma da outra vão JUNTAS: `
1668
+ + 'vários spawn_agent na MESMA resposta rodam em paralelo.)';
1669
+ }
1670
+
1671
+ /* O SALTO ENTRE AGENTES. O pai recebia só o texto final do especialista —
1672
+ "pronto, fiz a API" — e, para passar a vez ao próximo (o do front, o
1673
+ revisor), tinha de adivinhar o que existia. Agora a volta é uma ENTREGA:
1674
+ quem fez, o que disse, os arquivos que nasceram e mudaram, os comandos, o
1675
+ que falhou. É o contexto que o próximo especialista precisa no prompt
1676
+ dele, e é o que deixa o pai mandar a falha de volta para conserto (o laço
1677
+ do Qwen: planeja → despacha → confere → conserta). Pura, para o teste. */
1678
+ function entregaDoAgente(quem, texto, reg) {
1679
+ const linhas = [`ENTREGA DO ${String(quem).toUpperCase()}`, String(texto).trim().slice(0, 4000)];
1680
+ if (reg) {
1681
+ const lista = (nome, xs) => { if (xs && xs.length) linhas.push(`${nome}: ${xs.slice(0, 15).join(', ')}`); };
1682
+ lista('criou', reg.criados);
1683
+ lista('alterou', reg.alterados);
1684
+ lista('rodou', (reg.comandos || []).map((x) => `${x.comando}${x.ok === false ? ` (saiu ${x.code})` : ''}`));
1685
+ lista('testes', (reg.testes || []).map((x) => `${x.comando}: ${x.ok ? 'passou' : 'FALHOU'}`));
1686
+ if (reg.falhas && reg.falhas.length) linhas.push('FALHOU: ' + reg.falhas.slice(0, 6).map((f) => `${f.acao} ${f.alvo}: ${f.motivo}`.slice(0, 200)).join(' | '));
1687
+ }
1688
+ linhas.push(reg && ((reg.falhas && reg.falhas.length) || (reg.testes || []).some((x) => !x.ok))
1689
+ ? 'PRÓXIMO: há falha acima — mande de volta para conserto (spawn_agent com o erro no prompt) ou conserte você, antes de seguir.'
1690
+ : 'PRÓXIMO: se outra parte depende desta, passe a vez (spawn_agent) com esta entrega no prompt; senão, confira e siga.');
1691
+ return linhas.join('\n');
1371
1692
  }
1372
1693
 
1373
1694
  /* ══ O PIPELINE DE QUATRO PAPÉIS ═══════════════════════════════════════════
@@ -1480,5 +1801,5 @@ async function rodarPipeline(args, ctx) {
1480
1801
  });
1481
1802
  }
1482
1803
 
1483
- module.exports = { runAct, buildMemoryContext, interromper, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
1804
+ module.exports = { runAct, entregaDoAgente, lembreteDeParalelo, buildMemoryContext, leituraPeloShell, historicoEnxuto, interromper, mandarDuranteATarefa, trabalhando, sobrasDaCaixa, entregarRecados, compactar, motivoDaParada, pareceParadaNoMeio, ehTetoDeToken, ehTransitorio, esperaDoErro,
1484
1805
  anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };