terminal-smart-cli 0.97.31 → 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 +42 -16
- package/lib/agent.js +178 -27
- package/lib/conversation-scope.js +21 -0
- package/lib/meta.js +300 -44
- package/lib/tools.js +32 -1
- package/lib/verify.js +14 -1
- package/package.json +2 -2
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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) {
|
|
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
|
-
|
|
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;
|
|
@@ -1831,13 +1846,13 @@ async function metaCmd() {
|
|
|
1831
1846
|
const budget = flagNum('--budget', 400);
|
|
1832
1847
|
const maxRounds = flagNum('--rodadas', flagNum('--rounds', 20));
|
|
1833
1848
|
const forcedModel = flagStr('--modelo') || flagStr('--model'); // executor fixo (bake-off de modelos)
|
|
1834
|
-
const thinker = flagStr('--pensador') || flagStr('--thinker') || '
|
|
1849
|
+
const thinker = flagStr('--pensador') || flagStr('--thinker') || 'gpt-5.6-luna'; // planejador/diagnóstico dentro da equipe oficial
|
|
1835
1850
|
const maxMinutes = flagNum('--maxmin', 0); // freio de relógio (0 = sem limite)
|
|
1836
|
-
const
|
|
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');
|
|
1837
1853
|
const design = FLAGS.has('--sem-design') ? false : (flagStr('--design') || 'auto'); // auto = liga em app visual
|
|
1838
|
-
const eye = FLAGS.has('--sem-olho') ? null : (flagStr('--olho') || flagStr('--eye') || '
|
|
1839
|
-
const visualLadder = !FLAGS.has('--sem-escada') && !FLAGS.has('--barato'); //
|
|
1840
|
-
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
|
|
1841
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)
|
|
1842
1857
|
// ── MODO NOTURNO (auto-continuação por orçamento): retoma sozinho quando o teto
|
|
1843
1858
|
// de crédito da janela estoura, até a missão terminar OU bater um teto TOTAL. ──
|
|
@@ -3182,8 +3197,7 @@ function idioma(l) {
|
|
|
3182
3197
|
|
|
3183
3198
|
async function nova() {
|
|
3184
3199
|
const token = needToken();
|
|
3185
|
-
|
|
3186
|
-
cfg = config.save({ convId: r.id });
|
|
3200
|
+
await ensureConv(token, 'chat', process.cwd(), true);
|
|
3187
3201
|
console.log(ui.okLine(T.new_conv));
|
|
3188
3202
|
}
|
|
3189
3203
|
|
|
@@ -3293,6 +3307,18 @@ function recallCmd(args) {
|
|
|
3293
3307
|
// one-shot em TTY também passa pelo roteador ("ts crie um arquivo..." age);
|
|
3294
3308
|
// pipe/--json ficam determinísticos no chat (scripts não levam surpresa)
|
|
3295
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
|
+
}
|
|
3296
3322
|
if (process.stdin.isTTY && !JSON_OUT && cfg.token) {
|
|
3297
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)) {
|
|
3298
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,14 @@ 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
|
-
|
|
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'));
|
|
1263
1362
|
// Plano vazio/JSON inválido é uma falha objetiva, mas não autoriza trocar o
|
|
1264
1363
|
// planejador pelo executor. A segunda tentativa curta mantém a Luna como
|
|
1265
1364
|
// dona do planejamento e atende inclusive ao Free sem upgrade de modelo.
|
|
@@ -1281,10 +1380,11 @@ async function run(task, opts = {}) {
|
|
|
1281
1380
|
_phaseUsage[phase].attempts++;
|
|
1282
1381
|
onStep({ name: 'agente_' + role, detail: roleModel, phase, model: roleModel });
|
|
1283
1382
|
const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: roleModel, noTools: true, signalMs,
|
|
1383
|
+
maxCompletionTokens: role === 'planner' ? 2200 : 1400,
|
|
1284
1384
|
creditBudget: Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets[phase].credits - _phaseUsage[phase].credits)), tries: 1,
|
|
1285
1385
|
messages: [{ role: 'system', content: contract.prompt + '\n' + system }, { role: 'user', content: user }] });
|
|
1286
|
-
if (
|
|
1287
|
-
const err = new ApiError(`Modelo forçado não foi respeitado: solicitado "${
|
|
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' });
|
|
1288
1388
|
throw err;
|
|
1289
1389
|
}
|
|
1290
1390
|
const u = r.usage || {};
|
|
@@ -1381,7 +1481,7 @@ async function run(task, opts = {}) {
|
|
|
1381
1481
|
const sys = lang !== 'en'
|
|
1382
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.'
|
|
1383
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.';
|
|
1384
|
-
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 }] });
|
|
1385
1485
|
const u = r.usage || {};
|
|
1386
1486
|
acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
|
|
1387
1487
|
charged += (r.billing && r.billing.charged) || 0;
|
|
@@ -1410,7 +1510,10 @@ async function run(task, opts = {}) {
|
|
|
1410
1510
|
// PLANEJADOR: uma chamada curta, sem ferramentas, antes de tarefas que alteram algo.
|
|
1411
1511
|
// Decide o que é material para o usuário e transforma o restante em plano/aceite.
|
|
1412
1512
|
// Falha do planejador degrada para o executor normal; nunca derruba missão viável.
|
|
1413
|
-
|
|
1513
|
+
// O Meta já 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
|
|
1414
1517
|
&& (materialDecisionPreflight(taskText) || shouldRunPlanner({ priorMessages: opts.priorMessages, taskText }))) {
|
|
1415
1518
|
const preflight = materialDecisionPreflight(taskText);
|
|
1416
1519
|
if (preflight) {
|
|
@@ -1424,7 +1527,10 @@ async function run(task, opts = {}) {
|
|
|
1424
1527
|
const planner = await _callRole('planner',
|
|
1425
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":[]}. '
|
|
1426
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.',
|
|
1427
|
-
`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
|
+
: ''),
|
|
1428
1534
|
validatePlannerDecision);
|
|
1429
1535
|
if (!planner) {
|
|
1430
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.';
|
|
@@ -1515,7 +1621,13 @@ async function run(task, opts = {}) {
|
|
|
1515
1621
|
if (!tcs.length) {
|
|
1516
1622
|
// alguns modelos (MiniMax/DeepSeek) vazam o raciocínio em <think> — o usuário não precisa ver
|
|
1517
1623
|
const candidateText = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').replace(/<think>[\s\S]*$/i, '').trim();
|
|
1518
|
-
|
|
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
|
|
1519
1631
|
&& _incompleteRecoveryAttempts < 1 && !_guard()) {
|
|
1520
1632
|
_incompleteRecoveryAttempts++;
|
|
1521
1633
|
messages.push({ role: 'assistant', content: candidateText });
|
|
@@ -1821,6 +1933,26 @@ async function run(task, opts = {}) {
|
|
|
1821
1933
|
onStep({ name, detail: argsShort(name, input), blocked: true });
|
|
1822
1934
|
}
|
|
1823
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
|
+
}
|
|
1824
1956
|
// Gate de AUTO-MODIFICAÇÃO (skills): o agente só cria/melhora skill com o humano
|
|
1825
1957
|
// vendo a prévia. Em --yes (cron) NÃO pergunta nem grava live — marca _stage e a
|
|
1826
1958
|
// tools grava em ~/.ts/skills-pending pro dono revisar depois (ts skills pendentes).
|
|
@@ -1949,8 +2081,21 @@ async function run(task, opts = {}) {
|
|
|
1949
2081
|
// ORÇAMENTO DE INSPEÇÃO: em tarefa de ação, leitura e diagnóstico são uma fase
|
|
1950
2082
|
// finita. Reserva as rodadas restantes para editar, testar e corrigir, independente
|
|
1951
2083
|
// de o modelo executor ser conservador ou insistir em reler o projeto inteiro.
|
|
1952
|
-
|
|
1953
|
-
|
|
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) {
|
|
1954
2099
|
// O turno anterior já explicou que a investigação acabou. Repetir uma
|
|
1955
2100
|
// leitura agora não é exploração legítima: é insistência sem progresso.
|
|
1956
2101
|
// Cortamos aqui, antes de gastar mais turnos até MAX_ITER, e deixamos
|
|
@@ -1963,15 +2108,21 @@ async function run(task, opts = {}) {
|
|
|
1963
2108
|
steps++;
|
|
1964
2109
|
}
|
|
1965
2110
|
if (result === undefined && _actionExpected && _inspectionOnly && !_hasExecution()) {
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
+
}
|
|
1975
2126
|
}
|
|
1976
2127
|
if (result === undefined && READONLY.has(name)) {
|
|
1977
2128
|
const _cached = _cachedRead;
|
|
@@ -1985,7 +2136,7 @@ async function run(task, opts = {}) {
|
|
|
1985
2136
|
}
|
|
1986
2137
|
if (result === undefined) {
|
|
1987
2138
|
onStep({ name, detail: argsShort(name, input) });
|
|
1988
|
-
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 });
|
|
1989
2140
|
steps++; _ran = true;
|
|
1990
2141
|
// TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
|
|
1991
2142
|
// sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
|
|
@@ -2128,7 +2279,7 @@ async function run(task, opts = {}) {
|
|
|
2128
2279
|
try {
|
|
2129
2280
|
const signalMs = modelCallWindow({ maxDurationMs, elapsedMs: Date.now() - missionStartedAt });
|
|
2130
2281
|
if (!signalMs) throw new ApiError('model_timeout', { code: 'timeout' });
|
|
2131
|
-
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) });
|
|
2132
2283
|
const u = r.usage || {};
|
|
2133
2284
|
acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
|
|
2134
2285
|
charged += (r.billing && r.billing.charged) || 0;
|
|
@@ -2249,4 +2400,4 @@ async function run(task, opts = {}) {
|
|
|
2249
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() };
|
|
2250
2401
|
}
|
|
2251
2402
|
|
|
2252
|
-
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 };
|
package/lib/meta.js
CHANGED
|
@@ -11,8 +11,11 @@ const path = require('path');
|
|
|
11
11
|
const cp = require('child_process');
|
|
12
12
|
const crypto = require('crypto');
|
|
13
13
|
const compactor = require('./compactor');
|
|
14
|
+
const fileLock = require('./file-lock');
|
|
14
15
|
const { api, ApiError } = require('./api');
|
|
15
16
|
const keyring = require('./keyring');
|
|
17
|
+
const toolRunner = require('./tools');
|
|
18
|
+
const intelligence = require('./intelligence-core');
|
|
16
19
|
// Teto de IA estourado (free/plano) no gateway → 402 (ou 429 com type cost_cap). Os helpers de
|
|
17
20
|
// IA da missão DEVEM lançar (não retornar vazio em silêncio), senão a missão desperdiça rodadas
|
|
18
21
|
// fingindo trabalhar. O run() pausa (estado salvo = resumível) e mostra CTA de upgrade.
|
|
@@ -22,6 +25,15 @@ function _capGuard(res, j) {
|
|
|
22
25
|
}
|
|
23
26
|
const agent = require('./agent');
|
|
24
27
|
|
|
28
|
+
// Uma chamada de Meta pode ir direto ao gateway (sem passar pelo loop de
|
|
29
|
+
// agent.js). Preserve aqui o mesmo contrato: quando escolhemos um modelo
|
|
30
|
+
// específico para planejar, revisar ou recuperar uma edição, o gateway não
|
|
31
|
+
// pode trocar para outro sem avisar. IDs com prefixo de provedor normalizam.
|
|
32
|
+
function modelWasSubstituted(requested, returned) {
|
|
33
|
+
if (!requested || !returned) return false;
|
|
34
|
+
return intelligence.normalizeModelId(requested) !== intelligence.normalizeModelId(returned);
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
const FILE = '.ts-meta.json';
|
|
26
38
|
const MAX_ATTEMPTS_PER_ITEM = 2;
|
|
27
39
|
const MAX_ESCALATIONS = 3; // quantas vezes chamamos o modelo CARO "pensador"
|
|
@@ -51,6 +63,23 @@ function artifactForMetaItem(item, st, dir = '') {
|
|
|
51
63
|
const criteria = Array.isArray(st && st.criteria) ? st.criteria : [];
|
|
52
64
|
const namedCriterion = criteria.find(c => c && c.path && d.includes(path.basename(String(c.path)).toLowerCase()));
|
|
53
65
|
if (namedCriterion) return String(namedCriterion.path);
|
|
66
|
+
// Electron não usa index.html como artefato principal para o estado local.
|
|
67
|
+
// Associar a persistência ao módulo real impede que a rodada seja marcada
|
|
68
|
+
// sem prova ou que procure o fallback genérico artifacts/iN.md.
|
|
69
|
+
const electronProject = dir && fs.existsSync(path.join(dir, 'package.json'))
|
|
70
|
+
&& /electron/i.test(String(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')));
|
|
71
|
+
if (electronProject && /persist[êe]ncia|electron-store|salvar.*carregar|carregar.*salvar|armazenamento local/.test(d)) return 'src/store.js';
|
|
72
|
+
// Um checklist Electron precisa sempre apontar para o produto executável, nunca
|
|
73
|
+
// para o fallback artifacts/iN.md. Caso contrário o agente pode gastar uma
|
|
74
|
+
// rodada inteira escrevendo documentação sem mover o aplicativo adiante.
|
|
75
|
+
if (electronProject && /modelo de dados|filme.*s[ée]rie.*livro|estado.*nota|nota.*coment[áa]rio|tags.*progresso/.test(d)) return 'src/model.js';
|
|
76
|
+
if (electronProject && /transi[çc][ãa]o de estado|busca.*filtro|filtro.*busca/.test(d)) return 'src/model.js';
|
|
77
|
+
if (electronProject && /exporta[çc][ãa]o|importa[çc][ãa]o|backup/.test(d)) return 'src/store.js';
|
|
78
|
+
if (electronProject && /teste|verifica[çc][ãa]o automatizada|npm test/.test(d)) return 'test/core.test.js';
|
|
79
|
+
if (electronProject && /readme|instru[çc][õo]es/.test(d)) return 'README.md';
|
|
80
|
+
if (electronProject && /integra[çc][õo]es futuras|tmdb|google books/.test(d)) return 'INTEGRACOES-FUTURAS.md';
|
|
81
|
+
if (electronProject && /modelo de neg[óo]cio|freemium|privacidade|an[uú]ncio/.test(d)) return 'MODELO-DE-NEGOCIO.md';
|
|
82
|
+
if (electronProject && /interface|tela|biblioteca|descobrir|cole[çc][ãa]o demo|limpar demo/.test(d)) return 'src/index.html';
|
|
54
83
|
const splitWebProject = dir && fs.existsSync(path.join(dir, 'index.html')) && fs.existsSync(path.join(dir, 'src', 'game.js'));
|
|
55
84
|
// In an existing browser game, UI/HUD requests are implementation work.
|
|
56
85
|
// The generic UI-spec fallback below is only for greenfield design projects.
|
|
@@ -84,6 +113,72 @@ function artifactHash(file) {
|
|
|
84
113
|
catch (_) { return null; }
|
|
85
114
|
}
|
|
86
115
|
|
|
116
|
+
// A retomada não deve reabrir uma investigação inteira apenas para descobrir
|
|
117
|
+
// onde fazer um ajuste já conhecido. Entregamos um recorte determinístico do
|
|
118
|
+
// artefato-alvo, com números de linha, para que o executor tenha uma âncora
|
|
119
|
+
// exata de edição. Isso é contexto local verificável, não uma conclusão da IA.
|
|
120
|
+
function recoveryTargetExcerpt(file, item, maxChars = 7000) {
|
|
121
|
+
if (!file || !fs.existsSync(file)) return '';
|
|
122
|
+
let source;
|
|
123
|
+
try { source = fs.readFileSync(file, 'utf8'); }
|
|
124
|
+
catch (_) { return ''; }
|
|
125
|
+
const fold = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
126
|
+
const ignored = new Set(['implementar', 'criar', 'fazer', 'para', 'como', 'com', 'sem', 'uma', 'este', 'esta', 'item', 'dados']);
|
|
127
|
+
const terms = [...new Set(fold(item && item.desc).match(/[a-z0-9_-]{4,}/g) || [])]
|
|
128
|
+
.filter(term => !ignored.has(term));
|
|
129
|
+
if (!terms.length) return '';
|
|
130
|
+
const lines = source.split(/\r?\n/);
|
|
131
|
+
const ranked = lines.map((line, index) => ({
|
|
132
|
+
index,
|
|
133
|
+
score: terms.reduce((total, term) => total + (fold(line).includes(term) ? 1 : 0), 0),
|
|
134
|
+
})).filter(row => row.score > 0).sort((a, b) => b.score - a.score || a.index - b.index).slice(0, 5);
|
|
135
|
+
if (!ranked.length) return '';
|
|
136
|
+
const selected = new Set();
|
|
137
|
+
for (const row of ranked) {
|
|
138
|
+
for (let index = Math.max(0, row.index - 4); index <= Math.min(lines.length - 1, row.index + 4); index++) selected.add(index);
|
|
139
|
+
}
|
|
140
|
+
const excerpt = [...selected].sort((a, b) => a - b)
|
|
141
|
+
.map(index => `${String(index + 1).padStart(5, ' ')} | ${lines[index]}`).join('\n');
|
|
142
|
+
return excerpt.slice(0, maxChars);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Recuperação de microedição: quando o próprio Meta já tem a prova local e a
|
|
146
|
+
// âncora exata, não delegamos de novo a escolha de ferramentas ao loop geral.
|
|
147
|
+
// O DeepSeek Pro propõe exclusivamente a troca literal; o harness a aplica com
|
|
148
|
+
// editar_arquivo, que exige âncora única e faz backup. Assim a IA não consegue
|
|
149
|
+
// gastar a janela tentando pesquisar o mesmo arquivo pela quarta vez.
|
|
150
|
+
async function anchoredRecoveryEdit({ token, dir, expectedArtifact, expectedOnDisk, recoveryExcerpt, previousItemFacts, lang }) {
|
|
151
|
+
const localized = lang === 'en';
|
|
152
|
+
const system = localized
|
|
153
|
+
? 'You are the DeepSeek Pro code executor. Return ONLY valid JSON: {"buscar":"exact existing text from the excerpt","substituir":"replacement text","resumo":"short factual summary"}. Make one minimal edit that fulfills the requested checklist item. Do not explain, do not use Markdown, do not propose a plan.'
|
|
154
|
+
: 'Você é o executor de código DeepSeek Pro. Responda SOMENTE JSON válido: {"buscar":"trecho existente EXATO do recorte","substituir":"texto substituto","resumo":"resumo factual curto"}. Faça uma única edição mínima que cumpra o item do checklist. Não explique, não use Markdown, não proponha plano.';
|
|
155
|
+
const user = localized
|
|
156
|
+
? `CHECKLIST ITEM: ${String(expectedArtifact || '')}\nPREVIOUS FACTS:\n${previousItemFacts}\n\nTARGET SOURCE EXCERPT:\n${recoveryExcerpt}`
|
|
157
|
+
: `ITEM DO CHECKLIST: ${String(expectedArtifact || '')}\nFATOS ANTERIORES:\n${previousItemFacts}\n\nTRECHO DO ARTEFATO-ALVO:\n${recoveryExcerpt}`;
|
|
158
|
+
const proposal = await _llmJson({ token, model: 'deepseek-v4-pro', system, user, maxTokens: 2200 });
|
|
159
|
+
const patch = proposal && proposal.json || {};
|
|
160
|
+
const buscar = String(patch.buscar || '');
|
|
161
|
+
const substituir = String(patch.substituir || '');
|
|
162
|
+
if (!buscar || !substituir || buscar === substituir) throw new Error('RECOVERY_PATCH_INVALID: o executor não devolveu uma substituição válida');
|
|
163
|
+
const target = path.resolve(expectedOnDisk || path.join(dir, expectedArtifact || ''));
|
|
164
|
+
const confined = path.resolve(dir);
|
|
165
|
+
if (target !== confined && !target.startsWith(confined + path.sep)) throw new Error('RECOVERY_PATCH_SCOPE: artefato fora do projeto');
|
|
166
|
+
const result = await toolRunner.execute('editar_arquivo', { caminho: target, buscar, substituir }, {
|
|
167
|
+
confineDir: dir, baseDir: dir, allowedTools: ['editar_arquivo'],
|
|
168
|
+
});
|
|
169
|
+
if (!result || result.erro || !result.ok) throw new Error('RECOVERY_PATCH_FAILED: ' + String(result && (result.erro || result.stderr) || 'edição não confirmada'));
|
|
170
|
+
return {
|
|
171
|
+
text: String(patch.resumo || (localized ? 'Anchored recovery edit persisted.' : 'Edição de recuperação por âncora persistida.')),
|
|
172
|
+
steps: 1,
|
|
173
|
+
credits: Number(proposal.credits || 0),
|
|
174
|
+
tokens: { inTok: 0, outTok: 0, cachedTok: 0 },
|
|
175
|
+
model: 'deepseek-v4-pro',
|
|
176
|
+
actions: [{ name: 'editar_arquivo', target, result }],
|
|
177
|
+
toolErrors: [],
|
|
178
|
+
completion: { ok: true, reason: 'anchored_recovery_edit' },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
87
182
|
function normalizeMetaArtifact(relativePath, raw) {
|
|
88
183
|
let text = String(raw || '').trim().replace(/^```(?:json|gdscript|markdown|md)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
|
|
89
184
|
if (/\.json$/i.test(relativePath)) {
|
|
@@ -912,48 +1007,79 @@ function save(st, dir) {
|
|
|
912
1007
|
}
|
|
913
1008
|
}
|
|
914
1009
|
|
|
915
|
-
// Chamada JSON
|
|
1010
|
+
// Chamada JSON do planejador/verificador. Nunca fixa Flash: o papel que chama
|
|
1011
|
+
// escolhe o modelo conforme o contrato oficial e o plano do usuário.
|
|
916
1012
|
let _key = null;
|
|
917
1013
|
let _keyNuvem = null; // credencial do GATEWAY (visão) — separada da BYOK
|
|
918
|
-
async function _llmJson({ token, system, user, maxTokens = 900 }) {
|
|
1014
|
+
async function _llmJson({ token, system, user, maxTokens = 900, model = 'gpt-5.6-luna' }) {
|
|
919
1015
|
if (!_key) _key = await keyring.resolve(token, { feature: 'cli_agent' });
|
|
920
1016
|
const ctrl = new AbortController();
|
|
921
|
-
|
|
1017
|
+
let timer;
|
|
922
1018
|
let res;
|
|
923
1019
|
try {
|
|
924
|
-
|
|
1020
|
+
const request = fetch(String(_key.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
|
|
925
1021
|
method: 'POST', signal: ctrl.signal,
|
|
926
1022
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + _key.key },
|
|
927
|
-
body: JSON.stringify({ model: keyring.modeloPara(_key,
|
|
1023
|
+
body: JSON.stringify({ model: keyring.modeloPara(_key, model), stream: false, max_completion_tokens: maxTokens,
|
|
928
1024
|
messages: [{ role: 'system', content: system }, { role: 'user', content: user }] }),
|
|
929
1025
|
});
|
|
1026
|
+
// Alguns gateways não encerram o socket imediatamente após abort(). A
|
|
1027
|
+
// corrida garante que o Meta devolva uma falha tratável em até 60 s, em
|
|
1028
|
+
// vez de deixar a missão presa em "pensando" indefinidamente.
|
|
1029
|
+
const deadline = new Promise((_, reject) => {
|
|
1030
|
+
timer = setTimeout(() => { ctrl.abort(); reject(new Error('META_MODEL_TIMEOUT')); }, 60000);
|
|
1031
|
+
});
|
|
1032
|
+
res = await Promise.race([request, deadline]);
|
|
930
1033
|
} finally { clearTimeout(timer); }
|
|
931
1034
|
const j = await res.json().catch(() => null);
|
|
932
1035
|
if (!res.ok) _capGuard(res, j);
|
|
1036
|
+
if (modelWasSubstituted(model, j && j.model)) {
|
|
1037
|
+
throw new ApiError(`Modelo exigido não foi respeitado: solicitado "${model}", recebido "${j.model}".`, {
|
|
1038
|
+
status: 409, code: 'model_substituted',
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
const content = String(j?.choices?.[0]?.message?.content || '').trim();
|
|
1042
|
+
if (!content) {
|
|
1043
|
+
const providerMessage = String(j?.error?.message || j?.message || '').trim();
|
|
1044
|
+
throw new ApiError(`O modelo ${model || 'selecionado'} retornou um JSON vazio${providerMessage ? ': ' + providerMessage : '.'}`, {
|
|
1045
|
+
status: 502, code: 'empty_model_response',
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
933
1048
|
const u = j?.usage || {};
|
|
934
1049
|
let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
|
|
935
1050
|
if (u.prompt_tokens) {
|
|
936
|
-
if (!_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model
|
|
1051
|
+
if (!_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
|
|
937
1052
|
}
|
|
938
|
-
return { json: _tolerantJson(
|
|
1053
|
+
return { json: _tolerantJson(content), credits };
|
|
939
1054
|
}
|
|
940
1055
|
|
|
941
1056
|
// Chamada de TEXTO com modelo arbitrário — usada pelo PENSADOR (escalonamento).
|
|
942
1057
|
async function _llmText({ token, model, system, user, maxTokens = 1200, creditBudget = null }) {
|
|
943
1058
|
if (!_key) _key = await keyring.resolve(token, { feature: 'cli_agent' });
|
|
944
1059
|
const ctrl = new AbortController();
|
|
945
|
-
|
|
1060
|
+
let timer;
|
|
946
1061
|
let res;
|
|
947
1062
|
try {
|
|
948
|
-
|
|
1063
|
+
const request = fetch(String(_key.baseUrl).replace(/\/+$/, '') + '/chat/completions', {
|
|
949
1064
|
method: 'POST', signal: ctrl.signal,
|
|
950
1065
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + _key.key,
|
|
951
1066
|
...(Number.isFinite(Number(creditBudget)) && Number(creditBudget) > 0 ? { 'X-TS-Credit-Budget': String(Math.floor(Number(creditBudget))) } : {}) },
|
|
952
1067
|
body: JSON.stringify({ model: keyring.modeloPara(_key, model), stream: false, max_completion_tokens: maxTokens, messages: [{ role: 'system', content: system }, { role: 'user', content: user }] }),
|
|
953
1068
|
});
|
|
1069
|
+
// Idem ao fluxo do agente: o prazo deve vencer mesmo se o proxy mantiver
|
|
1070
|
+
// a conexão aberta depois do abort, permitindo pausar/retomar com estado.
|
|
1071
|
+
const deadline = new Promise((_, reject) => {
|
|
1072
|
+
timer = setTimeout(() => { ctrl.abort(); reject(new Error('META_MODEL_TIMEOUT')); }, 120000);
|
|
1073
|
+
});
|
|
1074
|
+
res = await Promise.race([request, deadline]);
|
|
954
1075
|
} finally { clearTimeout(timer); }
|
|
955
1076
|
const j = await res.json().catch(() => null);
|
|
956
1077
|
if (!res.ok) _capGuard(res, j);
|
|
1078
|
+
if (modelWasSubstituted(model, j && j.model)) {
|
|
1079
|
+
throw new ApiError(`Modelo exigido não foi respeitado: solicitado "${model}", recebido "${j.model}".`, {
|
|
1080
|
+
status: 409, code: 'model_substituted',
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
957
1083
|
const u = j?.usage || {};
|
|
958
1084
|
let credits = (j?.ts_billing && j.ts_billing.charged) || 0;
|
|
959
1085
|
if (u.prompt_tokens && !_key.billingAuthoritative) { try { const c = await api('/api/credit/charge', { method: 'POST', token, body: { model, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }); credits = (c && c.charged) || 0; } catch (_) {} }
|
|
@@ -1134,15 +1260,44 @@ const MARK_SYS = 'Você é o VERIFICADOR de uma missão. Recebe UM item do check
|
|
|
1134
1260
|
|
|
1135
1261
|
const fmtChecklist = (itens) => itens.map(it => `[${it.passes ? 'x' : it.blocked ? '!' : ' '}] (${it.id}) ${it.desc}`).join('\n');
|
|
1136
1262
|
|
|
1137
|
-
async function makeChecklist(goal, token) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
let
|
|
1144
|
-
|
|
1145
|
-
|
|
1263
|
+
async function makeChecklist(goal, token, plannerModel = 'gpt-5.6-luna') {
|
|
1264
|
+
// Plano vazio ou malformado nunca vira uma etapa com o objetivo inteiro: isso
|
|
1265
|
+
// autorizava uma execução cega. Repetimos Luna uma vez e só então usamos
|
|
1266
|
+
// DeepSeek Pro como fallback objetivo, limitado a uma tentativa.
|
|
1267
|
+
const attempts = [plannerModel, plannerModel];
|
|
1268
|
+
if (plannerModel !== 'deepseek-v4-pro') attempts.push('deepseek-v4-pro');
|
|
1269
|
+
let credits = 0, lastError = null;
|
|
1270
|
+
for (const model of attempts) {
|
|
1271
|
+
try {
|
|
1272
|
+
const r = await _llmJson({ token, model, system: CHECKLIST_SYS, user: 'OBJETIVO:\n' + goal, maxTokens: 1800 });
|
|
1273
|
+
credits += r.credits || 0;
|
|
1274
|
+
const itens = (Array.isArray(r.json?.itens) ? r.json.itens : []).slice(0, 15)
|
|
1275
|
+
.map((it, i) => ({ id: String(it.id || 'i' + (i + 1)), desc: String(it.desc || '').slice(0, 300), passes: false, attempts: 0 }))
|
|
1276
|
+
.filter(it => it.desc && it.desc.length >= 8);
|
|
1277
|
+
if (!itens.length) { lastError = new Error(`planner ${model} não definiu etapas executáveis`); continue; }
|
|
1278
|
+
// Critérios propostos pelo planner → VALIDADOS pelo compilador (formato conhecido; malformados caem fora).
|
|
1279
|
+
let criteria = [];
|
|
1280
|
+
try { criteria = require('./verify').compileCriteria(r.json?.criterios || []).criteria.slice(0, 6); } catch (_) {}
|
|
1281
|
+
return { itens, criteria, credits, plannerModel: model, fallback: model !== plannerModel };
|
|
1282
|
+
} catch (e) { lastError = e; }
|
|
1283
|
+
}
|
|
1284
|
+
const err = new ApiError('Não foi possível obter um plano válido; nenhuma etapa foi executada. Tente novamente em instantes.', {
|
|
1285
|
+
status: 502, code: 'planner_invalid',
|
|
1286
|
+
});
|
|
1287
|
+
err.credits = credits;
|
|
1288
|
+
err.cause = lastError;
|
|
1289
|
+
throw err;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Missões antigas podiam ter salvo aliases de UI como "smart" em `st.model`.
|
|
1293
|
+
// Esse estado não é uma escolha explícita do usuário e nunca pode substituir o
|
|
1294
|
+
// executor oficial de uma tarefa Meta.
|
|
1295
|
+
function officialExecutorModel(explicitModel, persistedModel) {
|
|
1296
|
+
if (explicitModel) return explicitModel;
|
|
1297
|
+
const normalized = String(persistedModel || '').trim().toLowerCase();
|
|
1298
|
+
return ['deepseek-v4-pro', 'deepseek/deepseek-v4-pro'].includes(normalized)
|
|
1299
|
+
? 'deepseek-v4-pro'
|
|
1300
|
+
: 'deepseek-v4-pro';
|
|
1146
1301
|
}
|
|
1147
1302
|
|
|
1148
1303
|
/**
|
|
@@ -1189,6 +1344,17 @@ async function run(goal, opts = {}) {
|
|
|
1189
1344
|
const onAlert = opts.onAlert || (() => {}); // avisos (escalonei / preciso de você / travei)
|
|
1190
1345
|
const startMs = Date.now();
|
|
1191
1346
|
|
|
1347
|
+
// Uma missão Meta persiste o mesmo .ts-meta.json em várias etapas. Duas
|
|
1348
|
+
// execuções concorrentes podem sobrescrever checklist, créditos e provas;
|
|
1349
|
+
// portanto cada projeto aceita somente uma rodada ativa por vez.
|
|
1350
|
+
const runLock = fileLock.acquire(stateFile(dir), { staleMs: 30 * 60 * 1000 });
|
|
1351
|
+
if (!runLock) {
|
|
1352
|
+
const current = load(dir) || {};
|
|
1353
|
+
onAlert({ type: 'busy', text: 'Outra missão Meta já está em execução neste projeto. Aguarde ela terminar antes de retomar.' });
|
|
1354
|
+
return Object.assign({}, current, { status: 'busy', busy: true });
|
|
1355
|
+
}
|
|
1356
|
+
try {
|
|
1357
|
+
|
|
1192
1358
|
let st = load(dir);
|
|
1193
1359
|
if (!st || st.status === 'done') {
|
|
1194
1360
|
if (!goal) return null; // nada pra retomar
|
|
@@ -1198,7 +1364,7 @@ async function run(goal, opts = {}) {
|
|
|
1198
1364
|
// FASE DE VIABILIDADE: avalia o POSSÍVEL e escopa honesto antes de codar app ambicioso
|
|
1199
1365
|
let feasBrief = null, feasCred = 0;
|
|
1200
1366
|
const feasMode = opts.feasibility === undefined ? 'auto' : opts.feasibility;
|
|
1201
|
-
const feasModel = thinker || '
|
|
1367
|
+
const feasModel = thinker || 'gpt-5.6-luna';
|
|
1202
1368
|
if (feasMode === true || (feasMode === 'auto' && looksComplex(goal))) {
|
|
1203
1369
|
onRound({ n: 0, item: (lang === 'en' ? `Checking feasibility with ${feasModel}…` : `Avaliando viabilidade com ${feasModel}…`), attempt: 1 });
|
|
1204
1370
|
try { const fe = await feasibilityPhase(goal, feasModel, token); feasBrief = fe.brief; feasCred = fe.credits || 0; try { fs.writeFileSync(path.join(dir, 'VIABILIDADE.md'), feasBrief); } catch (_) {} onAlert({ type: 'design', text: `🔎 ts: viabilidade avaliada (${feasModel}, ${feasCred} créditos) — o que dá e o que não dá está fixado. Salvo em VIABILIDADE.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; onAlert({ type: 'phase_error', text: `⚠ ts: viabilidade não concluída: ${String(_e && _e.message || _e).slice(0, 220)}` }); }
|
|
@@ -1216,12 +1382,12 @@ async function run(goal, opts = {}) {
|
|
|
1216
1382
|
// FASE DE ARQUITETURA (3º pilar): app complexo ganha um CONTRATO antes do checklist
|
|
1217
1383
|
let archBrief = null, archCred = 0;
|
|
1218
1384
|
const archMode = opts.arch === undefined ? 'auto' : opts.arch;
|
|
1219
|
-
const archModel = thinker || '
|
|
1385
|
+
const archModel = thinker || 'gpt-5.6-luna';
|
|
1220
1386
|
if (archMode === true || (archMode === 'auto' && looksComplex(goal))) {
|
|
1221
1387
|
onRound({ n: 0, item: (lang === 'en' ? `Designing the architecture with ${archModel}…` : `Projetando a arquitetura com ${archModel}…`), attempt: 1 });
|
|
1222
1388
|
try { const a = await archPhase(goal, archModel, token, { dir }); archBrief = a.brief; archCred = a.credits || 0; try { fs.writeFileSync(path.join(dir, 'ARQUITETURA.md'), archBrief); } catch (_) {} onAlert({ type: 'design', text: `📐 ts: contrato de arquitetura criado pelo ${archModel} (${archCred} créditos) — componentes, arquivos e assinaturas fixados. Salvo em ARQUITETURA.md.` }); } catch (_e) { if (_e && _e.code === 'no_credits') throw _e; onAlert({ type: 'phase_error', text: `⚠ ts: arquitetura não concluída: ${String(_e && _e.message || _e).slice(0, 220)}` }); }
|
|
1223
1389
|
}
|
|
1224
|
-
const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token);
|
|
1390
|
+
const mk = await makeChecklist(goal + (designBrief ? '\n\n[Há um DESIGN SYSTEM definido — o checklist deve refletir a aplicação desse visual]' : '') + (archBrief ? '\n\n[Há um CONTRATO DE ARQUITETURA definido — os itens devem seguir os componentes/arquivos dele]' : ''), token, thinker || 'gpt-5.6-luna');
|
|
1225
1391
|
// CRITÉRIOS (TaskSpec): usuário (--criterios) manda; senão, com --provar, combina o que o
|
|
1226
1392
|
// PLANNER propôs (mk.criteria, já validado) + os TESTES da stack — tudo recompilado.
|
|
1227
1393
|
// Mesmo sem --provar, critérios determinísticos derivados da stack rodam por padrão;
|
|
@@ -1232,7 +1398,10 @@ async function run(goal, opts = {}) {
|
|
|
1232
1398
|
else if (prove) _criteria = _V.compileCriteria([..._V.deriveFromStack(dir), ..._planned]).criteria.slice(0, 8);
|
|
1233
1399
|
else _criteria = _V.compileCriteria(_V.deriveFromStack(dir)).criteria.slice(0, 8);
|
|
1234
1400
|
st = { goal: String(goal).slice(0, 4000), mockup: opts.mockup || null, archBrief, feasBrief, checklist: mk.itens, rounds: [], creditsSpent: (mk.credits || 0) + designCred + archCred + feasCred,
|
|
1235
|
-
|
|
1401
|
+
// Missões locais pagas nunca devem cair no alias genérico `gateway`, que
|
|
1402
|
+
// pode apontar para Flash ou para um provedor sem ferramentas. DeepSeek
|
|
1403
|
+
// Pro é o executor oficial; uma escolha explícita do usuário ainda vence.
|
|
1404
|
+
budget, maxRounds, status: 'running', model: model || 'deepseek-v4-pro', designBrief: designBrief || null, criteria: _criteria, plannedCriteria: _planned, created_at: new Date().toISOString() };
|
|
1236
1405
|
save(st, dir);
|
|
1237
1406
|
} else {
|
|
1238
1407
|
st.status = 'running';
|
|
@@ -1246,7 +1415,10 @@ async function run(goal, opts = {}) {
|
|
|
1246
1415
|
if (opts.addBudget) st.budget += opts.addBudget;
|
|
1247
1416
|
else if (st.creditsSpent >= st.budget) st.budget = st.creditsSpent + budget; // nova janela ao retomar
|
|
1248
1417
|
}
|
|
1249
|
-
const useModel = model
|
|
1418
|
+
const useModel = officialExecutorModel(model, st.model);
|
|
1419
|
+
// Corrige o checkpoint legado para que a próxima retomada também fique no
|
|
1420
|
+
// contrato Luna (planner) + DeepSeek Pro (executor), sem depender da UI.
|
|
1421
|
+
if (!model && st.model !== useModel) { st.model = useModel; save(st, dir); }
|
|
1250
1422
|
const useThinker = thinker || st.thinker || null; // modelo caro "pensador" (escalonamento)
|
|
1251
1423
|
if (thinker && !st.thinker) { st.thinker = thinker; save(st, dir); }
|
|
1252
1424
|
onChecklist(st.checklist);
|
|
@@ -1426,7 +1598,7 @@ async function run(goal, opts = {}) {
|
|
|
1426
1598
|
}
|
|
1427
1599
|
if (!item.directArtifact && (item.attempts || 0) > 0 && expectedOnDisk && !fs.existsSync(expectedOnDisk)) {
|
|
1428
1600
|
item.directArtifact = true;
|
|
1429
|
-
item.directModel = item.directModel || '
|
|
1601
|
+
item.directModel = item.directModel || 'deepseek-v4-pro';
|
|
1430
1602
|
item.attempts = Math.max(0, item.attempts - 1);
|
|
1431
1603
|
}
|
|
1432
1604
|
item.attempts = (item.attempts || 0) + 1;
|
|
@@ -1436,7 +1608,7 @@ async function run(goal, opts = {}) {
|
|
|
1436
1608
|
// generated in validated batches from the start (classes/maps/quests/monsters).
|
|
1437
1609
|
if (!item.directArtifact && directArtifactPlan(item)) {
|
|
1438
1610
|
item.directArtifact = true;
|
|
1439
|
-
item.directModel = '
|
|
1611
|
+
item.directModel = 'deepseek-v4-pro';
|
|
1440
1612
|
}
|
|
1441
1613
|
|
|
1442
1614
|
// Some providers can generate a large document but refuse to place that
|
|
@@ -1448,7 +1620,7 @@ async function run(goal, opts = {}) {
|
|
|
1448
1620
|
save(st, dir);
|
|
1449
1621
|
}
|
|
1450
1622
|
if (item.directArtifact) {
|
|
1451
|
-
const directModel = item.directModel || '
|
|
1623
|
+
const directModel = item.directModel || 'deepseek-v4-pro';
|
|
1452
1624
|
const plan = directArtifactPlan(item);
|
|
1453
1625
|
let totalCredits = 0, finalText = '';
|
|
1454
1626
|
if (plan) {
|
|
@@ -1572,14 +1744,25 @@ async function run(goal, opts = {}) {
|
|
|
1572
1744
|
? `\nMINIMAL-CODE RULES (economy — mandatory, think like a pragmatic lazy senior):\n1) Does this really need to exist? If not, don't build it.\n2) Does it already exist in this project? If yes, REUSE it — never rewrite a second version.\n3) Does the language/platform/browser already do it natively? If yes, use the native thing (e.g. <input type="date"> instead of a datepicker lib; fetch instead of axios; CSS instead of a JS animation lib).\n4) Only then write code — the MINIMUM that solves the item. NO extra library without real need, no gratuitous abstractions/wrappers, no speculative "future-proofing".\n`
|
|
1573
1745
|
: `\nREGRAS DE CÓDIGO MÍNIMO (economia — obrigatórias; pense como um sênior pragmático e "preguiçoso"):\n1) Isso precisa mesmo existir? Se não precisa, NÃO construa.\n2) Já existe neste projeto? Se sim, REUSE — nunca escreva uma segunda versão.\n3) A linguagem/plataforma/navegador já faz isso nativo? Se sim, use o nativo (ex: <input type="date"> em vez de lib de datepicker; fetch em vez de axios; CSS em vez de lib JS de animação).\n4) Só então escreva código — o MÍNIMO que resolve o item. SEM biblioteca extra sem necessidade real, sem abstração/wrapper gratuito, sem "preparar pro futuro" especulativo.\n`);
|
|
1574
1746
|
const artifactBlock = lang === 'en'
|
|
1575
|
-
? `\nMANDATORY PROOF ARTIFACT FOR THIS ITEM: ${expectedArtifact}\nYou MUST call escrever_arquivo or editar_arquivo and persist the deliverable at exactly this relative path. A text-only answer does NOT complete the item. Create parent directories through the file tool as needed.\n`
|
|
1576
|
-
: `\nARTEFATO DE PROVA OBRIGATORIO DESTE ITEM: ${expectedArtifact}\nVoce DEVE chamar escrever_arquivo ou editar_arquivo e persistir a entrega exatamente nesse caminho relativo. Resposta apenas em texto NAO conclui o item. Crie os diretorios-pai pela ferramenta de arquivo quando necessario.\n`;
|
|
1577
|
-
const
|
|
1747
|
+
? `\nMANDATORY PROOF ARTIFACT FOR THIS ITEM: ${expectedArtifact}\nYou MUST call escrever_arquivo or editar_arquivo and persist the deliverable at exactly this relative path. A text-only answer does NOT complete the item. Create parent directories through the file tool as needed. For an existing HTML/CSS/JS file, prefer editar_arquivo with an exact anchor. Create a temporary patch script only as a last resort; validate it first (Python: python -m py_compile <file>) before executing it.\n`
|
|
1748
|
+
: `\nARTEFATO DE PROVA OBRIGATORIO DESTE ITEM: ${expectedArtifact}\nVoce DEVE chamar escrever_arquivo ou editar_arquivo e persistir a entrega exatamente nesse caminho relativo. Resposta apenas em texto NAO conclui o item. Crie os diretorios-pai pela ferramenta de arquivo quando necessario. Para HTML/CSS/JS existente, prefira editar_arquivo com ancora exata. Crie script temporario de patch somente em ultimo caso; antes de executa-lo, valide-o (Python: python -m py_compile <arquivo>). Use no maximo UM script temporario por item: se ele falhar, corrija o MESMO arquivo com editar_arquivo e valide de novo; nao crie outro script de patch.\n`;
|
|
1749
|
+
const previousItemFacts = item.lastAttempt && item.lastAttempt.result
|
|
1750
|
+
? String(item.lastAttempt.result).slice(0, 1400)
|
|
1751
|
+
: '';
|
|
1752
|
+
const recoveryExcerpt = previousItemFacts && expectedOnDisk
|
|
1753
|
+
? recoveryTargetExcerpt(expectedOnDisk, item)
|
|
1754
|
+
: '';
|
|
1755
|
+
const recoveryBlock = previousItemFacts
|
|
1756
|
+
? (lang === 'en'
|
|
1757
|
+
? `\nRECOVERY FACTS (already verified in the previous attempt; DO NOT rediscover them):\n${previousItemFacts}${recoveryExcerpt ? `\n\nTARGET SOURCE EXCERPT (deterministic local evidence; use these numbered lines as the exact edit anchor — do NOT search, reread or run commands):\n${recoveryExcerpt}` : ''}\n\nRECOVERY RULE: make the concrete edit in ${expectedArtifact} now. ${recoveryExcerpt ? 'Your first tool call MUST edit/write the target using this excerpt. Persist only the change; the harness runs objective proof after your edit.' : 'You may inspect ONLY that target once for an exact anchor. Your next tool call after that must edit/write the target, then run one objective proof.'}\n`
|
|
1758
|
+
: `\nFATOS DA TENTATIVA ANTERIOR (já verificados; NÃO os redescubra):\n${previousItemFacts}${recoveryExcerpt ? `\n\nTRECHO DO ARTEFATO-ALVO (prova local determinística; use as linhas numeradas como âncora exata da edição — NÃO pesquise, releia nem rode comandos):\n${recoveryExcerpt}` : ''}\n\nREGRA DE RECUPERAÇÃO: faça agora a edição concreta em ${expectedArtifact}. ${recoveryExcerpt ? 'Sua primeira chamada de ferramenta DEVE editar/escrever o alvo usando este trecho. Apenas persista a mudança; o harness roda a prova objetiva depois da edição.' : 'Você pode inspecionar SOMENTE esse alvo uma vez para obter a âncora exata. A próxima chamada de ferramenta deve editar/escrever o alvo e depois rodar uma prova objetiva.'}\n`)
|
|
1759
|
+
: '';
|
|
1760
|
+
const task = feasBlock + archBlock + designBlock + seedBlock + rulesBlock + minimalBlock + artifactBlock + recoveryBlock + (lang === 'en'
|
|
1578
1761
|
? `Bigger goal (context — do NOT do everything now):\n${st.goal.slice(0, 800)}\n\nCHECKLIST (state):\n${fmtChecklist(st.checklist)}\n${mapBlock}`
|
|
1579
1762
|
: `Objetivo maior (contexto — NÃO faça tudo agora):\n${st.goal.slice(0, 800)}\n\nCHECKLIST (estado):\n${fmtChecklist(st.checklist)}\n${mapBlock}`)
|
|
1580
1763
|
+ (last ? `\n${lang === 'en' ? 'Previous round' : 'Rodada anterior'}: ${String(last.result || '').slice(0, 600)}\n` : '')
|
|
1581
|
-
+ (item.attempts > 1
|
|
1582
|
-
? (lang === 'en' ? `\nPREVIOUS ATTEMPT at this item FAILED
|
|
1764
|
+
+ ((item.attempts > 1 || item.lastAttempt)
|
|
1765
|
+
? (lang === 'en' ? `\nPREVIOUS ATTEMPT at this item FAILED. RECOVERY MODE: inspect ONLY the mandatory artifact once (a focused range), then edit it directly. Do not use ler_arquivos/buscar_arquivos, do not reread the project, and do not create any patch script. Persist one concrete fix and run one objective proof.\n` : `\nA tentativa ANTERIOR deste item FALHOU. MODO DE RECUPERAÇÃO: inspecione APENAS o artefato obrigatório uma única vez (faixa focada) e depois edite-o diretamente. Não use ler_arquivos/buscar_arquivos, não releia o projeto e não crie script de patch. Persista um único ajuste concreto e rode uma prova objetiva.\n`)
|
|
1583
1766
|
: '')
|
|
1584
1767
|
+ (item.id === 'visual_fix' && st.buildError
|
|
1585
1768
|
? (lang === 'en' ? `\nAn ART DIRECTOR reviewed REAL screenshots and REJECTED the layout. Fix ONLY the points below by editing the layout/theme/drawable XML. RULES: fix the exact issue, do NOT redesign or add decorative backgrounds/blobs/ellipses behind rows (that makes it uglier); for clipped horizontal rows use a real HorizontalScrollView/RecyclerView with android:clipToPadding="false" and equal start/end padding so first AND last items show fully; keep it clean and minimal. Do NOT run the build yourself (the system re-checks and re-screenshots automatically):\n${st.buildError}\n` : `\nUm DIRETOR DE ARTE revisou PRINTS REAIS e REPROVOU o layout. Corrija APENAS os pontos abaixo editando os XML de layout/tema/drawable. REGRAS: conserte exatamente o problema, NÃO redesenhe nem adicione fundos/blobs/elipses decorativos atrás das fileiras (fica mais feio); pra fileira horizontal cortada use um HorizontalScrollView/RecyclerView de verdade com android:clipToPadding="false" e padding start/end IGUAIS pra o 1º E o último item aparecerem inteiros; mantenha limpo e minimalista. NÃO rode o build você mesmo (o sistema recompila e reprinta sozinho):\n${st.buildError}\n`)
|
|
@@ -1607,7 +1790,7 @@ async function run(goal, opts = {}) {
|
|
|
1607
1790
|
// spend three HTTP retries on it again for every checklist item. Keep the
|
|
1608
1791
|
// cooldown mission-local and short so normal routing recovers naturally.
|
|
1609
1792
|
if (!model && Number(st.autoExecutorDegradedUntil || 0) > Date.now()) {
|
|
1610
|
-
roundModel = '
|
|
1793
|
+
roundModel = 'gpt-5.6-luna';
|
|
1611
1794
|
}
|
|
1612
1795
|
// A model that answered with promises but executed zero tools is avoided
|
|
1613
1796
|
// for this item on the next window. This is a capability failure, not a
|
|
@@ -1617,7 +1800,7 @@ async function run(goal, opts = {}) {
|
|
|
1617
1800
|
// Use only model ids currently exposed by the CDC. Haiku remains in the
|
|
1618
1801
|
// intelligence catalog for pricing/history, but it is not a callable CDC
|
|
1619
1802
|
// id and must never be selected as the mission recovery executor.
|
|
1620
|
-
const candidates = ['
|
|
1803
|
+
const candidates = ['deepseek-v4-pro', 'gpt-5.6-luna'];
|
|
1621
1804
|
roundModel = candidates.find(c => !avoid.some(a => a.includes(c) || c.includes(a))) || roundModel;
|
|
1622
1805
|
}
|
|
1623
1806
|
if (visualLadder && item.id === 'visual_fix' && (st.visualFixes || 0) >= VISUAL_ESCALATE_AT && (useThinker || eye)) {
|
|
@@ -1631,11 +1814,41 @@ async function run(goal, opts = {}) {
|
|
|
1631
1814
|
// can have a minimum context larger than that, so use the remaining
|
|
1632
1815
|
// window budget while preserving the mission-wide ceiling.
|
|
1633
1816
|
const roundCreditBudget = Math.max(1, Math.min(5000, Math.floor(st.budget - st.creditsSpent)));
|
|
1634
|
-
|
|
1635
|
-
|
|
1817
|
+
let roundAllowedTools = Array.isArray(opts.allowedTools) ? opts.allowedTools : toolsForMetaItem(item, st.goal);
|
|
1818
|
+
// Com a âncora local já fornecida, a única responsabilidade do agente
|
|
1819
|
+
// nesta janela é persistir a correção. A prova é executada pelo
|
|
1820
|
+
// harness depois da edição; deixar comandos/leitura disponíveis fazia
|
|
1821
|
+
// o modelo gastar esta janela pesquisando de novo em vez de corrigir.
|
|
1822
|
+
if (recoveryExcerpt && Array.isArray(roundAllowedTools)) {
|
|
1823
|
+
const recoveryEditTools = new Set(['editar_arquivo', 'escrever_arquivo']);
|
|
1824
|
+
roundAllowedTools = roundAllowedTools.filter(name => recoveryEditTools.has(name));
|
|
1825
|
+
}
|
|
1826
|
+
// Cada item é uma janela independente. Sem este teto, uma resposta de
|
|
1827
|
+
// provedor que mantém o socket aberto deixa a missão em "running" por
|
|
1828
|
+
// muitos minutos, sem checkpoint nem retorno visual. Ao expirar, o
|
|
1829
|
+
// agente devolve uma pausa honesta e `ts meta` retoma do item salvo.
|
|
1830
|
+
const roundMaxDurationMs = Math.max(60000, Number(opts.roundMaxDurationMs) || 180000);
|
|
1831
|
+
out = recoveryExcerpt
|
|
1832
|
+
? await anchoredRecoveryEdit({ token, dir, expectedArtifact, expectedOnDisk, recoveryExcerpt, previousItemFacts, lang })
|
|
1833
|
+
: await agent.run(task, { token, lang, yes, autoAll, model: roundModel, plannerModel: st.thinker || 'gpt-5.6-luna', atomicMetaItem: true, recoveryMode: !!previousItemFacts, recoveryHasExcerpt: !!recoveryExcerpt, skipPlanner: !!recoveryExcerpt, maxCredits: roundCreditBudget, maxDurationMs: roundMaxDurationMs, allowedTools: roundAllowedTools, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, onStepDone: opts.onStepDone, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking });
|
|
1636
1834
|
}
|
|
1637
1835
|
catch (e) {
|
|
1638
1836
|
connErr = e;
|
|
1837
|
+
// A microrecuperação tem uma única responsabilidade e uma âncora
|
|
1838
|
+
// local. Se o provedor expirar ou devolver um patch inválido, isso não
|
|
1839
|
+
// pode escapar do Meta e deixar o usuário em "Pensando…" sem estado.
|
|
1840
|
+
// Persistimos a falha objetiva; a próxima rodada pode retomar sem
|
|
1841
|
+
// inventar que a edição ocorreu.
|
|
1842
|
+
if (recoveryExcerpt) {
|
|
1843
|
+
const evidence = String(e && (e.message || e.code) || 'falha desconhecida').replace(/\s+/g, ' ').slice(0, 500);
|
|
1844
|
+
out = {
|
|
1845
|
+
text: `Recuperação por âncora não foi concluída: ${evidence}. Nenhum artefato foi marcado como pronto.`,
|
|
1846
|
+
steps: 0, credits: 0, tokens: { inTok: 0, outTok: 0, cachedTok: 0 }, model: 'deepseek-v4-pro', actions: [],
|
|
1847
|
+
toolErrors: [{ tool: 'recuperacao_por_ancora', class: 'anchored_recovery_failed', retryable: /TIMEOUT|timeout|temporar|network/i.test(evidence), evidence }],
|
|
1848
|
+
completion: { ok: false, reason: 'anchored_recovery_failed' },
|
|
1849
|
+
};
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1639
1852
|
// teto de IA estourado → PAUSA limpa e resumível com CTA de upgrade (nunca segue em silêncio)
|
|
1640
1853
|
if (e && e.code === 'no_credits') {
|
|
1641
1854
|
st.status = 'paused'; st.pause_reason = 'no_credits'; save(st, dir);
|
|
@@ -1649,7 +1862,7 @@ async function run(goal, opts = {}) {
|
|
|
1649
1862
|
// retries, switch to another tool-capable model before pausing.
|
|
1650
1863
|
// Change provider family first: if Flash is down, Pro from the same
|
|
1651
1864
|
// family is likely affected too. GLM is the economical independent fallback.
|
|
1652
|
-
const fallbacks = ['
|
|
1865
|
+
const fallbacks = ['deepseek-v4-pro', 'gpt-5.6-luna'];
|
|
1653
1866
|
if (!model && att === 0 && !roundModel) {
|
|
1654
1867
|
st.autoExecutorDegradedUntil = Date.now() + (10 * 60 * 1000);
|
|
1655
1868
|
save(st, dir);
|
|
@@ -1678,6 +1891,25 @@ async function run(goal, opts = {}) {
|
|
|
1678
1891
|
}
|
|
1679
1892
|
|
|
1680
1893
|
const expectedNow = String(expectedArtifact || '').replace(/\\/g, '/').toLowerCase();
|
|
1894
|
+
// Uma mutação parcial não é entrega. O agente já produz um veredito
|
|
1895
|
+
// determinístico que considera loop, limite, ausência de ação e provas;
|
|
1896
|
+
// ignorá-lo aqui fazia o Meta marcar um item como concluído apenas porque
|
|
1897
|
+
// algum arquivo foi tocado antes de um comando/teste falhar.
|
|
1898
|
+
if (out.completion && out.completion.ok === false) {
|
|
1899
|
+
item.lastAttempt = {
|
|
1900
|
+
at: new Date().toISOString(),
|
|
1901
|
+
model: String(out.model || roundModel || 'automatico'),
|
|
1902
|
+
steps: Number(out.steps || 0),
|
|
1903
|
+
completion: out.completion.reason || 'unverified',
|
|
1904
|
+
guard: out.guard && out.guard.kind || '',
|
|
1905
|
+
toolErrors: Array.isArray(out.toolErrors) ? out.toolErrors.slice(-6) : [],
|
|
1906
|
+
result: String(out.text || '').slice(0, 1200),
|
|
1907
|
+
};
|
|
1908
|
+
st.status = 'paused'; st.pause_reason = 'execution_unverified';
|
|
1909
|
+
save(st, dir);
|
|
1910
|
+
onAlert({ type: 'verification', text: `A execução não foi comprovada (${out.completion.reason || 'sem veredito válido'}). O item ficou aberto; nenhuma alteração parcial foi tratada como entrega.` });
|
|
1911
|
+
return st;
|
|
1912
|
+
}
|
|
1681
1913
|
const expectedAfterHash = expectedOnDisk ? artifactHash(expectedOnDisk) : null;
|
|
1682
1914
|
const expectedChangedNow = !!expectedAfterHash && expectedAfterHash !== expectedBeforeHash;
|
|
1683
1915
|
const expectedChangedSinceBaseline = !!expectedAfterHash && !!item.artifactBaselineHash
|
|
@@ -1691,19 +1923,40 @@ async function run(goal, opts = {}) {
|
|
|
1691
1923
|
// several tool steps exploring the project and still fail to create the
|
|
1692
1924
|
// promised deliverable; treat that exactly like a zero-tool promise.
|
|
1693
1925
|
if (expectedNow && !wroteExpectedNow) {
|
|
1694
|
-
|
|
1695
|
-
|
|
1926
|
+
// CAPABILITY_DENIED é bloqueio do harness, não incapacidade do modelo.
|
|
1927
|
+
// Já ignorar o gate explícito de execução, por outro lado, é uma prova
|
|
1928
|
+
// objetiva de que este executor não avançou: a próxima rodada pode usar
|
|
1929
|
+
// o fallback do plano, sem mascarar a falha como "problema do harness".
|
|
1930
|
+
const ignoredExecutionGate = Array.isArray(out.toolErrors)
|
|
1931
|
+
&& out.toolErrors.some(e => e && e.class === 'inspection_phase_ignored');
|
|
1932
|
+
const harnessBlocked = /CAPABILITY_DENIED/i.test(JSON.stringify(out || {}))
|
|
1933
|
+
|| (!ignoredExecutionGate && /inspection_phase_ignored/i.test(JSON.stringify(out || {})));
|
|
1934
|
+
if (!harnessBlocked) item.avoidModels = [...new Set([...(item.avoidModels || []), String(out.model || roundModel || 'automatico')])];
|
|
1696
1935
|
const expectedTarget = path.isAbsolute(expectedArtifact) ? expectedArtifact : path.resolve(dir, expectedArtifact);
|
|
1697
1936
|
// Conteúdo direto é seguro para um artefato NOVO. Para código existente ele poderia
|
|
1698
1937
|
// substituir o projeto inteiro por uma resposta parcial do item; nesse caso troque o
|
|
1699
1938
|
// executor e obrigue uma edição real, preservando o arquivo atual.
|
|
1700
1939
|
item.directArtifact = !fs.existsSync(expectedTarget);
|
|
1701
|
-
|
|
1702
|
-
|
|
1940
|
+
// Conteúdo direto é uma recuperação controlada: mantenha o executor
|
|
1941
|
+
// oficial, nunca transforme um alias/Flash/GLM em política automática.
|
|
1942
|
+
if (item.directArtifact) item.directModel = 'deepseek-v4-pro';
|
|
1943
|
+
// Diagnóstico resumido e persistente: o próximo operador (ou o próprio
|
|
1944
|
+
// harness) consegue saber se faltou ferramenta, prova, tempo ou resposta
|
|
1945
|
+
// do modelo sem depender do console que já fechou.
|
|
1946
|
+
item.lastAttempt = {
|
|
1947
|
+
at: new Date().toISOString(),
|
|
1948
|
+
model: String(out.model || roundModel || 'automatico'),
|
|
1949
|
+
steps: Number(out.steps || 0),
|
|
1950
|
+
completion: out.completion && out.completion.reason || '',
|
|
1951
|
+
guard: out.guard && out.guard.kind || '',
|
|
1952
|
+
toolErrors: Array.isArray(out.toolErrors) ? out.toolErrors.slice(-4) : [],
|
|
1953
|
+
result: String(out.text || '').slice(0, 1200),
|
|
1954
|
+
};
|
|
1703
1955
|
st.status = 'paused'; st.pause_reason = 'model_no_action';
|
|
1704
|
-
st.budget = st.creditsSpent;
|
|
1705
1956
|
save(st, dir);
|
|
1706
|
-
onAlert({ type: 'retry', text:
|
|
1957
|
+
onAlert({ type: 'retry', text: harnessBlocked
|
|
1958
|
+
? `O harness bloqueou uma operação antes de o modelo persistir o artefato. Não marquei o item como concluído nem culpei o executor; a retomada repetirá o papel oficial após a correção local.`
|
|
1959
|
+
: `O modelo ${out.model || roundModel || 'automatico'} não persistiu o artefato esperado. Não marquei o item como concluído; a retomada usará outro executor sem substituir código existente.` });
|
|
1707
1960
|
return st;
|
|
1708
1961
|
}
|
|
1709
1962
|
|
|
@@ -1774,7 +2027,7 @@ async function run(goal, opts = {}) {
|
|
|
1774
2027
|
// narrativa de uma rodada não é evidência suficiente para concluir itens não executados.
|
|
1775
2028
|
try {
|
|
1776
2029
|
const evid = written.length ? '\n\nARQUIVOS REALMENTE GRAVADOS nesta rodada (evidência objetiva):\n' + written.map(_basename).join(', ') : '';
|
|
1777
|
-
const mk = await _llmJson({ token, system: MARK_SYS,
|
|
2030
|
+
const mk = await _llmJson({ token, model: 'gpt-5.6-luna', system: MARK_SYS,
|
|
1778
2031
|
user: `OBJETIVO:\n${st.goal.slice(0, 600)}\n\nCHECKLIST (contexto):\n${fmtChecklist(st.checklist)}\n\nITEM ALVO: (${item.id}) ${item.desc}\n\nRESULTADO DA RODADA:\n${String(out.text || '').slice(0, 2000)}${evid}` });
|
|
1779
2032
|
st.creditsSpent += mk.credits || 0;
|
|
1780
2033
|
if (mk.json && mk.json.passou === true && wroteExpectedArtifact) marks.push(item.id);
|
|
@@ -1788,6 +2041,9 @@ async function run(goal, opts = {}) {
|
|
|
1788
2041
|
save(st, dir);
|
|
1789
2042
|
onRoundDone({ marks, credits: out.credits || 0, checklist: st.checklist, spent: st.creditsSpent });
|
|
1790
2043
|
}
|
|
2044
|
+
} finally {
|
|
2045
|
+
runLock.release();
|
|
2046
|
+
}
|
|
1791
2047
|
}
|
|
1792
2048
|
|
|
1793
2049
|
// Notificação no Telegram do dono (best-effort — sem canal, segue em silêncio)
|
|
@@ -1839,4 +2095,4 @@ function trailSummary(st, lang) {
|
|
|
1839
2095
|
};
|
|
1840
2096
|
}
|
|
1841
2097
|
|
|
1842
|
-
module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, toolsForMetaItem, artifactForMetaItem, normalizeMetaArtifact, persistMetaArtifact, directArtifactPlan, canGenerateDirectArtifact, verificationLabel, _checkCriteria, trailSummary };
|
|
2098
|
+
module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, toolsForMetaItem, artifactForMetaItem, recoveryTargetExcerpt, anchoredRecoveryEdit, normalizeMetaArtifact, persistMetaArtifact, directArtifactPlan, canGenerateDirectArtifact, verificationLabel, _checkCriteria, trailSummary, officialExecutorModel, modelWasSubstituted };
|
package/lib/tools.js
CHANGED
|
@@ -39,6 +39,18 @@ function _snapshot(absPath) {
|
|
|
39
39
|
// pra compatibilidade (tools.isDestructive segue funcionando pra quem já importava).
|
|
40
40
|
const { isDestructive } = require('./core');
|
|
41
41
|
|
|
42
|
+
// `findstr`, `rg` e `grep` usam o código 1 para informar que a busca foi
|
|
43
|
+
// executada corretamente, mas não encontrou ocorrências. Para o harness isso
|
|
44
|
+
// é evidência de inspeção (por exemplo: confirmar que um texto antigo já não
|
|
45
|
+
// existe), e não uma falha operacional que justifique repetir a mesma ação.
|
|
46
|
+
function _isReadOnlySearchNoMatch(command, code, stdout, stderr) {
|
|
47
|
+
if (Number(code) !== 1 || String(stdout || '').trim() || String(stderr || '').trim()) return false;
|
|
48
|
+
const executable = String(command || '')
|
|
49
|
+
.trim()
|
|
50
|
+
.replace(/^\s*cd\s+(?:\/d\s+)?(?:"[^"]+"|'[^']+'|[^&|]+)\s*(?:&&|&)\s*/i, '');
|
|
51
|
+
return /^(?:findstr|rg|grep)\b/i.test(executable);
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
// Conteúdo de e-mail/site/arquivo pode tentar convencer o modelo a mandar
|
|
43
55
|
// segredos para fora. O prompt ajuda, mas a defesa precisa existir também no
|
|
44
56
|
// executor: se um comando combina transporte de rede com fonte sensível, ele
|
|
@@ -523,6 +535,15 @@ function _commandScopeViolation(cmd, confineDir) {
|
|
|
523
535
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(m[1])) candidates.push(m[1]);
|
|
524
536
|
}
|
|
525
537
|
for (const raw of candidates) {
|
|
538
|
+
// No CMD do Windows, `cd /d C:\\projeto` usa /d como um modificador
|
|
539
|
+
// (troca também a unidade atual), não como caminho Unix absoluto. Sem
|
|
540
|
+
// esta exceção o confinamento recusava uma mudança de pasta totalmente
|
|
541
|
+
// interna antes de o comando chegar ao shell.
|
|
542
|
+
// Opções do CMD também podem ter dois-pontos, por exemplo
|
|
543
|
+
// `findstr /c:"texto"`. Não são caminhos Unix nem tentativas de escapar da
|
|
544
|
+
// raiz. Sem esta exceção uma busca contendo `</script>` era lida como
|
|
545
|
+
// redirecionamento e o `/c:` seguinte virava um falso caminho externo.
|
|
546
|
+
if (process.platform === 'win32' && /^\/[a-z](?::|$)/i.test(raw)) continue;
|
|
526
547
|
const abs = path.resolve(root, raw);
|
|
527
548
|
if (!_scopeContains(root, abs)) {
|
|
528
549
|
return `CAPABILITY_DENIED: comando mutante referencia "${raw}", fora da raiz autorizada "${root}".`;
|
|
@@ -653,6 +674,11 @@ async function execute(name, input, opts = {}) {
|
|
|
653
674
|
}
|
|
654
675
|
const _outsideScope = (p) => {
|
|
655
676
|
if (!opts.confineDir) return false;
|
|
677
|
+
// Uma skill só pode ser lida fora do projeto se o chamador a tiver
|
|
678
|
+
// explicitamente anunciado nesta rodada. Isso permite seguir uma skill
|
|
679
|
+
// instalada sem transformar ~/.ts, ~/.codex etc. em acesso geral.
|
|
680
|
+
if (name === 'ler_arquivo' && Array.isArray(opts.trustedReadPaths)
|
|
681
|
+
&& opts.trustedReadPaths.some(allowed => path.resolve(String(allowed)) === path.resolve(String(p)))) return false;
|
|
656
682
|
return !_scopeContains(opts.confineDir, p);
|
|
657
683
|
};
|
|
658
684
|
const _scopeError = (p) => ({ erro: `CAPABILITY_DENIED: "${path.resolve(p)}" está fora da raiz autorizada "${path.resolve(opts.confineDir)}".` });
|
|
@@ -685,6 +711,11 @@ async function execute(name, input, opts = {}) {
|
|
|
685
711
|
// B18: mascara chave/token ANTES de virar contexto do modelo e linha de log.
|
|
686
712
|
const _rd = require('./core').redactSecrets;
|
|
687
713
|
const r = { stdout: _rd(c.out), stderr: _rd(compactor.stripAnsi(err || '').slice(0, 1500)), codigo: e?.code ?? 0 };
|
|
714
|
+
if (_isReadOnlySearchNoMatch(comandoExecutado, r.codigo, r.stdout, r.stderr)) {
|
|
715
|
+
r.codigo = 0;
|
|
716
|
+
r.no_match = true;
|
|
717
|
+
r.stdout = 'Nenhuma ocorrência encontrada.';
|
|
718
|
+
}
|
|
688
719
|
if (c.note) r.aviso = c.note;
|
|
689
720
|
if (normalized) r.normalizado = normalized.note;
|
|
690
721
|
if (!r.stdout.trim() && !r.stderr.trim() && r.codigo === 0 && /python3? -c/.test(comando) && comando.includes('\n'))
|
|
@@ -1165,4 +1196,4 @@ async function execute(name, input, opts = {}) {
|
|
|
1165
1196
|
}
|
|
1166
1197
|
|
|
1167
1198
|
module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, secretExfiltrationReason, buildProjectMap,
|
|
1168
|
-
_test: { shellGotcha: _shellGotcha, normalizeWindowsReadOnlyCommand: _normalizeWindowsReadOnlyCommand } };
|
|
1199
|
+
_test: { shellGotcha: _shellGotcha, normalizeWindowsReadOnlyCommand: _normalizeWindowsReadOnlyCommand, commandScopeViolation: _commandScopeViolation, isReadOnlySearchNoMatch: _isReadOnlySearchNoMatch } };
|
package/lib/verify.js
CHANGED
|
@@ -17,8 +17,21 @@ function vCommand(c, cwd) {
|
|
|
17
17
|
const want = c.exit != null ? Number(c.exit) : 0;
|
|
18
18
|
try {
|
|
19
19
|
const out = execSync(c.cmd, { cwd, encoding: 'utf8', timeout: (c.timeout_s || 120) * 1000, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
20
|
+
const output = String(out || '');
|
|
21
|
+
// Um script como `echo "No tests yet" && exit 0` não é uma prova de saúde.
|
|
22
|
+
// Para critérios que afirmam rodar testes, trate explicitamente essa saída
|
|
23
|
+
// como falha, mesmo que o processo tenha saído com código zero.
|
|
24
|
+
const isTestCommand = /(^|\s)(npm\s+test|pnpm\s+test|yarn\s+test|node\s+.*test)/i.test(String(c.cmd || ''));
|
|
25
|
+
const noTests = /\b(no tests? yet|no tests? (?:found|run)|0 tests? (?:found|run))\b/i.test(output);
|
|
26
|
+
if (want === 0 && isTestCommand && noTests) {
|
|
27
|
+
return { ok: false, detail: 'teste sem casos executados (falso positivo)', errorClass: 'verification_failed', evidence: output.slice(-200).replace(/\s+/g, ' ') };
|
|
28
|
+
}
|
|
29
|
+
const reportedFailure = /(^|\n)\s*(?:✗|FAIL(?:\s|:|$)|AssertionError\b)/im.test(output);
|
|
30
|
+
if (want === 0 && isTestCommand && reportedFailure) {
|
|
31
|
+
return { ok: false, detail: 'teste reportou falha apesar de exit 0 (falso positivo)', errorClass: 'verification_failed', evidence: output.slice(-300).replace(/\s+/g, ' ') };
|
|
32
|
+
}
|
|
20
33
|
const ok = want === 0;
|
|
21
|
-
return { ok, detail: ok ? 'exit 0' : `exit 0 mas esperava ${want}`, evidence:
|
|
34
|
+
return { ok, detail: ok ? 'exit 0' : `exit 0 mas esperava ${want}`, evidence: output.slice(-200).replace(/\s+/g, ' ') };
|
|
22
35
|
} catch (e) {
|
|
23
36
|
const code = typeof e.status === 'number' ? e.status : 1;
|
|
24
37
|
const ok = code === want;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-smart-cli",
|
|
3
|
-
"version": "0.97.
|
|
3
|
+
"version": "0.97.41",
|
|
4
4
|
"description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ts": "bin/ts.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "node test/core.test.js && node test/windows-shell-normalization.test.js && node test/gateways.test.js && node test/agent-recovery-guard.test.js && node test/agent-plan-model-contract.test.js && node test/agent-mission-limits.test.js && node test/agent-external-approval.test.js && node test/agent-prompt-injection.test.js && node test/file-concurrency.test.js && node test/mission-observability.test.js && node test/audit-packs.test.js && node test/intelligence-core.test.js && node test/cloud-slug.test.js && node test/eval-model.test.js && node test/project-cache.test.js && node test/memory-bus.test.js && node test/capabilities.test.js && node test/mcp-e2e.test.js && node test/erros.test.js && node test/evolution-telemetry.test.js && node test/owner-audit.test.js && node test/capability-pack.test.js && node test/video-generation.test.js && node test/image-job.test.js && node test/byok.test.js && node test/conhecimento.test.js && node test/policy.test.js && node test/temas.test.js && node test/skill-index.test.js && node test/doctor.test.js && node test/google-workspace-tools.test.js && node test/office-editors.test.js"
|
|
9
|
+
"test": "node test/core.test.js && node test/conversation-scope.test.js && node test/windows-shell-normalization.test.js && node test/gateways.test.js && node test/agent-recovery-guard.test.js && node test/agent-plan-model-contract.test.js && node test/agent-mission-limits.test.js && node test/agent-external-approval.test.js && node test/agent-prompt-injection.test.js && node test/file-concurrency.test.js && node test/mission-observability.test.js && node test/audit-packs.test.js && node test/intelligence-core.test.js && node test/cloud-slug.test.js && node test/eval-model.test.js && node test/project-cache.test.js && node test/memory-bus.test.js && node test/capabilities.test.js && node test/mcp-e2e.test.js && node test/erros.test.js && node test/evolution-telemetry.test.js && node test/owner-audit.test.js && node test/capability-pack.test.js && node test/video-generation.test.js && node test/image-job.test.js && node test/byok.test.js && node test/conhecimento.test.js && node test/policy.test.js && node test/temas.test.js && node test/skill-index.test.js && node test/doctor.test.js && node test/google-workspace-tools.test.js && node test/office-editors.test.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|