primocode 10.5.0 → 10.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/bin/primocode.js +117 -24
  3. package/lib/act.js +141 -9
  4. package/lib/agents-md.js +140 -0
  5. package/lib/auditoria.js +122 -0
  6. package/lib/browser.js +48 -1
  7. package/lib/catalogo.js +29 -6
  8. package/lib/comandos-extra.js +2 -0
  9. package/lib/config.js +32 -4
  10. package/lib/entrada.js +33 -11
  11. package/lib/estudio/cenarios.js +91 -0
  12. package/lib/estudio/colecao.js +35 -1
  13. package/lib/estudio/diretor.js +1 -0
  14. package/lib/estudio/falantes.js +30 -6
  15. package/lib/estudio/hibrido.js +144 -8
  16. package/lib/estudio/index.js +196 -63
  17. package/lib/estudio/kit/mundo.jsx +1060 -0
  18. package/lib/estudio/kit/voxel.jsx +315 -70
  19. package/lib/estudio/kit.js +1 -1
  20. package/lib/estudio/kokoro.js +18 -4
  21. package/lib/estudio/margem.js +1 -1
  22. package/lib/estudio/movimentos.js +212 -0
  23. package/lib/estudio/mundo.js +328 -0
  24. package/lib/estudio/personagem.js +164 -11
  25. package/lib/estudio/plano.js +6 -0
  26. package/lib/estudio/producao.js +107 -17
  27. package/lib/estudio/projeto.js +77 -22
  28. package/lib/leitura.js +176 -0
  29. package/lib/local/cerebro.json +180 -3
  30. package/lib/local/entrada.js +61 -4
  31. package/lib/mcp.js +296 -12
  32. package/lib/memoria-conversa.js +32 -3
  33. package/lib/mencoes.js +5 -1
  34. package/lib/nome-curto.js +126 -0
  35. package/lib/nome-projeto.js +5 -2
  36. package/lib/outra-pasta.js +161 -0
  37. package/lib/perigo.js +34 -2
  38. package/lib/referencia.js +40 -11
  39. package/lib/repeticao.js +20 -1
  40. package/lib/tela.js +1 -1
  41. package/lib/tools.js +76 -6
  42. package/lib/travado.js +141 -0
  43. package/lib/ui.js +1 -0
  44. package/package.json +2 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.4.7
1
+ # PrimoCode v10.7.0
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 v10.4.7
12
+ ▐▛███▜▌ Primo Code v10.7.0
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
package/bin/primocode.js CHANGED
@@ -21,11 +21,14 @@ const api = require('../lib/api');
21
21
  const tools = require('../lib/tools');
22
22
  const cfg = require('../lib/config');
23
23
  const nomeProjeto = require('../lib/nome-projeto');
24
+ const { nomeCurto } = require('../lib/nome-curto');
24
25
  const provedorLocal = require('../lib/local/provedor');
25
26
  const { runAct, interromper, ehTetoDeToken, ehTransitorio, esperaDoErro, mandarDuranteATarefa, sobrasDaCaixa } = require('../lib/act');
26
27
  const fala = require('../lib/fala');
27
28
  const boasVindas = require('../lib/boas-vindas');
28
29
  const pasta = require('../lib/pasta');
30
+ const outraPasta = require('../lib/outra-pasta');
31
+ const agentsMd = require('../lib/agents-md');
29
32
  const catalogo = require('../lib/catalogo');
30
33
  const effort = require('../lib/effort');
31
34
  const memoria = require('../lib/memoria-conversa');
@@ -793,32 +796,82 @@ function projectDir() {
793
796
  // ── Envio ──────────────────────────────────────────────────────────────
794
797
 
795
798
  /**
796
- * Falou de uma pasta? Ele procura no computador e muda para ela.
799
+ * Falou de OUTRA pasta? Ele LÊ lá sem pedir, e só TROCA com o seu sim.
797
800
  *
798
- * Conservador de propósito: só troca quando acha UMA pasta. Com mais de uma,
799
- * lista e espera o /dir — heurística não deve escolher onde o agente escreve.
801
+ * "Quando eu perguntar de algo de outra pasta, ela vai procurar lá; e se ela
802
+ * está aberta numa pasta e eu trabalho em outra, ela muda — com permissão."
803
+ * A pasta citada (caminho, "pasta X", ou o nome de um projeto/pasta recente)
804
+ * fica liberada para leitura (lib/tools.js). A troca pergunta, Enter é NÃO.
805
+ *
806
+ * Devolve a nota para o modelo quando a pessoa ficou onde estava — é ela que
807
+ * diz ao agente que pode procurar lá. Com mais de uma pasta do mesmo nome,
808
+ * lista e espera o /dir: heurística não escolhe onde o agente escreve.
800
809
  */
801
- function talvezTrocarDePasta(userText) {
802
- const candidato = pasta.detectarPasta(userText);
803
- if (!candidato) return;
804
-
805
- const achado = pasta.resolverPasta(candidato, { raiz: cfg.ROOT_DIR, cwd: CWD_INICIAL });
806
- if (!achado) return; // não existe no disco: segue onde está, sem barulho
807
-
808
- if (achado.ambiguo) {
809
- console.log(c.warn(` Achei ${achado.ambiguo.length} pastas chamadas "${candidato}":`));
810
- achado.ambiguo.forEach((a) => console.log(` ${c.dim('·')} ${c.white(shortPath(a.caminho))}`));
810
+ async function talvezTrocarDePasta(userText) {
811
+ const r = outraPasta.detectar(userText, {
812
+ conhecidas: outraPasta.conhecidas({ raiz: cfg.ROOT_DIR, recentes: cfg.pastasRecentes() }),
813
+ atual: projectDir(),
814
+ resolver: (cand) => pasta.resolverPasta(cand, { raiz: cfg.ROOT_DIR, cwd: CWD_INICIAL }),
815
+ });
816
+ if (r.ambiguo) {
817
+ console.log(c.warn(` Achei ${r.ambiguo.length} pastas chamadas "${r.candidato}":`));
818
+ r.ambiguo.forEach((a) => console.log(` ${c.dim('·')} ${c.white(shortPath(a.caminho))}`));
811
819
  console.log(c.dim(' Escolha com /dir <caminho>.'));
812
- return;
820
+ return '';
813
821
  }
814
- if (path.resolve(achado.caminho) === path.resolve(projectDir())) return; // já está lá
815
- if (!pastaSegura(achado.caminho)) return; // home ou raiz: só por /dir explícito
822
+ const alvo = (r.pastas || []).find((p) => pastaSegura(p.caminho));
823
+ if (!alvo) return ''; // home ou raiz: só por /dir explícito
824
+
825
+ tools.liberarLeitura(alvo.caminho);
826
+
827
+ /* No pedido único (`primocode "crie x na pasta loja"`) não há a quem
828
+ perguntar — e a pasta veio escrita no próprio comando, com marca
829
+ ("pasta", caminho). Aí vale o que ele sempre fez: troca. Nome solto
830
+ ("olha o landing-padaria") nunca troca sem pergunta. */
831
+ if (state.semTerminal) {
832
+ if (alvo.como !== 'caminho') return outraPasta.notaDeConsulta(alvo.caminho, projectDir());
833
+ entrarNoProjeto(alvo.caminho, 'detectada');
834
+ return '';
835
+ }
836
+ console.log(c.dim(` ⎿ você citou ${shortPath(alvo.caminho)} — posso ler lá daqui mesmo`));
837
+ if (!(await perguntarTroca(alvo.caminho))) return outraPasta.notaDeConsulta(alvo.caminho, projectDir());
838
+ trocarPara(alvo.caminho, 'detectada', `você falou "${alvo.nome}"`);
839
+ return '';
840
+ }
841
+
842
+ /* A pergunta da troca. Enter sozinho é NÃO: trocar muda onde os arquivos
843
+ nascem, e o padrão de quem apertou Enter sem ler não pode ser esse. */
844
+ async function perguntarTroca(caminho, motivo = '') {
845
+ if (motivo) console.log(' ' + c.muted(motivo));
846
+ const dito = String(await ask(`${c.brand('?')} Trabalhar em ${c.white(shortPath(caminho))} a partir de agora? ${c.muted('(s/N)')} `))
847
+ .trim().toLowerCase().replace(/[.!?,;:…]+/g, ' ').trim();
848
+ if (!process.stdin.isTTY) process.stdout.write('\n');
849
+ return /^(s|sim|y|yes|pode|ok|claro|muda|troca)\b/.test(dito);
850
+ }
816
851
 
817
- entrarNoProjeto(achado.caminho, 'detectada');
818
- console.log(c.ok(` Trabalhando em ${shortPath(achado.caminho)}`) + c.dim(` (você falou "${candidato}")`));
852
+ /* Entra na pasta e avisa. A memória dela (AGENTS.md) entra no próximo
853
+ pedido pelo buildMemoryContext, porque ainda não está no resumo. */
854
+ function trocarPara(caminho, origem, porque) {
855
+ entrarNoProjeto(caminho, origem);
856
+ const tem = agentsMd.ler(caminho) ? ' · com memória das sessões anteriores' : '';
857
+ console.log(c.ok(` Trabalhando em ${shortPath(caminho)}`) + c.dim(` (${porque}${tem})`));
819
858
  refreshPrompt();
820
859
  }
821
860
 
861
+ /* O mudar_pasta do modelo chega aqui (lib/outra-pasta.js → pedirTroca). */
862
+ async function mudarPastaPeloAgente({ caminho, motivo }) {
863
+ if (state.semTerminal) return { ok: false, error: 'sem terminal para o usuário responder.' };
864
+ console.log('');
865
+ console.log(' ' + c.warn('▸▸') + ' ' + c.bold(c.white('O Primo quer trabalhar em outra pasta.')));
866
+ if (!(await perguntarTroca(caminho, motivo ? `Motivo: ${motivo}` : ''))) {
867
+ console.log(' ' + c.muted('Fica onde está.') + c.dim(' Ele ainda pode ler a outra pasta.'));
868
+ tools.liberarLeitura(caminho);
869
+ return { ok: false };
870
+ }
871
+ trocarPara(caminho, 'detectada', 'você autorizou');
872
+ return { ok: true, caminho, memoria: agentsMd.ler(caminho) || undefined };
873
+ }
874
+
822
875
  /**
823
876
  * Decide, de uma vez, onde este pedido vai trabalhar.
824
877
  *
@@ -827,10 +880,10 @@ function talvezTrocarDePasta(userText) {
827
880
  * `primocode "crie x na pasta loja"` ignorava a pasta e criava projeto novo.
828
881
  * Duas cópias da mesma decisão significam que uma delas está errada.
829
882
  */
830
- function prepararPasta(userText) {
883
+ async function prepararPasta(userText) {
831
884
  // A pasta é decidida ANTES de o agente começar. Trocar no meio faria as
832
885
  // primeiras tools agirem num lugar e as seguintes em outro.
833
- talvezTrocarDePasta(userText);
886
+ const notaOutraPasta = await talvezTrocarDePasta(userText);
834
887
 
835
888
  const plan = planWorkDir(userText);
836
889
  batizarDepois(plan, userText);
@@ -843,6 +896,7 @@ function prepararPasta(userText) {
843
896
  console.log(c.ok(` Continuando em ${state.currentProject}`) + c.muted(` (${shortPath(plan.dir)})`));
844
897
  refreshPrompt();
845
898
  }
899
+ plan.notaOutraPasta = notaOutraPasta;
846
900
  return plan;
847
901
  }
848
902
 
@@ -1133,7 +1187,7 @@ async function enviarPedido(userText) {
1133
1187
  // mídia inventada, limite estourado) para chegar no mesmo lugar.
1134
1188
  if (await baixarCriacaoDireto(userText)) { refreshPrompt(); return; }
1135
1189
 
1136
- const plan = prepararPasta(userText);
1190
+ const plan = await prepararPasta(userText);
1137
1191
 
1138
1192
  // O Chrome só aparece na tela quando o pedido é de navegador (lib/browser.js, definirVisivel).
1139
1193
  require('../lib/browser.js').definirVisivel(querNavegador(userText));
@@ -1213,15 +1267,22 @@ async function enviarPedido(userText) {
1213
1267
  // No pedido único (`primo "…"`) não há ninguém para responder:
1214
1268
  // sem `autorizar`, o que pede licença é recusado (lib/perigo.js).
1215
1269
  autorizar: state.semTerminal ? undefined : autorizarControle,
1270
+ // A memória do AGENTS.md só vai se o resumo ainda não a carrega.
1271
+ memoriaDaPasta: !memoria.jaDobrou(state.memoria, plan.dir),
1272
+ mudarPasta: state.semTerminal ? null : mudarPastaPeloAgente,
1216
1273
  controle: state.controle,
1217
1274
  permissoes: state.config.permissoes,
1218
1275
  // Pedido de mexer no computador leva a dica que libera as
1219
1276
  // ferramentas desktop_* no servidor (ver DICA_COMPUTADOR).
1220
1277
  instrucao: [i === 0 && querComputador(userText) ? vozModo.DICA_COMPUTADOR : '',
1221
- i === 0 ? require('../lib/estudio/referencias.js').instrucao() : ''].filter(Boolean).join('\n\n'),
1278
+ i === 0 ? require('../lib/estudio/referencias.js').instrucao() : '',
1279
+ i === 0 ? plan.notaOutraPasta || '' : ''].filter(Boolean).join('\n\n'),
1222
1280
  });
1223
1281
 
1224
1282
  r = await rodarPasso();
1283
+ // O mudar_pasta trocou no meio: os passos seguintes seguem lá.
1284
+ if (r && r.pasta && path.resolve(r.pasta) !== path.resolve(plan.dir)) { plan.dir = r.pasta; plan.pending = false; }
1285
+ if (r && r.estudio) state.estudioAtivo = r.estudio;
1225
1286
  /* Passo que parou no meio NÃO devolve a tarefa para a pessoa
1226
1287
  dividir. Se ele já tinha feito alguma coisa (arquivo, comando),
1227
1288
  o próprio CLI pede a continuação — com a lista do que já existe
@@ -1423,6 +1484,14 @@ async function comprimirConversa(forcado = false, { silencioso = false } = {}) {
1423
1484
  const sp = silencioso ? { stop() {} } : new Spinner('resumindo a conversa').start();
1424
1485
  try {
1425
1486
  const conversa = state.history;
1487
+ const dir = projectDir();
1488
+ /* A memória que o AGENTS.md desta pasta já tinha entra no resumo
1489
+ (uma vez por pasta): sem isto a primeira resposta de uma sessão nova
1490
+ reescreveria o AGENTS.md só com ela, e o "de onde parei" de ontem
1491
+ sumia. */
1492
+ if (!memoria.jaDobrou(state.memoria, dir) && !(state.memoria.semente && state.memoria.semente.pasta === dir)) {
1493
+ state.memoria = memoria.semear(state.memoria, dir, agentsMd.ler(dir));
1494
+ }
1426
1495
  const nova = await memoria.atualizar(state.memoria, conversa, ({ resumo, mensagens }) =>
1427
1496
  api.requestResumo(state.config.server, { resumo, mensagens, token: state.config.token })
1428
1497
  .then((r) => r.resumo));
@@ -1431,6 +1500,7 @@ async function comprimirConversa(forcado = false, { silencioso = false } = {}) {
1431
1500
  if (state.history !== conversa) { sp.stop(); return; }
1432
1501
  state.memoria = nova;
1433
1502
  sp.stop();
1503
+ gravarMemoriaDaPasta(dir);
1434
1504
  const e = memoria.economia(state.memoria, state.history);
1435
1505
  if (!silencioso && e.poupado > 0) console.log(c.dim(` ⎿ conversa resumida · ${e.cheio} → ${e.enxuto} tokens por pedido`));
1436
1506
  } catch {
@@ -1451,6 +1521,24 @@ function resumirDepois() {
1451
1521
  atual.then(() => { if (state.resumindo === atual) state.resumindo = null; });
1452
1522
  }
1453
1523
 
1524
+ /* ── O AGENTS.md DE CADA PASTA ───────────────────────────────────────────
1525
+ * "Eu preciso de um AGENTS.md com a memória da conversa em cada pasta."
1526
+ * O resumo da conversa vai para o AGENTS.md da pasta de trabalho (só o bloco
1527
+ * do PrimoCode; o resto do arquivo é da pessoa — lib/agents-md.js), e para a
1528
+ * pasta do projeto do Estúdio em que se mexeu, se houver. A raiz
1529
+ * ~/primocode fica de fora: é o corredor, não um projeto. */
1530
+ function gravarMemoriaDaPasta(dir = projectDir()) {
1531
+ const resumo = state.memoria && state.memoria.resumo;
1532
+ if (!resumo) return;
1533
+ try { agentsMd.gravar(dir, resumo, { raiz: cfg.ROOT_DIR }); } catch { /* memória é conveniência */ }
1534
+ if (state.estudioAtivo) {
1535
+ try {
1536
+ const p = require('../lib/estudio/projeto.js').ler(state.estudioAtivo);
1537
+ if (p) agentsMd.gravar(p.pasta, resumo, { titulo: p.titulo });
1538
+ } catch { /* projeto sumiu */ }
1539
+ }
1540
+ }
1541
+
1454
1542
  async function esperarResumo(tetoMs = 6000) {
1455
1543
  if (!state.resumindo) return;
1456
1544
  await Promise.race([state.resumindo, new Promise((ok) => setTimeout(ok, tetoMs))]);
@@ -1495,7 +1583,11 @@ function contextoDoProjeto() {
1495
1583
  instruções do projeto entram. Sem isto o PRIMO.md valeria para o agente e
1496
1584
  seria ignorado quando o modo auto manda o pedido para a conversa. */
1497
1585
  function comInstrucoes(history) {
1498
- const texto = mencoesLib.instrucoesDoProjeto(state.currentProject || state.workDir ? projectDir() : null);
1586
+ const dir = state.currentProject || state.workDir ? projectDir() : null;
1587
+ let texto = mencoesLib.instrucoesDoProjeto(dir);
1588
+ // A memória do AGENTS.md também vale no chat, enquanto o resumo não a tem.
1589
+ const daPasta = dir && !memoria.jaDobrou(state.memoria, dir) ? agentsMd.ler(dir) : '';
1590
+ if (daPasta) texto = [texto, `[AGENTS.md — memória desta pasta: de onde a conversa parou]\n${daPasta}`].filter(Boolean).join('\n\n');
1499
1591
  return texto ? [{ role: 'system', content: texto }, ...history] : history;
1500
1592
  }
1501
1593
 
@@ -2004,7 +2096,8 @@ async function handleSlash(text) {
2004
2096
  sp.stop();
2005
2097
  const nomeNovo = extra.nomeDoNovo({
2006
2098
  descricao: rest, nomes, sugerido, derivar: deriveProjectName,
2007
- slugify: cfg.slugify, semColisao: nomeProjeto.semColisao,
2099
+ // A régua curta (lib/nome-curto.js) também no /novo.
2100
+ slugify: (t) => nomeCurto(t) || cfg.slugify(t), semColisao: nomeProjeto.semColisao,
2008
2101
  });
2009
2102
  const dir = cfg.createProjectDir(nomeNovo);
2010
2103
  entrarNoProjeto(dir, 'projeto');
package/lib/act.js CHANGED
@@ -26,6 +26,8 @@ const skills = require('./skills');
26
26
  const checkpoint = require('./checkpoint');
27
27
  const tempos = require('./tempos');
28
28
  const preLeitura = require('./pre-leitura');
29
+ const travado = require('./travado');
30
+ const auditoria = require('./auditoria');
29
31
 
30
32
  // 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
31
33
  // volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
@@ -41,7 +43,7 @@ const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
41
43
  // As leituras que, repetidas sem mudança no meio, devolvem o guardado.
42
44
  const RELEITURA = new Set(['read_file', 'list_dir']);
43
45
  /* 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']);
46
+ const SO_LEITURA = new Set(['read_file', 'list_dir', 'glob', 'grep', 'pesquisar_referencia', 'estudio_site', 'mcp_procurar']);
45
47
  const ehLeitura = (tc) => SO_LEITURA.has(tc.name)
46
48
  || (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) || '')));
47
49
 
@@ -275,6 +277,17 @@ function logResult(kind, result, profundidade) {
275
277
  }
276
278
  return;
277
279
  }
280
+ case 'mcp_procurar': {
281
+ console.log(p + c.muted(`${(result.resultados || []).length} MCP para "${result.busca}"`));
282
+ for (const r of (result.resultados || []).slice(0, 4)) console.log(p + c.dim(`${r.nome} · ${r.descricao}`.slice(0, 110)));
283
+ return;
284
+ }
285
+ case 'mcp_conectar':
286
+ console.log(p + c.ok('conectado') + c.muted(` · ${result.servidor} · ${(result.ferramentas || []).length} tools`));
287
+ return;
288
+ case 'mcp_usar':
289
+ console.log(p + c.muted(`${result.servidor}.${result.ferramenta} · ${String(result.resultado || '').length} caracteres`));
290
+ return;
278
291
  case 'desktop_windows': {
279
292
  console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
280
293
  for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
@@ -645,6 +658,11 @@ function historicoEnxuto(history) {
645
658
  * vêm do que o opencode e o Codex põem nos prompts deles para o modelo não
646
659
  * reler o que fez; entram em TODO pedido, curtas, porque o plano conta token.
647
660
  * A conferência de verdade é nossa (a página aberta no Chrome escondido). */
661
+ /* MCP primeiro, mouse por último: vale no modo local e no subagente, que não
662
+ recebem o prompt do servidor. */
663
+ const FERRAMENTA_CERTA = '- FERRAMENTA CERTA: para agir num serviço ou app, use primeiro um MCP (veja [mcp]). '
664
+ + 'Sem MCP: mcp_procurar → mcp_conectar → mcp_usar. Mouse/desktop_* só quando não houver MCP, API ou CLI.';
665
+
648
666
  const RITMO = '[ritmo]\n'
649
667
  + '- 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'
650
668
  + '- Várias ações independentes vão na MESMA resposta (ex.: escrever index.html, style.css e script.js juntos).\n'
@@ -653,24 +671,34 @@ const RITMO = '[ritmo]\n'
653
671
  + '- VOCÊ DECIDE O CAMINHO. Pedido simples: faça direto. Pedido grande com partes independentes: write_todos com as partes, '
654
672
  + 'depois spawn_agent — um por parte, até 8 NA MESMA resposta (rodam juntos), cada um com o especialista e tudo que ele precisa saber. '
655
673
  + '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'
656
- + '- Antes de dizer pronto, PROVE: rode o teste ou o comando, abra o resultado, olhe a imagem. Sem prova, não está pronto.';
674
+ + '- Antes de dizer pronto, PROVE: rode o teste ou o comando, abra o resultado, olhe a imagem. Sem prova, não está pronto.\n'
675
+ + FERRAMENTA_CERTA;
657
676
 
658
677
  /* O ritmo do subagente: o mesmo, sem a parte de orquestrar (ele não abre outro agente). */
659
678
  const RITMO_AGENTE = '[ritmo]\n'
660
679
  + '- O resultado de write_file/edit_file é a verdade. Não releia o que acabou de escrever.\n'
661
680
  + '- 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'
662
681
  + '- Várias ações independentes na MESMA resposta.\n'
663
- + '- Faça o que a sua parte pede, prove (teste ou comando) e chame finish. Polimento além do pedido não é sua parte.';
682
+ + '- Faça o que a sua parte pede, prove (teste ou comando) e chame finish. Polimento além do pedido não é sua parte.\n'
683
+ + FERRAMENTA_CERTA;
664
684
 
665
685
  /* `pedido` é opcional: com ele, o arquivo citado no pedido é o primeiro a
666
686
  entrar já lido (ver lib/pre-leitura.js). */
667
- function buildMemoryContext(projectDir, projectName, pedido = '') {
687
+ function buildMemoryContext(projectDir, projectName, pedido = '', { memoriaDaPasta = true } = {}) {
668
688
  const parts = [];
669
689
  // As instruções vêm primeiro: são regra de quem manda no projeto, e a
670
690
  // memória é o que o agente anotou. Entram aqui (e não no bin) para valer
671
691
  // igual no terminal, no modo voz e em cada passo de um plano.
672
692
  const instrucoes = require('./mencoes.js').instrucoesDoProjeto(projectDir);
673
693
  if (instrucoes) parts.push(instrucoes);
694
+ /* A conversa das sessões anteriores NESTA pasta (lib/agents-md.js). É o
695
+ que faz a sessão de amanhã começar de onde a de hoje parou. Quando o
696
+ resumo da conversa já a carrega (`memoriaDaPasta: false`, quem sabe é o
697
+ bin), ela não vem de novo. */
698
+ if (memoriaDaPasta) {
699
+ const daPasta = require('./agents-md.js').ler(projectDir);
700
+ if (daPasta) parts.push(`[AGENTS.md — memória desta pasta: de onde a conversa parou]\n${daPasta}`);
701
+ }
674
702
  const geral = cfg.readGeneralMemory();
675
703
  if (geral.trim()) parts.push(`[memory-geral.md]\n${geral}`);
676
704
  const projeto = cfg.readProjectMemory(projectDir);
@@ -693,6 +721,14 @@ async function runAct(userPrompt, opts) {
693
721
  const {
694
722
  server, model, token, history, projectDir, projectName,
695
723
  ensureWorkDir, profundidade = 0, subagent = false, effort,
724
+ /* A memória do AGENTS.md entra no contexto? O bin diz não quando o
725
+ resumo da conversa já a dobrou (lib/memoria-conversa.js). */
726
+ memoriaDaPasta = true,
727
+ /* Quem troca a pasta de trabalho quando o modelo chama mudar_pasta:
728
+ async ({ caminho, motivo }) → { ok, caminho, memoria?, error? }.
729
+ Vem do bin, porque é lá que se PERGUNTA ao usuário — sem ele (pipe,
730
+ subagente) a troca é recusada. */
731
+ mudarPasta = null,
696
732
  /* Quem responde ao pedido de controle do computador, e o estado da
697
733
  resposta. Vêm de fora porque quem sabe perguntar é o terminal, e
698
734
  quem sabe lembrar é a SESSÃO — um subagente não pode começar do
@@ -723,6 +759,8 @@ async function runAct(userPrompt, opts) {
723
759
  teste que troca o medidor não pode subir um Chrome de verdade. */
724
760
  aquecerPagina = opts && opts.medirPagina ? null
725
761
  : () => require('./browser.js').aquecerRevisor(),
762
+ /* Quem é este agente no livro de ações (auditoria.js). */
763
+ rotulo = subagent ? 'subagente' : 'principal',
726
764
  } = opts || {};
727
765
 
728
766
  // A pasta de projeto só nasce quando o agente for mesmo MEXER EM ARQUIVO.
@@ -731,6 +769,7 @@ async function runAct(userPrompt, opts) {
731
769
  // nada. Quem decide é o catálogo: `escreve: true`.
732
770
  let criarPasta = ensureWorkDir || null;
733
771
  let pastaAtual = projectDir; // onde as tools agem AGORA
772
+ let estudioDaTarefa = null; // o projeto do Estúdio desta tarefa, se houver
734
773
  let temPasta = !criarPasta; // já é a pasta definitiva?
735
774
  /* Cria a pasta do projeto pendente, uma vez. Chamado antes de qualquer
736
775
  ferramenta que faz código (ver `criaProjeto` no catálogo) — e ANTES do
@@ -783,6 +822,17 @@ async function runAct(userPrompt, opts) {
783
822
  ? `${texto}\n\nAtenção: abri a página e ela ainda tem problema — ${avisoDaPagina.trim()}` : texto);
784
823
  // Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
785
824
  const jaChamado = repeticao.novo();
825
+ /* Andando em círculo? (lib/travado.js — o subagente de 01/10 que releu o
826
+ mesmo arquivo por uma hora). Empurra uma vez; na segunda, para. */
827
+ const circulo = travado.novo();
828
+ let paradoPorTravar = null;
829
+ /* Anota ANTES de agir (lib/auditoria.js): recusado também fica no livro. */
830
+ const anotar = (tc, decisao, motivo) => {
831
+ const id = auditoria.antes({ agente: rotulo, ferramenta: tc.name, args: tc.arguments, decisao, motivo });
832
+ const falhou = auditoria.problema();
833
+ if (falhou) console.log(c.warn(`⚠ ${falhou}`));
834
+ return id;
835
+ };
786
836
  // O que read_file/list_dir devolveram, pela chave da época (repeticao.js).
787
837
  const leiturasGuardadas = new Map();
788
838
  // O que ELE escreveu inteiro nesta tarefa, por caminho absoluto — para
@@ -834,7 +884,7 @@ async function runAct(userPrompt, opts) {
834
884
 
835
885
  const messages = [];
836
886
  if (!subagent) {
837
- const memory = buildMemoryContext(projectDir, projectName, userPrompt);
887
+ const memory = buildMemoryContext(projectDir, projectName, userPrompt, { memoriaDaPasta });
838
888
  if (memory) messages.push({ role: 'system', content: `Memória persistente:\n\n${memory}` });
839
889
  for (const m of historicoEnxuto(history)) messages.push(m);
840
890
  } else {
@@ -1253,11 +1303,12 @@ async function runAct(userPrompt, opts) {
1253
1303
  if (spawns.length > 1 && !PARAR) {
1254
1304
  const juntos = spawns.slice(0, 8);
1255
1305
  fala.paralelo(juntos.length, profundidade);
1306
+ const ids = juntos.map((tc) => anotar(tc, 'permitida'));
1256
1307
  const rs = await Promise.all(juntos.map((tc) =>
1257
1308
  runSubagent(tc.arguments || {}, {
1258
1309
  server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1259
1310
  }).catch((e) => `Subagente falhou: ${e && e.message}`)));
1260
- juntos.forEach((tc, i) => resultadosSpawn.set(tc.id, rs[i]));
1311
+ juntos.forEach((tc, i) => { resultadosSpawn.set(tc.id, rs[i]); auditoria.depois(ids[i], { ok: true }); });
1261
1312
  }
1262
1313
 
1263
1314
  /* LEITURAS JUNTAS. O RITMO pede ao modelo para juntar as ações
@@ -1265,10 +1316,23 @@ async function runAct(userPrompt, opts) {
1265
1316
  grep e uma pesquisa. Só que o laço abaixo rodava uma de cada vez, e
1266
1317
  a rodada custava a soma. O que só LÊ (não muda arquivo, tela nem
1267
1318
  conta) roda tudo de uma vez aqui; o laço usa o resultado pronto,
1268
- passando pelos mesmos freios de sempre (repetição, registro). */
1319
+ passando pelos mesmos freios de sempre (repetição, registro).
1320
+ Só entra no lote o que o laço NÃO vai recusar: a releitura, a
1321
+ mesma leitura duas vezes na resposta e a pesquisa depois de três
1322
+ secas ficam de fora. Senão rodavam aqui e o livro anotava
1323
+ "recusada" para o que já tinha rodado. */
1269
1324
  const adiantadas = new Map();
1270
- const leituras = toolCalls.filter(ehLeitura);
1325
+ const chavesDoLote = new Set();
1326
+ const leituras = toolCalls.filter(ehLeitura).filter((tc) => {
1327
+ if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) return false;
1328
+ const { repetida, chave } = repeticao.espiar(jaChamado, tc.name, tc.arguments);
1329
+ if (repetida || chavesDoLote.has(chave)) return false;
1330
+ chavesDoLote.add(chave);
1331
+ return true;
1332
+ });
1333
+ const idsAdiantados = new Map();
1271
1334
  if (leituras.length > 1 && !PARAR) {
1335
+ leituras.slice(0, 8).forEach((tc) => idsAdiantados.set(tc.id, anotar(tc, 'permitida')));
1272
1336
  const rs = await Promise.all(leituras.slice(0, 8).map((tc) =>
1273
1337
  tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api })
1274
1338
  .catch((e) => ({ ok: false, error: e && e.message }))));
@@ -1276,6 +1340,7 @@ async function runAct(userPrompt, opts) {
1276
1340
  traco(`${adiantadas.size} leituras em paralelo`);
1277
1341
  }
1278
1342
 
1343
+ let motivoTravou = null;
1279
1344
  for (const tc of toolCalls) {
1280
1345
  if (PARAR) break;
1281
1346
  if (tc.name === 'finish') {
@@ -1309,13 +1374,37 @@ async function runAct(userPrompt, opts) {
1309
1374
  continue;
1310
1375
  }
1311
1376
 
1377
+ /* TROCAR DE PASTA mora aqui, e não no tools.execute: ela muda
1378
+ `pastaAtual`, que é deste laço, e precisa de quem PERGUNTA ao
1379
+ usuário (o bin). Sem a resposta dele, não troca. */
1380
+ if (tc.name === 'mudar_pasta') {
1381
+ algoRodou = true;
1382
+ fala.acao(tc.name, tc.arguments, profundidade);
1383
+ const idA = anotar(tc, 'permitida');
1384
+ const r = await require('./outra-pasta.js').pedirTroca(tc.arguments || {}, {
1385
+ atual: pastaAtual, subagent, trocar: mudarPasta,
1386
+ });
1387
+ if (r.ok) {
1388
+ pastaAtual = r.caminho;
1389
+ criarPasta = null;
1390
+ temPasta = true;
1391
+ }
1392
+ logResult(tc.name, r, profundidade);
1393
+ auditoria.depois(idA, r);
1394
+ repeticao.executou(jaChamado, tc.name, tc.arguments);
1395
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
1396
+ continue;
1397
+ }
1398
+
1312
1399
  if (tc.name === 'spawn_agent') {
1313
1400
  algoRodou = true;
1401
+ const idA = resultadosSpawn.has(tc.id) ? null : anotar(tc, 'permitida');
1314
1402
  const r = resultadosSpawn.has(tc.id)
1315
1403
  ? resultadosSpawn.get(tc.id)
1316
1404
  : await runSubagent(tc.arguments || {}, {
1317
1405
  server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1318
1406
  });
1407
+ if (idA) auditoria.depois(idA, { ok: true });
1319
1408
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1320
1409
  messages.push({ role: 'tool', tool_call_id: tc.id, content: r + lembreteDeParalelo(todosDaTarefa, spawns.length) });
1321
1410
  continue;
@@ -1326,10 +1415,12 @@ async function runAct(userPrompt, opts) {
1326
1415
  if (tc.name === 'planejar_producao') {
1327
1416
  algoRodou = true;
1328
1417
  fala.acao(tc.name, tc.arguments, profundidade);
1418
+ const idA = anotar(tc, 'permitida');
1329
1419
  const r = await rodarDiretor(tc.arguments || {}, {
1330
1420
  server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes,
1331
1421
  });
1332
1422
  logResult(tc.name, r, profundidade);
1423
+ auditoria.depois(idA, r);
1333
1424
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
1334
1425
  continue;
1335
1426
  }
@@ -1344,10 +1435,12 @@ async function runAct(userPrompt, opts) {
1344
1435
  algoRodou = true;
1345
1436
  garantirPasta();
1346
1437
  fala.acao(tc.name, tc.arguments, profundidade);
1438
+ const idA = anotar(tc, 'permitida');
1347
1439
  const r = await rodarPipeline(tc.arguments || {}, {
1348
1440
  server, model, token, projectDir: pastaAtual, projectName, profundidade, motor, permissoes, livro, autorizar, controle: portaoControle,
1349
1441
  });
1350
1442
  logResult(tc.name, r, profundidade);
1443
+ auditoria.depois(idA, r);
1351
1444
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1352
1445
  messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(r) });
1353
1446
  if (r && r.ok && catalogo.escreveArquivo(tc.name)) temPasta = true;
@@ -1362,6 +1455,7 @@ async function runAct(userPrompt, opts) {
1362
1455
  const eco = repeticao.ver(jaChamado, tc.name, tc.arguments);
1363
1456
  if (eco.repetida) {
1364
1457
  voltasEmFalso++;
1458
+ anotar(tc, 'recusada', `repetida (${eco.vezes}ª vez) — não executou de novo`);
1365
1459
  /* RELER SEM MUDANÇA devolve o que já foi lido, uma vez.
1366
1460
  O aviso seco ("o resultado está na conversa acima") mentia
1367
1461
  depois do `compactar`: a leitura velha já tinha virado 300
@@ -1377,6 +1471,11 @@ async function runAct(userPrompt, opts) {
1377
1471
  continue;
1378
1472
  }
1379
1473
 
1474
+ /* Só conta o que passou do freio de repetição: a chamada idêntica
1475
+ já é barrada lá (e a rodada parada desiste sozinha). Aqui entra
1476
+ o que escapa dele — a fatia sobreposta, a volta sem nada novo. */
1477
+ motivoTravou = motivoTravou || travado.observar(circulo, tc.name, tc.arguments, pastaAtual);
1478
+
1380
1479
  if (criarPasta && catalogo.criaProjeto(tc.name, tc.arguments)) garantirPasta();
1381
1480
 
1382
1481
  /* RELER O QUE ACABOU DE ESCREVER. O modelo grátis escreve o
@@ -1413,6 +1512,7 @@ async function runAct(userPrompt, opts) {
1413
1512
 
1414
1513
  if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
1415
1514
  voltasEmFalso++;
1515
+ anotar(tc, 'recusada', 'três pesquisas seguidas sem fonte');
1416
1516
  messages.push({
1417
1517
  role: 'tool',
1418
1518
  tool_call_id: tc.id,
@@ -1431,6 +1531,7 @@ async function runAct(userPrompt, opts) {
1431
1531
  if (permissao.precisa(tc.name)) {
1432
1532
  const porta = await permissao.pedir(portaoControle, tc.name, tc.arguments, autorizar);
1433
1533
  if (!porta.ok) {
1534
+ anotar(tc, 'recusada', porta.error);
1434
1535
  fala.acao(tc.name, tc.arguments, profundidade);
1435
1536
  console.log(fala.resultado(profundidade) + c.warn('não autorizado'));
1436
1537
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: porta.error });
@@ -1448,6 +1549,7 @@ async function runAct(userPrompt, opts) {
1448
1549
  projectDir: pastaAtual, subagent, perguntar: autorizar,
1449
1550
  });
1450
1551
  if (!risco.ok) {
1552
+ anotar(tc, 'recusada', risco.motivo || risco.error);
1451
1553
  fala.acao(tc.name, tc.arguments, profundidade);
1452
1554
  console.log(fala.resultado(profundidade) + c.warn(`não autorizado — ${risco.motivo}`));
1453
1555
  registro.anotar(livro, tc.name, tc.arguments, { ok: false, error: risco.error });
@@ -1467,9 +1569,15 @@ async function runAct(userPrompt, opts) {
1467
1569
  estudio_video transcreve no servidor, e sem eles a ferramenta
1468
1570
  respondia "a transcrição precisa do servidor". */
1469
1571
  traco(`executando ${tc.name}`);
1572
+ const idAcao = idsAdiantados.get(tc.id) || anotar(tc, 'permitida');
1470
1573
  const result = adiantadas.has(tc.id) ? adiantadas.get(tc.id)
1471
1574
  : await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual, server, token, api });
1575
+ auditoria.depois(idAcao, result);
1472
1576
  traco(`${tc.name} devolveu ok=${result && result.ok}`);
1577
+ if (/^estudio_|^planejar_producao$/.test(tc.name) && result && result.ok !== false) {
1578
+ const id = result.id || (result.projeto && result.projeto.id) || (tc.arguments && tc.arguments.id);
1579
+ if (typeof id === 'string' && id) estudioDaTarefa = id;
1580
+ }
1473
1581
  logResult(tc.name, result, profundidade);
1474
1582
  repeticao.executou(jaChamado, tc.name, tc.arguments);
1475
1583
  if (RELEITURA.has(tc.name) && result && result.ok !== false) {
@@ -1548,6 +1656,18 @@ async function runAct(userPrompt, opts) {
1548
1656
  // A conferência da página tem linha própria: aqui fica só o resto.
1549
1657
  tempos.somar('ferramentas', Date.now() - tTools - (msConferindo - conferindoAntes));
1550
1658
 
1659
+ /* ANDANDO EM CÍRCULO (lib/travado.js): a primeira vez, um empurrão
1660
+ com o que ele está repetindo; a segunda, para — antes da cota. */
1661
+ const giro = travado.fimDaVolta(circulo);
1662
+ const travou = finished ? null : travado.decidir(circulo, motivoTravou || giro);
1663
+ if (travou && travou.acao === 'empurrar') {
1664
+ traco(`travado: ${motivoTravou || giro}`);
1665
+ messages.push({ role: 'user', content: travou.texto });
1666
+ } else if (travou) {
1667
+ paradoPorTravar = travou.texto;
1668
+ break;
1669
+ }
1670
+
1551
1671
  // Uma volta inteira sem executar nada é o modelo preso. Insistir custa
1552
1672
  // um pedido ao servidor por volta, e as 15 voltas saíam caras para
1553
1673
  // terminar no mesmo lugar — melhor parar e devolver a vez ao usuário.
@@ -1587,6 +1707,11 @@ async function runAct(userPrompt, opts) {
1587
1707
  // que "parou sem concluir" não pode virar fato para o passo seguinte.
1588
1708
  const concluiu = Boolean(finalText) && !PARAR;
1589
1709
 
1710
+ if (paradoPorTravar && !finalText) {
1711
+ finalText = paradoPorTravar;
1712
+ if (!subagent) fala.aviso(finalText);
1713
+ }
1714
+
1590
1715
  if (PARAR && !finalText) {
1591
1716
  finalText = 'Parado por você (Esc Esc). O que já foi feito, ficou feito.';
1592
1717
  if (!subagent) fala.aviso(finalText);
@@ -1609,7 +1734,9 @@ async function runAct(userPrompt, opts) {
1609
1734
  // (buildMemoryContext), e sessão sem projeto nomeado (ex.: primeiro pedido
1610
1735
  // antes de /project) tem `projectName` undefined mesmo já tendo mexido em
1611
1736
  // arquivo de verdade — a memória era descartada nesse caso, sempre.
1612
- if (!subagent && temPasta) {
1737
+ // A raiz de trabalho não é projeto: um memory.md solto em ~/primocode
1738
+ // era o que um pedido de vídeo (que trabalha na raiz) deixava para trás.
1739
+ if (!subagent && temPasta && require('path').resolve(pastaAtual) !== require('path').resolve(cfg.ROOT_DIR)) {
1613
1740
  cfg.appendProjectMemory(pastaAtual, `**Pedido:** ${userPrompt.slice(0, 300)}\n**Resultado:** ${finalText.slice(0, 500)}`);
1614
1741
  }
1615
1742
 
@@ -1639,6 +1766,10 @@ async function runAct(userPrompt, opts) {
1639
1766
  // de o usuário ter mandado parar.
1640
1767
  parado: PARAR,
1641
1768
  concluiu,
1769
+ // Onde terminou: o mudar_pasta pode ter trocado no meio.
1770
+ pasta: pastaAtual,
1771
+ // O projeto do Estúdio em que se mexeu, para a memória ir para a pasta dele.
1772
+ estudio: estudioDaTarefa,
1642
1773
  };
1643
1774
  }
1644
1775
 
@@ -1665,6 +1796,7 @@ async function runSubagent(args, ctx) {
1665
1796
  permissoes,
1666
1797
  autorizar: ctx.autorizar,
1667
1798
  controle: ctx.controle, // o mesmo objeto: a resposta vale para todos
1799
+ rotulo: `subagente: ${String(descricao).slice(0, 60)}`,
1668
1800
  });
1669
1801
 
1670
1802
  /* O que o subagente fez entra no livro de quem o chamou. Sem isto, o