primocode 9.8.0-beta.6 → 9.8.0-beta.7

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v9.8.0-beta.6
1
+ # PrimoCode v9.8.0-beta.7
2
2
 
3
3
  Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
4
4
  Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
9
9
  está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
10
10
 
11
11
  ```
12
- ▐▛███▜▌ Primo Code v9.8.0-beta.6
12
+ ▐▛███▜▌ Primo Code v9.8.0-beta.7
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/bin/primocode.js CHANGED
@@ -53,6 +53,7 @@ extra.instalarContador(api, contadorDaSessao, () => {
53
53
  const p = provedorLocal.atual();
54
54
  return p ? p.nome : 'Servidor PrimoCode';
55
55
  });
56
+ const tempos = require('../lib/tempos');
56
57
 
57
58
  // Rede de segurança de último recurso: sem isto, uma exceção fora de um
58
59
  // try/catch (ex.: dentro de um callback de socket, como o parser de SSE) não
@@ -1055,6 +1056,16 @@ async function sendPrompt(userText) {
1055
1056
  return;
1056
1057
  }
1057
1058
 
1059
+ // PRIMOCODE_TEMPOS=1: uma linha no fim com o custo de cada fase.
1060
+ tempos.comecar();
1061
+ try { await enviarPedido(userText); }
1062
+ finally {
1063
+ const linha = tempos.relatorio();
1064
+ if (linha) console.log(c.dim(` ⏱ ${linha}`));
1065
+ }
1066
+ }
1067
+
1068
+ async function enviarPedido(userText) {
1058
1069
  let prompt = userText;
1059
1070
  // "@index.html" no pedido é um /attach só deste pedido: vai junto dos
1060
1071
  // anexos, mas não fica em state.attachedFiles para o próximo.
@@ -1091,15 +1102,22 @@ async function sendPrompt(userText) {
1091
1102
 
1092
1103
  // O resumo da rodada anterior é feito em segundo plano logo depois
1093
1104
  // dela; aqui só se espera ele terminar, para não sair com memória velha.
1094
- await esperarResumo();
1095
-
1105
+ //
1096
1106
  // O pedido passa por uma cabeça antes de virar trabalho: conserta o
1097
1107
  // que está torto, diz qual é o entregável e o que conta como pronto.
1098
1108
  // UMA chamada faz as duas coisas: arruma o pedido torto e, se fizer
1099
1109
  // sentido, divide em passos. Eram duas idas ao modelo por pedido, e
1100
1110
  // num plano limitado por REQUISIÇÕES (o grátis da Groq são 30 por
1101
1111
  // minuto) cada chamada extra custa tanto quanto token.
1102
- const passos = await dividirTarefa(prompt);
1112
+ //
1113
+ // As duas esperas correm JUNTAS. Uma não lê o que a outra escreve: o
1114
+ // plano só olha o texto do pedido, e o resumo só a conversa de antes.
1115
+ // Em fila, o pedido de duas fases pagava o resumo e DEPOIS o plano —
1116
+ // duas idas ao modelo em série antes da primeira rodada.
1117
+ const [, passos] = await Promise.all([
1118
+ tempos.medir('espera do resumo', () => esperarResumo()),
1119
+ tempos.medir('plano', () => dividirTarefa(prompt)),
1120
+ ]);
1103
1121
  const instrucaoKit = await prepararKit(plan, userText);
1104
1122
 
1105
1123
  let r = null;
@@ -1214,8 +1232,8 @@ async function sendPrompt(userText) {
1214
1232
  state.gravador.turno(userText, r.assistantMessage.content,
1215
1233
  { modo: 'primocode', projeto: state.currentProject || null });
1216
1234
  } else {
1217
- await esperarResumo();
1218
- await runChat(prompt, userText);
1235
+ await tempos.medir('espera do resumo', () => esperarResumo());
1236
+ await tempos.medir('chat', () => runChat(prompt, userText));
1219
1237
  }
1220
1238
 
1221
1239
  state.attachedFiles = [];
package/lib/act.js CHANGED
@@ -24,6 +24,8 @@ const permissao = require('./permissao');
24
24
  const perigo = require('./perigo');
25
25
  const skills = require('./skills');
26
26
  const checkpoint = require('./checkpoint');
27
+ const tempos = require('./tempos');
28
+ const preLeitura = require('./pre-leitura');
27
29
 
28
30
  // 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
29
31
  // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
@@ -35,6 +37,8 @@ const MAX_EMPURROES = 3;
35
37
  // Voltas seguidas sem executar nada antes de desistir.
36
38
  const MAX_RODADAS_PARADAS = 3;
37
39
  const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
40
+ // As leituras que, repetidas sem mudança no meio, devolvem o guardado.
41
+ const RELEITURA = new Set(['read_file', 'list_dir']);
38
42
 
39
43
  // ── Interrupção (Esc Esc) ─────────────────────────────────────────────────
40
44
  // O usuário aperta Esc duas vezes e o agente PARA — entre uma rodada e outra
@@ -382,6 +386,7 @@ async function pausar(ms, subagent) {
382
386
  const sp = subagent ? null : new Spinner(PENSANDO).start();
383
387
  await new Promise((r) => setTimeout(r, ms));
384
388
  if (sp) sp.stop();
389
+ tempos.somar('espera de cota/rede', ms);
385
390
  }
386
391
 
387
392
  /**
@@ -451,10 +456,10 @@ function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
451
456
  const PULAR_NO_MAPA = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'out',
452
457
  '.cache', 'coverage', '__pycache__', '.venv', 'venv', '.turbo', '.vercel', '.idea', '.vscode']);
453
458
 
454
- function mapaDoProjeto(dir, limite = 80) {
459
+ function arquivosDoProjeto(dir, limite = 80) {
455
460
  const path = require('path');
456
461
  const fs = require('fs');
457
- if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return '';
462
+ if (!dir || path.resolve(dir) === path.resolve(cfg.ROOT_DIR)) return [];
458
463
  const achados = [];
459
464
  const andar = (atual, fundo) => {
460
465
  if (fundo > 4 || achados.length >= limite) return;
@@ -470,12 +475,18 @@ function mapaDoProjeto(dir, limite = 80) {
470
475
  }
471
476
  };
472
477
  andar(dir, 0);
478
+ return achados;
479
+ }
480
+
481
+ function mapaDoProjeto(dir, limite = 80, achados = arquivosDoProjeto(dir, limite)) {
473
482
  if (!achados.length) return '';
474
483
  const mais = achados.length >= limite ? `\n… (mais arquivos — use glob para achar)` : '';
475
484
  return `Arquivos que JÁ existem neste projeto (edite-os em vez de recriar):\n${achados.join('\n')}${mais}`;
476
485
  }
477
486
 
478
- function buildMemoryContext(projectDir, projectName) {
487
+ /* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
488
+ entrar já lido (ver lib/pre-leitura.js). */
489
+ function buildMemoryContext(projectDir, projectName, pedido = '') {
479
490
  const parts = [];
480
491
  // As instruções vêm primeiro: são regra de quem manda no projeto, e a
481
492
  // memória é o que o agente anotou. Entram aqui (e não no bin) para valer
@@ -486,8 +497,11 @@ function buildMemoryContext(projectDir, projectName) {
486
497
  if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
487
498
  const projeto = cfg.readProjectMemory(projectDir);
488
499
  if (projeto.trim()) parts.push(`[${projectName || 'projeto'}/memory.md]\n${projeto}`);
489
- const mapa = mapaDoProjeto(projectDir);
490
- if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}`);
500
+ const arquivos = arquivosDoProjeto(projectDir);
501
+ const mapa = mapaDoProjeto(projectDir, 80, arquivos);
502
+ if (mapa) parts.push(`[${projectName || 'projeto'}/arquivos]\n${mapa}\n\n${preLeitura.JUNTAR_LEITURAS}`);
503
+ const lidos = preLeitura.bloco(preLeitura.escolher(projectDir, arquivos, pedido));
504
+ if (lidos) parts.push(lidos);
491
505
  return parts.join('\n\n');
492
506
  }
493
507
 
@@ -520,6 +534,11 @@ async function runAct(userPrompt, opts) {
520
534
  /* Quem abre a página para conferir antes do "pronto". Injetável pelo
521
535
  mesmo motivo do `motor`: o teste mede a regra sem abrir um Chrome. */
522
536
  medirPagina = (dir) => require('./pipeline.js').medirNoNavegador(dir),
537
+ /* Quem LIGA o navegador escondido antes da hora. Injetável pelo mesmo
538
+ motivo do `medirPagina` — e só vale junto com o padrão dele: um
539
+ teste que troca o medidor não pode subir um Chrome de verdade. */
540
+ aquecerPagina = opts && opts.medirPagina ? null
541
+ : () => require('./browser.js').aquecerRevisor(),
523
542
  } = opts || {};
524
543
 
525
544
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
@@ -550,6 +569,7 @@ async function runAct(userPrompt, opts) {
550
569
  let mexeuNaPagina = false; // mexeu desde a última conferência?
551
570
  let conferencias = 0; // até duas: entregar, e conferir o conserto
552
571
  let avisoDaPagina = ''; // o que ainda estava quebrado na última
572
+ let msConferindo = 0; // para o PRIMOCODE_TEMPOS separar as fases
553
573
  /* Abre a página e devolve o relato quando é para VOLTAR (achou problema e
554
574
  ainda cabe uma volta); null quando é para entregar. O que ficar
555
575
  quebrado na última conferência fica em `avisoDaPagina`, e a entrega sai
@@ -560,10 +580,13 @@ async function runAct(userPrompt, opts) {
560
580
  conferencias++;
561
581
  mexeuNaPagina = false;
562
582
  let medida = null;
583
+ const t = Date.now();
563
584
  try {
564
585
  fala.pensamento('abrindo a página para conferir antes de entregar', profundidade);
565
586
  medida = await medirPagina(pastaAtual);
566
587
  } catch { medida = null; }
588
+ msConferindo += Date.now() - t;
589
+ tempos.somar('conferir página', Date.now() - t);
567
590
  if (!medida || !medida.testou) return null;
568
591
  avisoDaPagina = medida.limpo ? ''
569
592
  : String(medida.relato || '').split('\n').filter((l) => /·|BRANCO/.test(l)).slice(0, 3).join(' ');
@@ -574,6 +597,8 @@ async function runAct(userPrompt, opts) {
574
597
  ? `${texto}\n\nAtenção: abri a página e ela ainda tem problema — ${avisoDaPagina.trim()}` : texto);
575
598
  // Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
576
599
  const jaChamado = repeticao.novo();
600
+ // O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
601
+ const leiturasGuardadas = new Map();
577
602
  /* O livro deste pedido: arquivos, comandos, links, testes e falhas. Sai
578
603
  no fim como balanço (ver lib/registro.js). O subagente tem o seu, que
579
604
  não se mistura com o do principal — mas o que ele fez também precisa
@@ -623,7 +648,7 @@ async function runAct(userPrompt, opts) {
623
648
 
624
649
  const messages = [];
625
650
  if (!subagent) {
626
- const memory = buildMemoryContext(projectDir, projectName);
651
+ const memory = buildMemoryContext(projectDir, projectName, userPrompt);
627
652
  if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
628
653
  for (const m of (history || []).slice(-20)) {
629
654
  if (m && m.role && m.content) messages.push({ role: m.role, content: m.content });
@@ -735,6 +760,7 @@ async function runAct(userPrompt, opts) {
735
760
  let fluxo = null;
736
761
 
737
762
  emVoo = new AbortController();
763
+ const tRodada = Date.now();
738
764
  try {
739
765
  await motor.requestAct(server, { messages: compactar(messages), model, effort, token, projectDir: pastaAtual, projectName, subagent, sinal: emVoo.signal }, (evt) => {
740
766
  if (evt.erro) { streamError = evt.erro; return; }
@@ -781,6 +807,7 @@ async function runAct(userPrompt, opts) {
781
807
  }
782
808
  sp.stop();
783
809
  emVoo = null;
810
+ tempos.somar(subagent ? 'rodada do subagente' : 'rodada do modelo', Date.now() - tRodada);
784
811
  if (PARAR) { if (fluxo) fluxo.fechar(); break; }
785
812
 
786
813
  // Chamada de função malformada: o modelo errou a SINTAXE da chamada,
@@ -957,6 +984,8 @@ async function runAct(userPrompt, opts) {
957
984
 
958
985
  let finished = null;
959
986
  let algoRodou = false;
987
+ const tTools = Date.now();
988
+ const conferindoAntes = msConferindo;
960
989
 
961
990
  // ORQUESTRAÇÃO: subagentes da MESMA rodada rodam juntos. O modelo que
962
991
  // pede "3 agentes, um por arquivo" esperava paralelismo e recebia uma
@@ -1064,7 +1093,18 @@ async function runAct(userPrompt, opts) {
1064
1093
  const eco = repeticao.ver(jaChamado, tc.name, tc.arguments);
1065
1094
  if (eco.repetida) {
1066
1095
  voltasEmFalso++;
1067
- messages.push({ role: 'tool', tool_call_id: tc.id, content: eco.aviso });
1096
+ /* RELER SEM MUDANÇA devolve o que já foi lido, uma vez.
1097
+ O aviso seco ("o resultado está na conversa acima") mentia
1098
+ depois do `compactar`: a leitura velha já tinha virado 300
1099
+ caracteres, e o modelo gastava OUTRA ida ao servidor para
1100
+ achar o arquivo por outro caminho (um `cat`, um grep). Na
1101
+ segunda vez vai o conteúdo guardado — ler de novo sem nada
1102
+ mudar dá o mesmo texto, então não há o que executar. Da
1103
+ terceira em diante é laço, e o freio segue como era. */
1104
+ const guardado = eco.vezes === 2 && leiturasGuardadas.get(eco.chave);
1105
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: guardado
1106
+ ? `(sem mudança desde a última leitura — é o mesmo conteúdo; não precisa ler de novo)\n${guardado}`
1107
+ : eco.aviso });
1068
1108
  continue;
1069
1109
  }
1070
1110
 
@@ -1125,6 +1165,9 @@ async function runAct(userPrompt, opts) {
1125
1165
  const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
1126
1166
  logResult(tc.name, result, profundidade);
1127
1167
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1168
+ if (RELEITURA.has(tc.name) && result && result.ok !== false) {
1169
+ leiturasGuardadas.set(eco.chave, summarizeForModel(result));
1170
+ }
1128
1171
  if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
1129
1172
  // Além de narrar, ANOTA: no fim do pedido sai o balanço do que
1130
1173
  // nasceu, mudou, rodou e falhou. Narrar sozinho não basta — numa
@@ -1133,6 +1176,19 @@ async function runAct(userPrompt, opts) {
1133
1176
  if (result && result.ok !== false && catalogo.criaProjeto(tc.name, tc.arguments)
1134
1177
  && /\.(html?|css|m?jsx?|tsx?|vue|svelte)$/i.test(String((tc.arguments && tc.arguments.path) || ''))) {
1135
1178
  mexeuNaPagina = true;
1179
+ /* O NAVEGADOR ESCONDIDO SOBE AGORA, e não no `finish`.
1180
+ A conferência só roda quando o agente diz "pronto", e até
1181
+ lá ainda há pelo menos uma rodada do modelo. Subir o Chrome
1182
+ ali, em paralelo com ela, tira do "pronto" o tempo de ligar
1183
+ o navegador e de fazer a PRIMEIRA navegação dele, que é a
1184
+ parte cara (ver `aquecerRevisor` em lib/browser.js). Medido
1185
+ em 24/09: a conferência num Chrome recém-aberto levava de
1186
+ 2,1 a 4,6s; aquecido antes, 1,24s — que é o assentar de
1187
+ 1,2s, e nada mais. Sem `await` e sem erro: se não subir, a
1188
+ conferência tenta de novo e diz o que houve. */
1189
+ if (aquecerPagina && !subagent && conferencias < 2) {
1190
+ try { Promise.resolve(aquecerPagina()).catch(() => {}); } catch { /* segue */ }
1191
+ }
1136
1192
  }
1137
1193
 
1138
1194
  // Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
@@ -1174,6 +1230,9 @@ async function runAct(userPrompt, opts) {
1174
1230
  messages.push({ role: 'tool', tool_call_id: tc.id, content: summarizeForModel(result) });
1175
1231
  }
1176
1232
 
1233
+ // A conferência da página tem linha própria: aqui fica só o resto.
1234
+ tempos.somar('ferramentas', Date.now() - tTools - (msConferindo - conferindoAntes));
1235
+
1177
1236
  // Uma volta inteira sem executar nada é o modelo preso. Insistir custa
1178
1237
  // um pedido ao servidor por volta, e as 15 voltas saíam caras para
1179
1238
  // terminar no mesmo lugar — melhor parar e devolver a vez ao usuário.
package/lib/browser.js CHANGED
@@ -821,6 +821,7 @@ function close() {
821
821
  let PORTA_REVISOR = Number(process.env.PRIMOCODE_CDP_PORT_REVISOR || 0);
822
822
  let perfilRevisor = null;
823
823
  let revisorChild = null;
824
+ let saidaRegistrada = false;
824
825
 
825
826
  function lerPortaDoPerfil(perfil) {
826
827
  try {
@@ -830,7 +831,20 @@ function lerPortaDoPerfil(perfil) {
830
831
  } catch { return 0; }
831
832
  }
832
833
 
833
- async function subirRevisor() {
834
+ /* Quem está subindo o revisor AGORA. O agente liga o navegador escondido
835
+ adiantado, quando escreve a primeira página (lib/act.js), e a conferência
836
+ do "pronto" chama isto de novo — às vezes com o primeiro ainda subindo.
837
+ Sem esta promessa em comum, a segunda chamada via PORTA_REVISOR zerada e
838
+ lançava um SEGUNDO Chrome, que ficava órfão. */
839
+ let subindoRevisor = null;
840
+
841
+ function subirRevisor() {
842
+ if (subindoRevisor) return subindoRevisor;
843
+ subindoRevisor = _subirRevisor().finally(() => { subindoRevisor = null; });
844
+ return subindoRevisor;
845
+ }
846
+
847
+ async function _subirRevisor() {
834
848
  if (PORTA_REVISOR) {
835
849
  try {
836
850
  const v = await httpGetJson(`http://127.0.0.1:${PORTA_REVISOR}/json/version`, 800);
@@ -880,6 +894,17 @@ async function subirRevisor() {
880
894
  revisorChild = spawn(chrome, args, { detached: true, stdio: 'ignore' });
881
895
  revisorChild.on('error', () => {});
882
896
  revisorChild.unref();
897
+ /* O REVISOR MORRE COM O PROCESSO. Ele é `detached` + `unref` para não
898
+ segurar o CLI de pé — e por isso mesmo sobrevivia a ele: só o pipeline
899
+ (construir_app) chamava fecharRevisor, e toda sessão que conferiu uma
900
+ página deixava um Chrome invisível para trás. Medido em 24/09 nesta
901
+ máquina: 7 revisores órfãos somando ~2 GB de memória, um deles com 3
902
+ minutos de CPU. É o tipo de lentidão que a pessoa sente no computador
903
+ inteiro e não liga ao PrimoCode. */
904
+ if (!saidaRegistrada) {
905
+ saidaRegistrada = true;
906
+ process.on('exit', () => { try { fecharRevisor(); } catch { /* saindo */ } });
907
+ }
883
908
 
884
909
  const prazo = Date.now() + 15000;
885
910
  while (Date.now() < prazo) {
@@ -1110,6 +1135,44 @@ async function abrirEscondido(url) {
1110
1135
  /* O revisor é ferramenta de uma tarefa, não um processo que fica. Deixá-lo de
1111
1136
  pé come memória até a pessoa desligar o computador, e ele não é o navegador
1112
1137
  dela — ninguém sentiria falta nem saberia que existe para fechar. */
1138
+ /* LIGAR O REVISOR ANTES DA HORA — e não só ligar: navegar uma vez.
1139
+ *
1140
+ * Medido em 24/09, cada fase da conferência num Chrome recém-aberto:
1141
+ * subir 313ms · abrir aba 24ms · canal 29ms · PRIMEIRA navegação 3.019ms ·
1142
+ * assentar 1.200ms. Na segunda página, no mesmo Chrome, a navegação levou
1143
+ * 4ms. O caro não é o processo, é a primeira ida à rede (o Chrome sobe o
1144
+ * serviço de rede e o primeiro renderizador nela) — navegar para about:blank
1145
+ * ou data: não aquece isso (a conferência seguinte continuou em ~2,1s);
1146
+ * navegar para um http local aquece (caiu para 1.240ms, que é o assentar).
1147
+ *
1148
+ * Por isso isto abre uma aba num servidor de uma linha, em 127.0.0.1, e fecha.
1149
+ * Uma vez por navegador: depois da primeira, não há o que aquecer. Nunca
1150
+ * rejeita — aquecer é atalho, e quem confere de verdade tenta de novo e diz
1151
+ * o que houve. */
1152
+ let aquecido = null; // a porta do revisor que já foi aquecido
1153
+ let aquecendo = null;
1154
+ function aquecerRevisor() {
1155
+ if (aquecendo) return aquecendo;
1156
+ aquecendo = (async () => {
1157
+ await subirRevisor();
1158
+ if (aquecido && aquecido === PORTA_REVISOR) return true;
1159
+ const srv = http.createServer((req, res) => {
1160
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1161
+ res.end('<!doctype html><title>.</title>');
1162
+ });
1163
+ try {
1164
+ await new Promise((ok, falhou) => { srv.once('error', falhou); srv.listen(0, '127.0.0.1', ok); });
1165
+ const aba = await abaEscondida(`http://127.0.0.1:${srv.address().port}/`, { assentar: 0, prazoLoad: 10000 });
1166
+ await aba.fechar();
1167
+ aquecido = PORTA_REVISOR;
1168
+ return true;
1169
+ } finally {
1170
+ try { srv.close(); } catch { /* já fechou */ }
1171
+ }
1172
+ })().catch(() => false).finally(() => { aquecendo = null; });
1173
+ return aquecendo;
1174
+ }
1175
+
1113
1176
  function fecharRevisor() {
1114
1177
  if (revisorChild && revisorChild.pid) {
1115
1178
  try { process.kill(-revisorChild.pid); } catch {
@@ -1132,5 +1195,5 @@ module.exports = {
1132
1195
  scriptDaSeta, verSeta, findChrome,
1133
1196
  open, click, clickText, clickSelector, type, typeInSelector,
1134
1197
  key, screenshot, evaluate, waitFor, wait, scroll, close,
1135
- inspecionarEscondido, rodarEscondido, abrirEscondido, fecharRevisor,
1198
+ inspecionarEscondido, rodarEscondido, abrirEscondido, fecharRevisor, subirRevisor, aquecerRevisor,
1136
1199
  };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * pre-leitura.js — os arquivos que o agente ia ler, já lidos.
3
+ *
4
+ * "Toda vez a IA lê as pastas novamente, só pra ler vão 30 chamadas de API."
5
+ * (dono, 24/09/2026)
6
+ *
7
+ * O caro não é o disco — ler index.html leva 1ms. O caro é a IDA AO MODELO: o
8
+ * servidor grátis conta pedidos por dia, e cada "deixa eu ler o style.css"
9
+ * numa resposta própria é um pedido. Medido com um modelo de mentira que
10
+ * segue uma política fixa (lê o que ainda não viu, um arquivo por resposta,
11
+ * e confere o index.html antes de editar), num "muda a cor do botão" em
12
+ * projeto de 10 arquivos: 9 pedidos ao modelo antes, 2 depois. Com todos os
13
+ * arquivos grandes demais para entrar aqui, a ordem de juntar leituras e a
14
+ * releitura sem mudança (lib/act.js, RELEITURA) ainda levam de 9 para 4.
15
+ *
16
+ * O mapa do projeto já dava os NOMES. Isto põe o CONTEÚDO dos poucos arquivos
17
+ * que quase todo pedido de página lê — o citado no pedido primeiro, depois
18
+ * index.html, o css e o js principais, package.json, README — dentro de um
19
+ * orçamento. Pequeno pelo servidor grátis (o teto é de ~6 mil tokens por
20
+ * MINUTO, e isto vai em toda rodada); folgado com um provedor próprio.
21
+ *
22
+ * Fica de fora o que não se lê como texto ou não vale o espaço: binário,
23
+ * minificado, lockfile, arquivo grande, e o que já entra por outro caminho
24
+ * (memory.md, PRIMO.md/AGENTS.md/CLAUDE.md). Arquivo que não cabe inteiro no
25
+ * que sobrou do orçamento NÃO entra cortado: meio arquivo marcado como
26
+ * "conteúdo atual" faria o modelo editar às cegas a metade que não viu.
27
+ *
28
+ * Puro, menos a leitura do disco — o teste mede a escolha sem rede.
29
+ */
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+
35
+ const ORCAMENTO_SERVIDOR = 6 * 1024;
36
+ const ORCAMENTO_LOCAL = 40 * 1024;
37
+ const MAIOR_ARQUIVO = 20 * 1024;
38
+
39
+ const TEXTO = /\.(html?|css|scss|m?jsx?|tsx?|vue|svelte|json|md|txt|py|toml|ya?ml)$/i;
40
+ const LOCKFILES = new Set(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb',
41
+ 'composer.lock', 'Cargo.lock', 'poetry.lock', 'Gemfile.lock']);
42
+ // Já entram no contexto por outro caminho (buildMemoryContext / mencoes.js).
43
+ const JA_ENTRAM = new Set(['memory.md', 'PRIMO.md', 'AGENTS.md', 'CLAUDE.md']);
44
+
45
+ /** O orçamento de agora: folgado só com provedor próprio (/api). */
46
+ function orcamento() {
47
+ try { return require('./local/provedor.js').atual() ? ORCAMENTO_LOCAL : ORCAMENTO_SERVIDOR; }
48
+ catch { return ORCAMENTO_SERVIDOR; }
49
+ }
50
+
51
+ /** Quanto o arquivo importa: menor é mais importante; null = não entra. */
52
+ function prioridade(rel, pedido) {
53
+ const nome = path.basename(rel);
54
+ if (JA_ENTRAM.has(nome) || LOCKFILES.has(nome) || /\.min\./i.test(nome) || !TEXTO.test(nome)) return null;
55
+ const fundo = rel.split(/[\\/]/).length - 1;
56
+ const texto = String(pedido || '').toLowerCase();
57
+ // Citado no pedido ("muda o style.css") é o que ele vai ler com certeza.
58
+ if (texto && (texto.includes(rel.toLowerCase()) || texto.includes(nome.toLowerCase()))) return 0;
59
+ if (fundo > 1) return null;
60
+ if (/^index\.html?$/i.test(nome)) return 1 + fundo;
61
+ if (/\.(css|scss)$/i.test(nome)) return 3 + fundo;
62
+ if (/\.(m?jsx?|tsx?|vue|svelte)$/i.test(nome) && !/\.(test|spec|config)\./i.test(nome)) return 5 + fundo;
63
+ if (nome === 'package.json' && fundo === 0) return 7;
64
+ if (/^readme\.md$/i.test(nome) && fundo === 0) return 8;
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Escolhe e lê. Devolve [{ rel, conteudo }] na ordem de importância.
70
+ * @param {string} dir a pasta do projeto
71
+ * @param {string[]} arquivos caminhos relativos (o mesmo que o mapa listou)
72
+ * @param {string} pedido o texto do pedido — o citado nele vem primeiro
73
+ * @param {number} limite bytes no total
74
+ */
75
+ function escolher(dir, arquivos, pedido, limite = orcamento()) {
76
+ const candidatos = (arquivos || [])
77
+ .map((rel) => ({ rel, p: prioridade(rel, pedido) }))
78
+ .filter((x) => x.p !== null)
79
+ .sort((a, b) => a.p - b.p || a.rel.localeCompare(b.rel));
80
+ const lidos = [];
81
+ let soma = 0;
82
+ for (const { rel } of candidatos) {
83
+ const abs = path.join(dir, rel);
84
+ let st;
85
+ try { st = fs.statSync(abs); } catch { continue; }
86
+ if (!st.isFile() || st.size === 0 || st.size > MAIOR_ARQUIVO || soma + st.size > limite) continue;
87
+ let buf;
88
+ try { buf = fs.readFileSync(abs); } catch { continue; }
89
+ if (buf.includes(0)) continue; // binário com nome de texto
90
+ const conteudo = buf.toString('utf-8');
91
+ // Uma linha de milhares de caracteres é minificado sem ".min" no nome.
92
+ if (conteudo.split('\n').some((l) => l.length > 2000)) continue;
93
+ lidos.push({ rel, conteudo });
94
+ soma += st.size;
95
+ }
96
+ return lidos;
97
+ }
98
+
99
+ /** O bloco que vai no contexto, ou '' quando não há nada que valha. */
100
+ function bloco(lidos) {
101
+ if (!lidos || !lidos.length) return '';
102
+ const partes = lidos.map(({ rel, conteudo }) =>
103
+ `[conteúdo atual de ${rel} — não precisa ler de novo]\n${conteudo.replace(/\s+$/, '')}\n[fim de ${rel}]`);
104
+ return 'Estes arquivos estão aqui como estavam no COMEÇO deste pedido — use o que está abaixo em vez de '
105
+ + 'chamar read_file neles. Depois que você editar um deles, o texto abaixo fica velho: aí, sim, leia.\n\n'
106
+ + partes.join('\n\n');
107
+ }
108
+
109
+ /* A ordem de juntar. Vai junto do mapa porque é ali que o modelo decide o que
110
+ ler. Cada resposta dele é uma ida ao servidor; cinco leituras em cinco
111
+ respostas custam cinco pedidos, e as mesmas cinco numa resposta só custam
112
+ um. */
113
+ const JUNTAR_LEITURAS = 'Precisa ler, listar ou buscar mais de uma coisa? Peça TUDO na MESMA resposta, '
114
+ + 'com várias chamadas de ferramenta juntas — nunca uma leitura por resposta. Cada resposta sua é '
115
+ + 'uma ida ao servidor, e o plano conta idas.';
116
+
117
+ module.exports = { escolher, bloco, prioridade, orcamento, JUNTAR_LEITURAS,
118
+ ORCAMENTO_SERVIDOR, ORCAMENTO_LOCAL, MAIOR_ARQUIVO };
package/lib/repeticao.js CHANGED
@@ -173,9 +173,11 @@ function ver(registro, nome, args) {
173
173
  }
174
174
 
175
175
  registro.vistos.set(chave, vezes);
176
- if (vezes === 1) return { repetida: false, vezes, aviso: null };
176
+ // `chave` volta para quem chamou guardar o resultado da leitura pela
177
+ // mesma época em que ela foi vista (lib/act.js, RELEITURA).
178
+ if (vezes === 1) return { repetida: false, vezes, aviso: null, chave };
177
179
 
178
- return { repetida: true, vezes, aviso: aviso(nome, vezes, parecidaCom) };
180
+ return { repetida: true, vezes, aviso: aviso(nome, vezes, parecidaCom), chave };
179
181
  }
180
182
 
181
183
  /** O recado, apertando a cada volta. */
package/lib/tempos.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * tempos.js — para onde foi o tempo de um pedido.
3
+ *
4
+ * PRIMOCODE_TEMPOS=1 primo
5
+ *
6
+ * No fim de cada pedido sai UMA linha com o que custou cada fase: o plano, a
7
+ * espera do resumo, o nome da pasta, cada rodada do modelo, as ferramentas e
8
+ * a conferência da página. Existe porque "está lento" sem número vira chute:
9
+ * medido em 24/09 com um servidor de mentira que responde na hora, o que o
10
+ * PrimoCode gastava sozinho num pedido de página era quase todo a conferência
11
+ * no navegador escondido (2,1 a 4,6s num Chrome recém-aberto), e não o que
12
+ * parecia: as ferramentas de três rodadas somaram 7 a 23ms, e o planejador
13
+ * nem é chamado em pedido de uma fase só.
14
+ *
15
+ * Desligado, não custa nada: `medir` chama a função e devolve, sem relógio.
16
+ *
17
+ * Um pedido por vez (é assim que o CLI trabalha), então o registro é do
18
+ * módulo. Subagente soma no mesmo pedido, com o nome da fase.
19
+ */
20
+ 'use strict';
21
+
22
+ let atual = null;
23
+
24
+ function ligado() { return process.env.PRIMOCODE_TEMPOS === '1'; }
25
+
26
+ /** Começa o registro de um pedido. Desligado, não faz nada. */
27
+ function comecar() {
28
+ atual = ligado() ? { inicio: Date.now(), fases: new Map() } : null;
29
+ return atual;
30
+ }
31
+
32
+ /** Soma `ms` à fase `nome` (várias rodadas somam na mesma linha, com contagem). */
33
+ function somar(nome, ms) {
34
+ if (!atual) return;
35
+ const f = atual.fases.get(nome) || { ms: 0, vezes: 0 };
36
+ f.ms += ms;
37
+ f.vezes++;
38
+ atual.fases.set(nome, f);
39
+ }
40
+
41
+ /** Mede uma função (síncrona ou async) e soma o tempo à fase. */
42
+ async function medir(nome, fn) {
43
+ if (!atual) return fn();
44
+ const t = Date.now();
45
+ try { return await fn(); } finally { somar(nome, Date.now() - t); }
46
+ }
47
+
48
+ function fmt(ms) { return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`; }
49
+
50
+ /** A linha do fim. Encerra o registro. null se estava desligado. */
51
+ function relatorio() {
52
+ if (!atual) return null;
53
+ const total = Date.now() - atual.inicio;
54
+ const partes = [...atual.fases].map(([nome, f]) =>
55
+ `${nome} ${fmt(f.ms)}${f.vezes > 1 ? ` (${f.vezes}x)` : ''}`);
56
+ atual = null;
57
+ return `tempos · ${partes.join(' · ')}${partes.length ? ' · ' : ''}total ${fmt(total)}`;
58
+ }
59
+
60
+ function ativo() { return !!atual; }
61
+
62
+ module.exports = { ligado, comecar, somar, medir, relatorio, ativo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "9.8.0-beta.6",
3
+ "version": "9.8.0-beta.7",
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": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/kits.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/ide.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/kits.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/ide.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"