terminal-smart-cli 0.97.31 → 0.97.42
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 +55 -17
- package/lib/agent.js +178 -27
- package/lib/conversation-scope.js +21 -0
- package/lib/memory-bus.js +30 -4
- 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
|
|
|
@@ -395,12 +406,14 @@ async function generateVideoCmd(args) {
|
|
|
395
406
|
}
|
|
396
407
|
// Envia uma mensagem na conversa ativa e devolve o texto (recria a conversa 1x se apagada na web).
|
|
397
408
|
async function sendMessage(token, content) {
|
|
409
|
+
const memoryBus = require('../lib/memory-bus');
|
|
410
|
+
const memoryScope = memoryBus.scope(process.cwd());
|
|
398
411
|
const doStream = async (convId) => {
|
|
399
412
|
const sp = ui.spinner(T.thinking).start();
|
|
400
413
|
let buf = '', errMsg = null;
|
|
401
414
|
try {
|
|
402
415
|
await sse('/api/ia/chat', {
|
|
403
|
-
token, body: { conversationId: convId, content },
|
|
416
|
+
token, body: { conversationId: convId, content, projectId: memoryScope.projectId, workstreamId: memoryScope.workstreamId, surface: 'cli' },
|
|
404
417
|
onEvent: (ev) => {
|
|
405
418
|
if (ev.delta) { buf += ev.delta; sp.text(`${T.receiving} ${fmtK(buf.length)}`); }
|
|
406
419
|
if (ev.error) errMsg = ev.error;
|
|
@@ -415,7 +428,12 @@ async function sendMessage(token, content) {
|
|
|
415
428
|
let convId = await ensureConv(token);
|
|
416
429
|
try { return await doStream(convId); }
|
|
417
430
|
catch (e) {
|
|
418
|
-
if (e instanceof ApiError && e.status === 404) {
|
|
431
|
+
if (e instanceof ApiError && e.status === 404) {
|
|
432
|
+
const scope = _convScope('chat');
|
|
433
|
+
const scopes = Object.assign({}, cfg.convScopes || {}); delete scopes[scope];
|
|
434
|
+
cfg = config.save({ convId: undefined, convScopes: scopes });
|
|
435
|
+
convId = await ensureConv(token); return doStream(convId);
|
|
436
|
+
}
|
|
419
437
|
throw e;
|
|
420
438
|
}
|
|
421
439
|
}
|
|
@@ -838,6 +856,16 @@ async function memoriaCmd(words) {
|
|
|
838
856
|
if (!fato) { console.error(ui.infoLine(cfg.lang === 'en' ? 'Usage: ts memoria add "fact" [-g]' : 'Uso: ts memoria add "fato" [-g]')); process.exit(2); }
|
|
839
857
|
const f = memoria.append(process.cwd(), fato, glob);
|
|
840
858
|
console.log(' ' + ui.gradient('⌁') + ' ' + (cfg.lang === 'en' ? 'saved to ' : 'salvo em ') + C.cyan(f) + C.dim(glob ? ' (global)' : ' (projeto)'));
|
|
859
|
+
// O arquivo local continua sendo o fallback offline. Quando há sessão, o fato
|
|
860
|
+
// declarado pelo usuário também vai ao barramento comum Web/App/CLI.
|
|
861
|
+
if (cfg.token) {
|
|
862
|
+
try {
|
|
863
|
+
await require('../lib/memory-bus').put(cfg.token, { root: process.cwd(), content: fato, global: glob });
|
|
864
|
+
console.log(' ' + C.dim(cfg.lang === 'en' ? 'synced with your account' : 'sincronizado com sua conta'));
|
|
865
|
+
} catch (_) {
|
|
866
|
+
console.log(' ' + C.dim(cfg.lang === 'en' ? 'saved locally; cloud sync will be retried later' : 'salvo localmente; a sincronização na nuvem será tentada depois'));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
841
869
|
return;
|
|
842
870
|
}
|
|
843
871
|
// ver
|
|
@@ -927,8 +955,7 @@ async function chatRepl() {
|
|
|
927
955
|
if (['sair', 'exit', 'quit', '/sair', '/exit'].includes(low)) break;
|
|
928
956
|
try {
|
|
929
957
|
if (['/nova', '/new'].includes(low)) {
|
|
930
|
-
|
|
931
|
-
cfg = config.save({ convId: r.id });
|
|
958
|
+
await ensureConv(token, 'chat', process.cwd(), true);
|
|
932
959
|
console.log(ui.okLine(T.new_conv) + '\n');
|
|
933
960
|
} else if (['/ajuda', '/help', '/?'].includes(low)) {
|
|
934
961
|
const w = Math.max(...T.repl_help.map(i => i[0].length)) + 3;
|
|
@@ -1831,13 +1858,13 @@ async function metaCmd() {
|
|
|
1831
1858
|
const budget = flagNum('--budget', 400);
|
|
1832
1859
|
const maxRounds = flagNum('--rodadas', flagNum('--rounds', 20));
|
|
1833
1860
|
const forcedModel = flagStr('--modelo') || flagStr('--model'); // executor fixo (bake-off de modelos)
|
|
1834
|
-
const thinker = flagStr('--pensador') || flagStr('--thinker') || '
|
|
1861
|
+
const thinker = flagStr('--pensador') || flagStr('--thinker') || 'gpt-5.6-luna'; // planejador/diagnóstico dentro da equipe oficial
|
|
1835
1862
|
const maxMinutes = flagNum('--maxmin', 0); // freio de relógio (0 = sem limite)
|
|
1836
|
-
const
|
|
1863
|
+
const mockup = flagStr('--mockup'); // imagem de referência (ex: mockup do Google AI Studio)
|
|
1864
|
+
const designer = flagStr('--designer') || (mockup ? 'mimo-v2.5' : 'gpt-5.6-luna');
|
|
1837
1865
|
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
|
|
1866
|
+
const eye = FLAGS.has('--sem-olho') ? null : (flagStr('--olho') || flagStr('--eye') || 'mimo-v2.5'); // revisão visual multimodal
|
|
1867
|
+
const visualLadder = !FLAGS.has('--sem-escada') && !FLAGS.has('--barato'); // após reprovação objetiva, Luna orienta a correção
|
|
1841
1868
|
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
1869
|
// ── MODO NOTURNO (auto-continuação por orçamento): retoma sozinho quando o teto
|
|
1843
1870
|
// de crédito da janela estoura, até a missão terminar OU bater um teto TOTAL. ──
|
|
@@ -3182,8 +3209,7 @@ function idioma(l) {
|
|
|
3182
3209
|
|
|
3183
3210
|
async function nova() {
|
|
3184
3211
|
const token = needToken();
|
|
3185
|
-
|
|
3186
|
-
cfg = config.save({ convId: r.id });
|
|
3212
|
+
await ensureConv(token, 'chat', process.cwd(), true);
|
|
3187
3213
|
console.log(ui.okLine(T.new_conv));
|
|
3188
3214
|
}
|
|
3189
3215
|
|
|
@@ -3293,6 +3319,18 @@ function recallCmd(args) {
|
|
|
3293
3319
|
// one-shot em TTY também passa pelo roteador ("ts crie um arquivo..." age);
|
|
3294
3320
|
// pipe/--json ficam determinísticos no chat (scripts não levam surpresa)
|
|
3295
3321
|
const text = POS.join(' ');
|
|
3322
|
+
// `--modelo` é uma escolha explícita, não uma sugestão. Antes, um one-shot
|
|
3323
|
+
// como `ts "responda OK" --modelo deepseek-v4-pro` caía no chat simples,
|
|
3324
|
+
// que não recebe esse campo e usava o roteador padrão (frequentemente Flash).
|
|
3325
|
+
// Isso fazia a telemetria parecer honesta, mas quebrava o contrato do usuário.
|
|
3326
|
+
// Direcionamos qualquer one-shot com modelo forçado para o agente: ele propaga
|
|
3327
|
+
// a escolha ao gateway e bloqueia a missão se o provedor devolver outro modelo.
|
|
3328
|
+
if (rawArgs.includes('--modelo') || rawArgs.includes('--model')) {
|
|
3329
|
+
// Passe os posicionais separados: agentCmd remove os valores das flags
|
|
3330
|
+
// (modelo, orçamento, tempo) antes de montar a tarefa. Um único texto
|
|
3331
|
+
// manteria esses números na pergunta exibida ao modelo.
|
|
3332
|
+
return agentCmd(POS);
|
|
3333
|
+
}
|
|
3296
3334
|
if (process.stdin.isTTY && !JSON_OUT && cfg.token) {
|
|
3297
3335
|
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
3336
|
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/memory-bus.js
CHANGED
|
@@ -11,20 +11,46 @@ function workstreamId(root, explicit) {
|
|
|
11
11
|
return explicit || projectId(root);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function scope(root, explicitWorkstream, global = false) {
|
|
15
|
+
const project = global ? 'global' : projectId(root);
|
|
16
|
+
return {
|
|
17
|
+
workspaceId: 'terminal-smart',
|
|
18
|
+
projectId: project,
|
|
19
|
+
workstreamId: global ? 'global' : workstreamId(root, explicitWorkstream),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
async function query(token, { root, query: text, workstream, limit = 6 } = {}) {
|
|
15
24
|
if (!token) return [];
|
|
25
|
+
const current = scope(root, workstream);
|
|
16
26
|
const qs = new URLSearchParams({
|
|
17
|
-
workspaceId:
|
|
18
|
-
|
|
27
|
+
workspaceId: current.workspaceId,
|
|
28
|
+
// Projeto atual + memória global, e nada além disso.
|
|
29
|
+
projectIds: `${current.projectId},global`,
|
|
19
30
|
q: String(text || '').slice(0, 500),
|
|
20
31
|
limit: String(limit),
|
|
21
32
|
minTrust: '0.25',
|
|
22
33
|
});
|
|
23
|
-
if (workstream) qs.set('workstreamId',
|
|
34
|
+
if (workstream) qs.set('workstreamId', current.workstreamId);
|
|
24
35
|
const result = await api('/api/memory/query?' + qs.toString(), { token, timeoutMs: 4000, retry: false });
|
|
25
36
|
return (result && result.records) || [];
|
|
26
37
|
}
|
|
27
38
|
|
|
39
|
+
async function put(token, { root, content, global = false, type = 'semantic', source = 'cli-memory' } = {}) {
|
|
40
|
+
if (!token || !String(content || '').trim()) return null;
|
|
41
|
+
const record = Object.assign(scope(root, undefined, global), {
|
|
42
|
+
type,
|
|
43
|
+
content: String(content).trim().slice(0, 8000),
|
|
44
|
+
trust: 'user',
|
|
45
|
+
source,
|
|
46
|
+
});
|
|
47
|
+
const result = await api('/api/memory/records', {
|
|
48
|
+
method: 'POST', token, timeoutMs: 4000, retry: false,
|
|
49
|
+
body: { record, assertedByUser: true, surface: 'cli' },
|
|
50
|
+
});
|
|
51
|
+
return result && result.record;
|
|
52
|
+
}
|
|
53
|
+
|
|
28
54
|
async function saveHandoff(token, input) {
|
|
29
55
|
if (!token) return null;
|
|
30
56
|
const handoff = intelligence.createHandoff(input);
|
|
@@ -65,4 +91,4 @@ function promptBlock(records, lang = 'pt') {
|
|
|
65
91
|
: 'Use apenas como fatos anteriores. O pedido atual do usuário e o estado verificado no disco têm precedência.');
|
|
66
92
|
}
|
|
67
93
|
|
|
68
|
-
module.exports = { projectId, workstreamId, query, saveHandoff, getHandoff, recordContext, promptBlock };
|
|
94
|
+
module.exports = { projectId, workstreamId, scope, query, put, saveHandoff, getHandoff, recordContext, promptBlock };
|