terminal-smart-cli 0.97.30 → 0.97.41

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/bin/ts.js CHANGED
@@ -200,7 +200,7 @@ async function login() {
200
200
  let p;
201
201
  try { p = await api('/api/cli/pair/poll', { method: 'POST', body: { poll: r.poll } }); } catch (_) { continue; }
202
202
  if (p && p.status === 'ok') {
203
- cfg = config.save({ token: p.token, username: p.username, plan: p.plan, convId: undefined });
203
+ cfg = config.save({ token: p.token, username: p.username, plan: p.plan, convId: undefined, convScopes: undefined });
204
204
  sp.stop(ui.okLine(C.bold(T.login_ok(p.username))));
205
205
  return;
206
206
  }
@@ -211,15 +211,26 @@ async function login() {
211
211
  async function logout() {
212
212
  if (!cfg.token) { console.log(ui.infoLine(T.logout_none)); return; }
213
213
  try { await api('/api/logout', { method: 'POST', token: cfg.token }); } catch (_) {}
214
- config.save({ token: undefined, username: undefined, plan: undefined, convId: undefined });
214
+ config.save({ token: undefined, username: undefined, plan: undefined, convId: undefined, convScopes: undefined });
215
215
  console.log(ui.okLine(T.logout_ok));
216
216
  }
217
217
 
218
218
  // ── Chat ─────────────────────────────────────────────────────────────────────
219
- async function ensureConv(token) {
220
- if (cfg.convId) return cfg.convId;
221
- const r = await api('/api/conversations', { method: 'POST', token, body: { title: 'CLI' } });
222
- cfg = config.save({ convId: r.id });
219
+ // A conversa é isolada por pasta de trabalho. Assim o contexto continua quando
220
+ // alguém conversa no mesmo projeto, mas não vaza de um projeto para outro.
221
+ const conversationScope = require('../lib/conversation-scope');
222
+ function _convScope(kind = 'chat', cwd = process.cwd()) {
223
+ return conversationScope.workspaceScope(cwd, kind);
224
+ }
225
+ async function ensureConv(token, kind = 'chat', cwd = process.cwd(), forceNew = false) {
226
+ const scope = _convScope(kind, cwd);
227
+ const scopes = Object.assign({}, cfg.convScopes || {});
228
+ if (!forceNew && scopes[scope]) return scopes[scope];
229
+ const r = await api('/api/conversations', { method: 'POST', token, body: { title: conversationScope.conversationTitle(cwd, 'CLI') } });
230
+ scopes[scope] = r.id;
231
+ // convId permanece como ponte de compatibilidade para instalações antigas;
232
+ // toda nova leitura usa convScopes, nunca o id global legado.
233
+ cfg = config.save({ convId: r.id, convScopes: scopes });
223
234
  return r.id;
224
235
  }
225
236
 
@@ -415,7 +426,12 @@ async function sendMessage(token, content) {
415
426
  let convId = await ensureConv(token);
416
427
  try { return await doStream(convId); }
417
428
  catch (e) {
418
- if (e instanceof ApiError && e.status === 404) { cfg = config.save({ convId: undefined }); convId = await ensureConv(token); return doStream(convId); }
429
+ if (e instanceof ApiError && e.status === 404) {
430
+ const scope = _convScope('chat');
431
+ const scopes = Object.assign({}, cfg.convScopes || {}); delete scopes[scope];
432
+ cfg = config.save({ convId: undefined, convScopes: scopes });
433
+ convId = await ensureConv(token); return doStream(convId);
434
+ }
419
435
  throw e;
420
436
  }
421
437
  }
@@ -927,8 +943,7 @@ async function chatRepl() {
927
943
  if (['sair', 'exit', 'quit', '/sair', '/exit'].includes(low)) break;
928
944
  try {
929
945
  if (['/nova', '/new'].includes(low)) {
930
- const r = await api('/api/conversations', { method: 'POST', token, body: { title: 'CLI' } });
931
- cfg = config.save({ convId: r.id });
946
+ await ensureConv(token, 'chat', process.cwd(), true);
932
947
  console.log(ui.okLine(T.new_conv) + '\n');
933
948
  } else if (['/ajuda', '/help', '/?'].includes(low)) {
934
949
  const w = Math.max(...T.repl_help.map(i => i[0].length)) + 3;
@@ -1079,6 +1094,19 @@ async function pollRun(token, runId, sp) {
1079
1094
  }
1080
1095
  }
1081
1096
 
1097
+ // A run é o resumo; o motivo técnico da falha fica na etapa. Centralizar a
1098
+ // leitura evita que o usuário receba apenas "falhou" e tenha de investigar
1099
+ // logs ou a interface web para saber o que fazer em seguida.
1100
+ function runFailureMessage(run) {
1101
+ const failed = (run && Array.isArray(run.steps) ? run.steps : []).find(s => s && s.status === 'failed');
1102
+ const raw = String((failed && (failed.error_message || failed.result_summary)) || (run && run.error) || '').trim();
1103
+ const code = String((failed && failed.error_code) || '').trim();
1104
+ if (code === 'execution_unavailable' || /n[aã]o executo comandos diretamente|n[aã]o tenho acesso.*(windows|computador|m[aá]quina|sistema)|execute.*powershell/i.test(raw)) {
1105
+ return 'Esta orquestração roda na nuvem e não acessa arquivos do seu computador. Para criar, editar ou inspecionar algo local, use: ts agente "sua tarefa".';
1106
+ }
1107
+ return raw || 'Nenhuma ação foi concluída. Consulte as etapas da execução para ver o motivo.';
1108
+ }
1109
+
1082
1110
  // Fluxo completo de orquestração (usado pelo `ts run` e pelo /run do modo conversa).
1083
1111
  // askFn permite aprovar com o readline do REPL sem conflito de stdin; nunca chama process.exit.
1084
1112
  async function runFlow(goal, { askFn = ui.ask, json = false, yes = false } = {}) {
@@ -1112,7 +1140,7 @@ async function runFlow(goal, { askFn = ui.ask, json = false, yes = false } = {})
1112
1140
  const result = run.final_result || run.result || '';
1113
1141
  if (result) { console.log(''); console.log(ui.md(String(result)).split('\n').map(l => ' ' + l).join('\n')); console.log(''); }
1114
1142
  } else if (run.status === 'failed') {
1115
- console.error(ui.errLine(C.bold(T.run_failed) + (run.error ? C.dim(' · ' + String(run.error).slice(0, 140)) : '')));
1143
+ console.error(ui.errLine(C.bold(T.run_failed) + C.dim(' · ' + runFailureMessage(run).slice(0, 320))));
1116
1144
  } else {
1117
1145
  console.log(ui.infoLine(T.run_paused(runLbl(run.status))));
1118
1146
  }
@@ -1818,13 +1846,13 @@ async function metaCmd() {
1818
1846
  const budget = flagNum('--budget', 400);
1819
1847
  const maxRounds = flagNum('--rodadas', flagNum('--rounds', 20));
1820
1848
  const forcedModel = flagStr('--modelo') || flagStr('--model'); // executor fixo (bake-off de modelos)
1821
- const thinker = flagStr('--pensador') || flagStr('--thinker') || 'grok-4.5'; // modelo caro que PENSA quando trava
1849
+ const thinker = flagStr('--pensador') || flagStr('--thinker') || 'gpt-5.6-luna'; // planejador/diagnóstico dentro da equipe oficial
1822
1850
  const maxMinutes = flagNum('--maxmin', 0); // freio de relógio (0 = sem limite)
1823
- const designer = flagStr('--designer') || 'grok-4.5'; // modelo forte que projeta o VISUAL antes de codar
1851
+ const mockup = flagStr('--mockup'); // imagem de referência (ex: mockup do Google AI Studio)
1852
+ const designer = flagStr('--designer') || (mockup ? 'mimo-v2.5' : 'gpt-5.6-luna');
1824
1853
  const design = FLAGS.has('--sem-design') ? false : (flagStr('--design') || 'auto'); // auto = liga em app visual
1825
- const eye = FLAGS.has('--sem-olho') ? null : (flagStr('--olho') || flagStr('--eye') || 'grok-4.5'); // "olho" que critica o print do app
1826
- const visualLadder = !FLAGS.has('--sem-escada') && !FLAGS.has('--barato'); // escada visual: fix visual escala pro modelo forte (caro). --sem-escada/--barato deixa no executor barato
1827
- const mockup = flagStr('--mockup'); // imagem de referência (ex: mockup do Google AI Studio) — designer projeta a partir dela e o olho cobra fidelidade
1854
+ const eye = FLAGS.has('--sem-olho') ? null : (flagStr('--olho') || flagStr('--eye') || 'mimo-v2.5'); // revisão visual multimodal
1855
+ const visualLadder = !FLAGS.has('--sem-escada') && !FLAGS.has('--barato'); // após reprovação objetiva, Luna orienta a correção
1828
1856
  const arch = FLAGS.has('--sem-arch') ? false : (FLAGS.has('--arch') || FLAGS.has('--arquitetura') ? true : 'auto'); // contrato de arquitetura antes de codar (auto = liga em app complexo)
1829
1857
  // ── MODO NOTURNO (auto-continuação por orçamento): retoma sozinho quando o teto
1830
1858
  // de crédito da janela estoura, até a missão terminar OU bater um teto TOTAL. ──
@@ -3169,8 +3197,7 @@ function idioma(l) {
3169
3197
 
3170
3198
  async function nova() {
3171
3199
  const token = needToken();
3172
- const r = await api('/api/conversations', { method: 'POST', token, body: { title: 'CLI' } });
3173
- cfg = config.save({ convId: r.id });
3200
+ await ensureConv(token, 'chat', process.cwd(), true);
3174
3201
  console.log(ui.okLine(T.new_conv));
3175
3202
  }
3176
3203
 
@@ -3280,6 +3307,18 @@ function recallCmd(args) {
3280
3307
  // one-shot em TTY também passa pelo roteador ("ts crie um arquivo..." age);
3281
3308
  // pipe/--json ficam determinísticos no chat (scripts não levam surpresa)
3282
3309
  const text = POS.join(' ');
3310
+ // `--modelo` é uma escolha explícita, não uma sugestão. Antes, um one-shot
3311
+ // como `ts "responda OK" --modelo deepseek-v4-pro` caía no chat simples,
3312
+ // que não recebe esse campo e usava o roteador padrão (frequentemente Flash).
3313
+ // Isso fazia a telemetria parecer honesta, mas quebrava o contrato do usuário.
3314
+ // Direcionamos qualquer one-shot com modelo forçado para o agente: ele propaga
3315
+ // a escolha ao gateway e bloqueia a missão se o provedor devolver outro modelo.
3316
+ if (rawArgs.includes('--modelo') || rawArgs.includes('--model')) {
3317
+ // Passe os posicionais separados: agentCmd remove os valores das flags
3318
+ // (modelo, orçamento, tempo) antes de montar a tarefa. Um único texto
3319
+ // manteria esses números na pergunta exibida ao modelo.
3320
+ return agentCmd(POS);
3321
+ }
3283
3322
  if (process.stdin.isTTY && !JSON_OUT && cfg.token) {
3284
3323
  if (/^\s*(?:(?:quero|preciso)\s+(?:que\s+)?(?:voc[eê]\s+)?|(?:por favor[,\s]+)?)?(?:gere|gerar|crie|criar|fa[çc]a|produza|desenhe|ilustre|edite)\s+(?:uma|a|minha)?\s*(?:imagem|foto|ilustra[çc][aã]o|arte|banner|capa|mockup)\b/i.test(text)) {
3285
3324
  return imageCmd([text]);
package/lib/agent.js CHANGED
@@ -218,7 +218,18 @@ function materialDecisionPreflight(task) {
218
218
  }
219
219
 
220
220
  function actionExpectedForTask(task) {
221
- return /\b(?:cri|fa[cç]|implemente|corrija|edite|altere|instale|execute|rode|deploy|publique|remova|apague|delete|escreva|salve|envie|enviar|mande|mandar|dispare|disparar|encaminhe|encaminhar|responda|responder|send|forward|reply)\w*/i.test(String(task || ''));
221
+ const text = String(task || '');
222
+ // "Responda exatamente: OK" é um pedido de texto, não uma ação externa. Só
223
+ // exigimos prova de ação para responder/encaminhar uma mensagem identificável.
224
+ // Sem essa distinção, perguntas curtas ganhavam um aviso falso de missão não
225
+ // concluída mesmo quando o modelo tinha respondido corretamente.
226
+ if (/\b(?:responda|responder|reply|forward)\b[\s\S]{0,80}\b(?:e-?mail|email|mensagem|message|outlook|gmail|destinat[aá]rio)\b/i.test(text)) return true;
227
+ // "Retorne somente código" pede uma resposta no chat, ainda que o código use
228
+ // verbos como "implemente". Não force uma mutação local fictícia nem polua a
229
+ // entrega com aviso de evidência ausente.
230
+ if (/\b(?:retorne|responda|mostre)\b[\s\S]{0,60}\b(?:somente|apenas)\b/i.test(text)
231
+ && !/\b(?:arquivo|pasta|projeto|diret[oó]rio|salve|grave|crie\s+(?:um\s+)?(?:app|aplicativo|site))\b/i.test(text)) return false;
232
+ return /\b(?:cri|fa[cç]|implemente|corrija|edite|altere|instale|execute|rode|deploy|publique|remova|apague|delete|escreva|salve|envie|enviar|mande|mandar|dispare|disparar|encaminhe|encaminhar|send)\w*/i.test(text);
222
233
  }
223
234
 
224
235
  // Uma auditoria ou investigação solicitada explicitamente em modo somente
@@ -417,7 +428,15 @@ function explicitTaskWorkdir(taskText, exists = fs.existsSync) {
417
428
  // seja silenciosamente rebaixada para `status` nem dispare uma investigação.
418
429
  function restrictedGatewayDecision(taskText, entries = gateways.list()) {
419
430
  const text = gateways.normalized(taskText);
420
- const gateway = gateways.findInText(text, entries);
431
+ // O identificador interno pode ser genérico (por exemplo, "producao").
432
+ // Nunca o use sozinho para desviar uma tarefa local: um objetivo de produto
433
+ // pode falar em "produção" sem ter qualquer relação com o gateway SSH.
434
+ // Para uma operação privilegiada exigimos o alias ou o rótulo completo,
435
+ // ambos termos que o usuário/configuração realmente reconhecem.
436
+ const gateway = entries.find(entry => [entry.label, entry.sshAlias]
437
+ .map(gateways.normalized)
438
+ .filter(Boolean)
439
+ .some(term => text.includes(term))) || null;
421
440
  if (!gateway) return null;
422
441
  // O alias pode conter palavras de operação (ex.: "...-deploy"); elas não
423
442
  // representam intenção do usuário. Classificamos somente o restante do texto.
@@ -562,6 +581,20 @@ function parseTextToolCalls(content) {
562
581
  const xml = /<(?:tool_call|invoke)\s+name=["']([a-zA-Z_][a-zA-Z0-9_.]*)["']>([\s\S]*?)<\/(?:tool_call|invoke)>/g;
563
582
  while ((m = xml.exec(src))) add(m[1], paramsFrom(m[2]));
564
583
 
584
+ // DeepSeek V4 pode devolver chamadas como texto no dialeto DSML, em vez do
585
+ // campo OpenAI `tool_calls`. O gateway continua válido; o harness precisa
586
+ // apenas normalizar esse envelope para não transformar uma ação real em
587
+ // "resposta vazia". Aceitamos somente nome/argumentos com a mesma validação
588
+ // estrita usada nos demais formatos.
589
+ const dsml = /<|DSML|invoke\s+name=["']([a-zA-Z_][a-zA-Z0-9_.]*)["'][^>]*>([\s\S]*?)<\/|DSML|invoke>/g;
590
+ while ((m = dsml.exec(src))) {
591
+ const args = {};
592
+ const params = /<|DSML|parameter\s+name=["']([a-zA-Z_][a-zA-Z0-9_]*)["'][^>]*>([\s\S]*?)<\/|DSML|parameter>/g;
593
+ let p;
594
+ while ((p = params.exec(m[2]))) args[p[1]] = scalar(p[2]);
595
+ add(m[1], args);
596
+ }
597
+
565
598
  // Responses/Codex textual envelope used by some OpenAI-compatible transports.
566
599
  const codexHead = /(?:^|\n)\s*to=functions\.([a-zA-Z_][a-zA-Z0-9_]*)\s+code:\s*\n\s*/g;
567
600
  while ((m = codexHead.exec(src))) {
@@ -611,7 +644,31 @@ function untrustedToolEnvelope(name, content, lang = 'pt') {
611
644
  function isRemoteDeployCommand(command) {
612
645
  return /\b(?:docker\s+(?:compose\s+)?(?:up|deploy|restart)|kubectl\s+(?:apply|rollout|set\s+image)|helm\s+(?:upgrade|install)|systemctl\s+(?:restart|start)|pm2\s+(?:restart|start|reload)|git\s+push|npm\s+run\s+deploy|vercel\s+(?:--prod|deploy)|netlify\s+deploy)\b/i.test(String(command || ''));
613
646
  }
614
- async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools = false, toolsOverride = null, onRetry = null, creditBudget = null, tries = 4 }) {
647
+
648
+ // Navegador é só-leitura até haver ação que pode alterar uma conta, disparar
649
+ // uma compra/publicação ou enviar dados para um provedor visual. A página não
650
+ // é confiável e o modelo não pode transformar "auto" em autorização humana.
651
+ function browserApprovalRequest(input = {}) {
652
+ const action = String(input.acao || '').toLowerCase();
653
+ const ref = String(input.ref || '?').replace(/[^e0-9]/g, '').slice(0, 12) || '?';
654
+ if (action === 'clicar') return {
655
+ kind: 'browser_action',
656
+ cmd: `NAVEGADOR → clicar no elemento ${ref}`,
657
+ warning: 'Este clique pode enviar formulário, publicar, comprar ou alterar dados em um site externo.',
658
+ };
659
+ if (action === 'digitar') return {
660
+ kind: 'browser_action',
661
+ cmd: `NAVEGADOR → digitar ${String(input.texto || '').length} caractere(s) no elemento ${ref}`,
662
+ warning: 'O texto será enviado para uma página externa. Revise e confirme antes de continuar.',
663
+ };
664
+ if (action === 'print' && String(input.pergunta || '').trim()) return {
665
+ kind: 'browser_visual_analysis',
666
+ cmd: 'NAVEGADOR → analisar captura de tela com IA de visão',
667
+ warning: 'A captura da página será enviada ao modelo de visão para análise. Não use em páginas com dados sensíveis sem sua confirmação.',
668
+ };
669
+ return null;
670
+ }
671
+ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools = false, toolsOverride = null, onRetry = null, creditBudget = null, maxCompletionTokens = null, tries = 4 }) {
615
672
  // RESILIÊNCIA: o gateway CDC pode reiniciar/oscilar no meio de uma missão longa.
616
673
  // withRetry cobre conn/timeout/5xx (backoff+jitter); NUNCA re-tenta no_credits/auth.
617
674
  return withRetry(async () => {
@@ -628,7 +685,11 @@ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools =
628
685
  ? { 'X-TS-Credit-Budget': String(Math.floor(Number(creditBudget))) }
629
686
  : {}),
630
687
  },
631
- body: JSON.stringify({ model: model || DEFAULT_EXECUTOR, messages, ...(noTools ? {} : { tools: toolsOverride || tools.DEFS, tool_choice: 'auto' }), stream: false }),
688
+ body: JSON.stringify({ model: model || DEFAULT_EXECUTOR, messages,
689
+ ...(Number.isFinite(Number(maxCompletionTokens)) && Number(maxCompletionTokens) > 0
690
+ ? { max_completion_tokens: Math.max(32, Math.min(Math.floor(Number(maxCompletionTokens)), 16000)) }
691
+ : {}),
692
+ ...(noTools ? {} : { tools: toolsOverride || tools.DEFS, tool_choice: 'auto' }), stream: false }),
632
693
  });
633
694
  // Alguns proxies mantem o socket aberto mesmo apos AbortController.abort().
634
695
  // A corrida garante que a missao receba um timeout tipado no prazo e possa
@@ -1023,6 +1084,10 @@ async function run(task, opts = {}) {
1023
1084
  ? ((lang === 'en' ? '\n\nINSTALLED SKILLS (from the user, in ~/.ts/skills). If ONE of them fits this task, READ its file with ler_arquivo and FOLLOW its instructions:\n' : '\n\nSKILLS INSTALADAS (do usuário, em ~/.ts/skills). Se UMA delas servir pra esta tarefa, LEIA o arquivo dela com ler_arquivo e SIGA as instruções:\n')
1024
1085
  + _sk.map(s => `- ${s.name} (${s.slug}): ${s.description || 'skill'} → ${s.path}`).join('\n'))
1025
1086
  : '';
1087
+ // Caminhos das skills que o próprio harness anunciou ao modelo nesta rodada.
1088
+ // São permitidos SOMENTE para leitura: toda escrita continua confinada ao
1089
+ // projeto ativo, inclusive em missões automáticas.
1090
+ const _trustedSkillPaths = _sk.map(s => s && s.path).filter(Boolean);
1026
1091
  // DESCOBERTA DETERMINÍSTICA: busca a TAREFA no índice de skills disponíveis e injeta as
1027
1092
  // que passaram do limiar. Não depende do modelo lembrar de procurar — o harness procura.
1028
1093
  // Sem match, `sugestaoBlock` é '' e o custo em token é zero. Só nome/descrição entram:
@@ -1172,6 +1237,13 @@ async function run(task, opts = {}) {
1172
1237
  const _mcpSeen = new Set(); // servidores MCP já autorizados NESTA sessão (1ª chamada pede OK)
1173
1238
  const actions = []; // ações REAIS bem-sucedidas (evidência objetiva pro marcador do meta)
1174
1239
  let _inspectionCalls = 0;
1240
+ // A trava de inspeção deve cortar reler o mesmo material, não impedir que o
1241
+ // executor veja o último arquivo de ligação de uma alteração concreta. Uma
1242
+ // única leitura nova, focada, é aceita depois do teto; a segunda volta a ser
1243
+ // loop. Isso evita tanto a investigação infinita quanto o falso bloqueio de
1244
+ // itens pequenos que cruzam estado + interface + prova.
1245
+ const _inspectionSignatures = new Set();
1246
+ let _lateFocusedInspectionUsed = false;
1175
1247
  const _hasExecution = () => actions.some(a => ['escrever_arquivo', 'editar_arquivo', 'editar_documento', 'editar_planilha'].includes(a.name)
1176
1248
  || (a.name === 'executar_comando' && !isInspectionCommand(a.target)));
1177
1249
  const _toolErrs = []; // erros de ferramenta TIPADOS na run (core.classifyToolResult) — sinal duro anti-done-falso
@@ -1191,10 +1263,30 @@ async function run(task, opts = {}) {
1191
1263
  let _incompleteRecoveryAttempts = 0;
1192
1264
  let _inspectionPhaseClosed = false;
1193
1265
  const _actionExpected = requiresActionEvidence(taskText, roMode);
1266
+ // As rodadas de /meta já chegam com um artefato-alvo obrigatório. Ainda assim,
1267
+ // em código existente o executor normalmente precisa ver o layout, o modelo e
1268
+ // um ponto de ligação antes de editar com segurança. Quatro inspeções é um
1269
+ // teto finito que preserva a maior parte da janela para escrever/testar, sem
1270
+ // transformar a proteção de custo em bloqueio prematuro.
1271
+ const _hasMandatoryArtifact = /ARTEFATO(?:\s+DE\s+PROVA)?\s+OBRIGAT[ÓO]RIO|MANDATORY PROOF ARTIFACT/i.test(taskText);
1194
1272
  // Hotfixes não devem virar investigação infinita; missões compostas, porém,
1195
1273
  // precisam ler banco, configuração e fontes antes de escrever. Escala o teto
1196
1274
  // pelo orçamento de passos já autorizado, preservando um limite duro.
1197
- const _inspectionMax = _complexTask ? Math.min(6, Math.max(4, Math.ceil(maxIter * 0.20))) : 4;
1275
+ // Um item atômico de interface existente normalmente precisa de contexto em
1276
+ // HTML, estado, modelo e um segundo trecho paginado do arquivo principal.
1277
+ // Seis leituras ainda cortava fluxos legítimos quando o executor precisava
1278
+ // cruzar store, modelo, interface e prova existente antes de editar. Oito
1279
+ // preserva uma janela finita e ainda deixa passos para escrever, testar e
1280
+ // corrigir; repetição da mesma leitura continua coberta pelo detector de loop.
1281
+ // Retomada de item já investigado não reabre o projeto: o Meta entrega os
1282
+ // fatos da tentativa anterior e deixa só UMA leitura de âncora antes da
1283
+ // mutação. Assim, uma falha objetiva vira correção curta em vez de outra
1284
+ // rodada inteira de exploração.
1285
+ const _inspectionMax = opts.recoveryMode && _actionExpected
1286
+ ? 1
1287
+ : (_hasMandatoryArtifact && _actionExpected
1288
+ ? (opts.atomicMetaItem ? 8 : 2)
1289
+ : (_complexTask ? Math.min(6, Math.max(4, Math.ceil(maxIter * 0.20))) : 4));
1198
1290
  const _inspectionWarnAt = Math.max(2, _inspectionMax - 1);
1199
1291
  let _executionStartedAt = 0;
1200
1292
 
@@ -1236,7 +1328,7 @@ async function run(task, opts = {}) {
1236
1328
  const reply = await llmCall({
1237
1329
  baseUrl: k.baseUrl, key: k.key, messages, model: selectedModel,
1238
1330
  toolsOverride: mainTools, creditBudget: Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets.execution.credits - _phaseUsage.execution.credits)),
1239
- ...extra, signalMs, tries: 1,
1331
+ ...extra, maxCompletionTokens: extra.maxCompletionTokens || 6000, signalMs, tries: 1,
1240
1332
  });
1241
1333
  if (model && forcedModelMismatch(model, reply && reply.model)) {
1242
1334
  const err = new ApiError(`Modelo forçado não foi respeitado: solicitado "${model}", recebido "${reply.model}". A missão foi interrompida sem fallback.`, { status: 409, code: 'model_substituted' });
@@ -1259,7 +1351,20 @@ async function run(task, opts = {}) {
1259
1351
  async function _callRole(role, system, user, validate = null) {
1260
1352
  const phase = role === 'planner' ? 'planning' : (role === 'inspector' ? 'verification' : 'execution');
1261
1353
  const contract = intelligence.agentRoleContract(role, opts.accountPlan || 'free', { complex: _complexTask });
1262
- const candidates = (model ? [selectedModel] : [...contract.candidates]).slice(0, 1 + planUpgradeLimit(opts.accountPlan || 'free'));
1354
+ // O executor pode ser fixado sem sequestrar o planejador. Missões do
1355
+ // produto usam DeepSeek Pro para executar/corrigir e Luna para planejar;
1356
+ // antes desta separação, `model` aplicava DeepSeek também ao planner.
1357
+ const roleForcedModel = role === 'planner' && opts.plannerModel
1358
+ ? String(opts.plannerModel)
1359
+ : model;
1360
+ const baseCandidates = (roleForcedModel ? [role === 'planner' && opts.plannerModel ? roleForcedModel : selectedModel] : [...contract.candidates])
1361
+ .slice(0, 1 + planUpgradeLimit(opts.accountPlan || 'free'));
1362
+ // Plano vazio/JSON inválido é uma falha objetiva, mas não autoriza trocar o
1363
+ // planejador pelo executor. A segunda tentativa curta mantém a Luna como
1364
+ // dona do planejamento e atende inclusive ao Free sem upgrade de modelo.
1365
+ const candidates = role === 'planner' && baseCandidates.length === 1
1366
+ ? [baseCandidates[0], baseCandidates[0]]
1367
+ : baseCandidates;
1263
1368
  let last = null;
1264
1369
  let lastFailureReported = false;
1265
1370
  const phaseStartedAt = Date.now();
@@ -1275,10 +1380,11 @@ async function run(task, opts = {}) {
1275
1380
  _phaseUsage[phase].attempts++;
1276
1381
  onStep({ name: 'agente_' + role, detail: roleModel, phase, model: roleModel });
1277
1382
  const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: roleModel, noTools: true, signalMs,
1383
+ maxCompletionTokens: role === 'planner' ? 2200 : 1400,
1278
1384
  creditBudget: Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets[phase].credits - _phaseUsage[phase].credits)), tries: 1,
1279
1385
  messages: [{ role: 'system', content: contract.prompt + '\n' + system }, { role: 'user', content: user }] });
1280
- if (model && forcedModelMismatch(model, r && r.model)) {
1281
- const err = new ApiError(`Modelo forçado não foi respeitado: solicitado "${model}", recebido "${r.model}". A missão foi interrompida sem fallback.`, { status: 409, code: 'model_substituted' });
1386
+ if (roleForcedModel && forcedModelMismatch(roleForcedModel, r && r.model)) {
1387
+ const err = new ApiError(`Modelo forçado não foi respeitado: solicitado "${roleForcedModel}", recebido "${r.model}". A missão foi interrompida sem fallback.`, { status: 409, code: 'model_substituted' });
1282
1388
  throw err;
1283
1389
  }
1284
1390
  const u = r.usage || {};
@@ -1296,8 +1402,12 @@ async function run(task, opts = {}) {
1296
1402
  onStepDone({ name: 'agente_' + role, ok: false, status: 'error', evidence: last.message });
1297
1403
  lastFailureReported = true;
1298
1404
  if (candidateIndex + 1 < candidates.length) {
1299
- _fallbackTrace.push({ role, from: roleModel, to: candidates[candidateIndex + 1], reason: last.message });
1300
- onStep({ name: 'fallback_modelo', detail: `${last.message}: ${roleModel} ${candidates[candidateIndex + 1]}`, retry: true });
1405
+ const next = candidates[candidateIndex + 1];
1406
+ if (next === roleModel) onStep({ name: 'nova_tentativa', detail: `${last.message}: nova tentativa curta com ${roleModel}`, retry: true });
1407
+ else {
1408
+ _fallbackTrace.push({ role, from: roleModel, to: next, reason: last.message });
1409
+ onStep({ name: 'fallback_modelo', detail: `${last.message}: ${roleModel} → ${next}`, retry: true });
1410
+ }
1301
1411
  }
1302
1412
  continue;
1303
1413
  }
@@ -1313,8 +1423,12 @@ async function run(task, opts = {}) {
1313
1423
  lastFailureReported = true;
1314
1424
  if (candidateIndex + 1 < candidates.length) {
1315
1425
  const reason = String(e && e.message || 'falha objetiva').slice(0, 100);
1316
- _fallbackTrace.push({ role, from: roleModel, to: candidates[candidateIndex + 1], reason });
1317
- onStep({ name: 'fallback_modelo', detail: `${reason}: ${roleModel} ${candidates[candidateIndex + 1]}`, retry: true });
1426
+ const next = candidates[candidateIndex + 1];
1427
+ if (next === roleModel) onStep({ name: 'nova_tentativa', detail: `${reason}: nova tentativa curta com ${roleModel}`, retry: true });
1428
+ else {
1429
+ _fallbackTrace.push({ role, from: roleModel, to: next, reason });
1430
+ onStep({ name: 'fallback_modelo', detail: `${reason}: ${roleModel} → ${next}`, retry: true });
1431
+ }
1318
1432
  }
1319
1433
  if (!isTransientModelError(e) && Number(e && e.status) !== 404 && !/model.*(?:not found|unavailable|unsupported)/i.test(String(e && e.message || ''))) break;
1320
1434
  }
@@ -1367,7 +1481,7 @@ async function run(task, opts = {}) {
1367
1481
  const sys = lang !== 'en'
1368
1482
  ? 'Resuma a conversa de agente abaixo em UM bloco curto e denso (máx ~300 palavras): objetivo, o que já foi feito (arquivos/comandos e resultados), decisões tomadas e o que falta. Preserve caminhos de arquivos e fatos técnicos EXATOS. Sem preâmbulo.'
1369
1483
  : 'Summarize the agent conversation below into ONE short dense block (max ~300 words): goal, what was done (files/commands and results), decisions, and what remains. Preserve EXACT file paths and technical facts. No preamble.';
1370
- const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, signalMs: 60000, creditBudget: Math.max(1, maxCredits - charged - _visionCredits), messages: [{ role: 'system', content: sys }, { role: 'user', content: lines }] });
1484
+ const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, signalMs: 60000, maxCompletionTokens: 900, creditBudget: Math.max(1, maxCredits - charged - _visionCredits), messages: [{ role: 'system', content: sys }, { role: 'user', content: lines }] });
1371
1485
  const u = r.usage || {};
1372
1486
  acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
1373
1487
  charged += (r.billing && r.billing.charged) || 0;
@@ -1396,7 +1510,10 @@ async function run(task, opts = {}) {
1396
1510
  // PLANEJADOR: uma chamada curta, sem ferramentas, antes de tarefas que alteram algo.
1397
1511
  // Decide o que é material para o usuário e transforma o restante em plano/aceite.
1398
1512
  // Falha do planejador degrada para o executor normal; nunca derruba missão viável.
1399
- if (!roMode && _actionExpected && opts.orchestrate !== false
1513
+ // O Meta planejou o item atômico antes de chamar este executor. Quando a
1514
+ // retomada também trouxe um trecho local verificável, planejar de novo só
1515
+ // duplica latência e pode consumir toda a janela antes da correção.
1516
+ if (!opts.skipPlanner && !roMode && _actionExpected && opts.orchestrate !== false
1400
1517
  && (materialDecisionPreflight(taskText) || shouldRunPlanner({ priorMessages: opts.priorMessages, taskText }))) {
1401
1518
  const preflight = materialDecisionPreflight(taskText);
1402
1519
  if (preflight) {
@@ -1410,7 +1527,10 @@ async function run(task, opts = {}) {
1410
1527
  const planner = await _callRole('planner',
1411
1528
  'Responda APENAS JSON válido: {"decision":"execute|ask","decision_kind":"recipient|destination|irreversible|credential|external_authorization|payment|business_choice|technical|cosmetic|none","question":"","reason":"","assumptions":[],"steps":[],"acceptance":[]}. '
1412
1529
  + 'Pergunte SOMENTE se falta uma escolha material que muda o resultado ou autoriza efeito externo/irreversível. NUNCA pergunte sobre framework, cor, layout, nome interno, pasta descobrível, dependência ou detalhe reversível: escolha com bom senso. Se puder descobrir com ferramentas, execute.',
1413
- `TAREFA:\n${taskText}\n\nPROJETO:\n${_projectBrief.slice(0, 8500)}`,
1530
+ `TAREFA:\n${taskText}\n\nPROJETO:\n${_projectBrief.slice(0, 8500)}`
1531
+ + (opts.atomicMetaItem
1532
+ ? '\n\nMODO ITEM ATÔMICO: esta é UMA etapa de checklist, não o projeto inteiro. Produza no máximo 3 passos diretamente ligados a esta etapa e critérios que possam ser provados nesta mesma rodada. Não inclua roadmap, próximas telas ou tarefas futuras.'
1533
+ : ''),
1414
1534
  validatePlannerDecision);
1415
1535
  if (!planner) {
1416
1536
  finalText = 'Não iniciei a execução porque o planejador não produziu um plano válido dentro do orçamento da fase. Nenhum arquivo foi alterado. Você pode tentar novamente; o sistema registrou a falha objetiva para diagnóstico.';
@@ -1501,7 +1621,13 @@ async function run(task, opts = {}) {
1501
1621
  if (!tcs.length) {
1502
1622
  // alguns modelos (MiniMax/DeepSeek) vazam o raciocínio em <think> — o usuário não precisa ver
1503
1623
  const candidateText = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').replace(/<think>[\s\S]*$/i, '').trim();
1504
- if (!roMode && _actionExpected && candidateAdmitsIncomplete(candidateText)
1624
+ // Em uma tarefa que exige alteração comprovável, uma resposta sem ferramenta
1625
+ // não é entrega — mesmo que o modelo tenha escrito uma frase confiante. Antes
1626
+ // este recovery só disparava quando o próprio modelo admitia a pendência; isso
1627
+ // deixava respostas vazias/"completed" encerrarem missões sem modificar nada.
1628
+ const _noActionResponse = !actions.length
1629
+ && (!candidateText || candidateAdmitsIncomplete(candidateText) || _actionExpected);
1630
+ if (!roMode && _actionExpected && _noActionResponse
1505
1631
  && _incompleteRecoveryAttempts < 1 && !_guard()) {
1506
1632
  _incompleteRecoveryAttempts++;
1507
1633
  messages.push({ role: 'assistant', content: candidateText });
@@ -1807,6 +1933,26 @@ async function run(task, opts = {}) {
1807
1933
  onStep({ name, detail: argsShort(name, input), blocked: true });
1808
1934
  }
1809
1935
  }
1936
+ // Navegador assistido: abrir/ler é seguro; clicar, digitar e analisar uma
1937
+ // captura com modelo externo exigem confirmação SEMPRE. Nem --yolo silencia
1938
+ // este gate, porque o agente não consegue saber se um botão publica, compra
1939
+ // ou envia um formulário.
1940
+ if (result === undefined && name === 'navegador') {
1941
+ const browserRequest = browserApprovalRequest(input);
1942
+ if (browserRequest) {
1943
+ let approved = false, remoteTried = false;
1944
+ if (!yes) {
1945
+ const dec = await askApprove(browserRequest);
1946
+ approved = decideApproval({ autoAll: false, decision: dec }).approved;
1947
+ } else ({ approved, remoteTried } = await _remoteApprove(browserRequest.cmd, token, onRemote));
1948
+ if (!approved) {
1949
+ result = { erro: yes
1950
+ ? (remoteTried ? 'RECUSADO: o dono negou ou não respondeu à ação no navegador.' : 'RECUSADO: ação no navegador exige aprovação remota no modo --yes.')
1951
+ : 'O usuário recusou a ação no navegador. A página continua apenas em modo de leitura.' };
1952
+ onStep({ name: 'navegador', detail: browserRequest.cmd, blocked: true });
1953
+ }
1954
+ }
1955
+ }
1810
1956
  // Gate de AUTO-MODIFICAÇÃO (skills): o agente só cria/melhora skill com o humano
1811
1957
  // vendo a prévia. Em --yes (cron) NÃO pergunta nem grava live — marca _stage e a
1812
1958
  // tools grava em ~/.ts/skills-pending pro dono revisar depois (ts skills pendentes).
@@ -1935,8 +2081,21 @@ async function run(task, opts = {}) {
1935
2081
  // ORÇAMENTO DE INSPEÇÃO: em tarefa de ação, leitura e diagnóstico são uma fase
1936
2082
  // finita. Reserva as rodadas restantes para editar, testar e corrigir, independente
1937
2083
  // de o modelo executor ser conservador ou insistir em reler o projeto inteiro.
1938
- const _inspectionOnly = READONLY.has(name) || (name === 'executar_comando' && isInspectionCommand(input.comando));
1939
- if (result === undefined && _actionExpected && _inspectionOnly && _inspectionPhaseClosed && !_hasExecution()) {
2084
+ // A leitura de uma skill que o harness acabou de anunciar não é
2085
+ // investigação do projeto. Contá-la no teto de inspeção deixava apenas
2086
+ // uma leitura do código antes de bloquear a edição em missões /meta.
2087
+ const _trustedSkillRead = name === 'ler_arquivo'
2088
+ && _trustedSkillPaths.some(allowed => path.resolve(String(allowed)) === path.resolve(String(input && input.caminho || '')));
2089
+ const _inspectionOnly = !_trustedSkillRead && (READONLY.has(name) || (name === 'executar_comando' && isInspectionCommand(input.comando)));
2090
+ const _inspectionSignature = name === 'ler_arquivo'
2091
+ ? `file:${path.resolve(cwd, String(input && input.caminho || ''))}`
2092
+ : (name === 'executar_comando'
2093
+ ? `command:${String(input && input.comando || '').trim().replace(/\s+/g, ' ')}`
2094
+ : `${name}:${JSON.stringify(input || {})}`);
2095
+ const _lateFocusedInspection = _inspectionPhaseClosed
2096
+ && !_lateFocusedInspectionUsed
2097
+ && !_inspectionSignatures.has(_inspectionSignature);
2098
+ if (result === undefined && _actionExpected && _inspectionOnly && _inspectionPhaseClosed && !_hasExecution() && !_lateFocusedInspection) {
1940
2099
  // O turno anterior já explicou que a investigação acabou. Repetir uma
1941
2100
  // leitura agora não é exploração legítima: é insistência sem progresso.
1942
2101
  // Cortamos aqui, antes de gastar mais turnos até MAX_ITER, e deixamos
@@ -1949,15 +2108,21 @@ async function run(task, opts = {}) {
1949
2108
  steps++;
1950
2109
  }
1951
2110
  if (result === undefined && _actionExpected && _inspectionOnly && !_hasExecution()) {
1952
- _inspectionCalls++;
1953
- const _phase = inspectionGateDecision({ actionExpected: true, hasMutation: false, calls: _inspectionCalls, warnAt: _inspectionWarnAt, max: _inspectionMax });
1954
- if (_phase === 'block') {
1955
- _inspectionPhaseClosed = true;
1956
- _forceActionGate = true;
1957
- onStep({ name, detail: 'fase de inspeção encerrada', blocked: true });
1958
- result = { erro: 'FASE DE INSPEÇÃO ENCERRADA: o pacote do projeto e as leituras anteriores já são suficientes. Não leia nem pesquise mais. A próxima chamada deve EDITAR/ESCREVER/EXECUTAR a solução; depois rode os testes.' };
1959
- steps++;
1960
- } else if (_phase === 'warn') _inspectionWarning = true;
2111
+ _inspectionSignatures.add(_inspectionSignature);
2112
+ if (_lateFocusedInspection) {
2113
+ _lateFocusedInspectionUsed = true;
2114
+ onStep({ name, detail: 'leitura focada final autorizada antes da edição', retry: true });
2115
+ } else {
2116
+ _inspectionCalls++;
2117
+ const _phase = inspectionGateDecision({ actionExpected: true, hasMutation: false, calls: _inspectionCalls, warnAt: _inspectionWarnAt, max: _inspectionMax });
2118
+ if (_phase === 'block') {
2119
+ _inspectionPhaseClosed = true;
2120
+ _forceActionGate = true;
2121
+ onStep({ name, detail: 'fase de inspeção encerrada', blocked: true });
2122
+ result = { erro: 'FASE DE INSPEÇÃO ENCERRADA: o pacote do projeto e as leituras anteriores já são suficientes. Não leia nem pesquise mais. A próxima chamada deve EDITAR/ESCREVER/EXECUTAR a solução; depois rode os testes.' };
2123
+ steps++;
2124
+ } else if (_phase === 'warn') _inspectionWarning = true;
2125
+ }
1961
2126
  }
1962
2127
  if (result === undefined && READONLY.has(name)) {
1963
2128
  const _cached = _cachedRead;
@@ -1971,7 +2136,7 @@ async function run(task, opts = {}) {
1971
2136
  }
1972
2137
  if (result === undefined) {
1973
2138
  onStep({ name, detail: argsShort(name, input) });
1974
- result = await tools.execute(name, input, { confineDir, baseDir: cwd, token, allowedTools: _allowedToolNames });
2139
+ result = await tools.execute(name, input, { confineDir, baseDir: cwd, token, allowedTools: _allowedToolNames, trustedReadPaths: _trustedSkillPaths });
1975
2140
  steps++; _ran = true;
1976
2141
  // TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
1977
2142
  // sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
@@ -2114,7 +2279,7 @@ async function run(task, opts = {}) {
2114
2279
  try {
2115
2280
  const signalMs = modelCallWindow({ maxDurationMs, elapsedMs: Date.now() - missionStartedAt });
2116
2281
  if (!signalMs) throw new ApiError('model_timeout', { code: 'timeout' });
2117
- const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs, tries: 1, creditBudget: Math.max(1, maxCredits - charged - _visionCredits) });
2282
+ const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs, maxCompletionTokens: 900, tries: 1, creditBudget: Math.max(1, maxCredits - charged - _visionCredits) });
2118
2283
  const u = r.usage || {};
2119
2284
  acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
2120
2285
  charged += (r.billing && r.billing.charged) || 0;
@@ -2235,4 +2400,4 @@ async function run(task, opts = {}) {
2235
2400
  return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped, completion, verification: verifyReport, observability: _observability, report: _finalReport, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage }, missionCache: _missionCache.stats() };
2236
2401
  }
2237
2402
 
2238
- module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, executorFallbackChain, forcedModelMismatch, isTransientModelError, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand } };
2403
+ module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, executorFallbackChain, forcedModelMismatch, isTransientModelError, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ // Conversas de chat precisam ter continuidade DENTRO do mesmo trabalho, mas nunca
4
+ // carregar o contexto de um projeto para outro. Um único `convId` global fazia uma
5
+ // pergunta feita em D:\\CineHub herdar uma conversa antiga do EspecialRO.
6
+ const crypto = require('crypto');
7
+ const path = require('path');
8
+
9
+ function workspaceScope(cwd = process.cwd(), kind = 'chat') {
10
+ const absolute = path.resolve(cwd || process.cwd());
11
+ const normalized = process.platform === 'win32' ? absolute.toLowerCase() : absolute;
12
+ const digest = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
13
+ return `${String(kind || 'chat').toLowerCase()}:${digest}`;
14
+ }
15
+
16
+ function conversationTitle(cwd = process.cwd(), kind = 'CLI') {
17
+ const name = path.basename(path.resolve(cwd || process.cwd())) || 'início';
18
+ return `${kind} · ${name}`.slice(0, 120);
19
+ }
20
+
21
+ module.exports = { workspaceScope, conversationTitle };