terminal-smart-cli 0.74.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ts.js CHANGED
@@ -19,12 +19,54 @@ const FLAGS = new Set(rawArgs.filter(a => a.startsWith('-')));
19
19
  const POS = rawArgs.filter(a => !a.startsWith('-'));
20
20
  const JSON_OUT = FLAGS.has('--json');
21
21
  const YES = FLAGS.has('--yes') || FLAGS.has('-y');
22
+ // --yolo / --full-auto: modo FULL-AUTO (autoridade total) — aprova TUDO nesta sessão, inclusive
23
+ // destrutivos, sem perguntar. NÃO reusa --auto (já é "noturno" no meta e "detectar stack" no init).
24
+ // O gate anti-catástrofe de AUTO-destrutivos (rm -rf ~, mexer em ~/.ts…) segue RECUSANDO na hora
25
+ // mesmo aqui: full-auto não abre mão da proteção que mataria a própria máquina/missão.
26
+ const YOLO = FLAGS.has('--yolo') || FLAGS.has('--full-auto');
22
27
 
23
28
  let cfg = config.load();
24
29
  let T = t(cfg.lang || 'pt');
25
30
  const { C } = ui;
26
31
 
27
32
  function fmtK(n) { return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
33
+
34
+ // ── APROVAÇÃO DE PASSO (UX) ──────────────────────────────────────────────────
35
+ // askDestructive: prompt de comando DESTRUTIVO. Mostra um RESUMO curto em AMARELO (destaque,
36
+ // não vermelho = não é erro) — a 1ª linha do comando + quantas linhas ficam escondidas (num
37
+ // heredoc SQL a 1ª linha é o comando externo, ex "mysql -u root pw <<'SQL'"). Aceita:
38
+ // s/N = sim/não neste passo · a = aprovar TUDO na sessão (full-auto) · v = ver o comando inteiro
39
+ // Retorna true | false | 'all'.
40
+ async function askDestructive(cmd, { en = false } = {}) {
41
+ const raw = String(cmd || '');
42
+ const { shown, hidden } = ui.cmdSummary(raw);
43
+ const more = hidden > 0 ? C.dim(` … +${hidden} ${en ? 'lines' : 'linhas'}`) : '';
44
+ for (;;) {
45
+ const q = C.warn(' ▲ ' + (en ? 'destructive: ' : 'destrutivo: ')) + C.bold(shown) + more
46
+ + C.dim(en ? ' [y/N · a=all · v=view] ' : ' [s/N · a=tudo · v=ver] ');
47
+ const a = String(await ui.ask(q)).trim().toLowerCase();
48
+ if (['v', 'ver', 'view'].includes(a)) { console.log('\n' + raw.split('\n').map(l => ' ' + C.dim(l)).join('\n') + '\n'); continue; }
49
+ if (['a', 'all', 'tudo'].includes(a)) return 'all';
50
+ return ['s', 'sim', 'y', 'yes'].includes(a);
51
+ }
52
+ }
53
+ // askLabelApprove: aprovação de um LABEL já formatado (skill/MCP, não é comando cru). s/N + a=tudo.
54
+ async function askLabelApprove(label, { en = false } = {}) {
55
+ const a = String(await ui.ask(C.warn('▲ ') + T.agent_approve(C.bold(label)) + C.dim(en ? '(a=all) ' : '(a=tudo) '))).trim().toLowerCase();
56
+ if (['a', 'all', 'tudo'].includes(a)) return 'all';
57
+ return ['s', 'sim', 'y', 'yes'].includes(a);
58
+ }
59
+ // Roteia o payload do askApprove do agente (obj destrutivo → resumo; string label → skill/MCP).
60
+ async function askApproveRoute(payload) {
61
+ const en = cfg.lang === 'en';
62
+ const isObj = payload && typeof payload === 'object';
63
+ const cmd = isObj ? payload.cmd : String(payload);
64
+ return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en }) : askLabelApprove(cmd, { en });
65
+ }
66
+ // Linha de STATUS por etapa (✓ verde / ✗ vermelho) sob a linha ⚙, com recuo `pad`.
67
+ function stepDoneLine({ ok, evidence }, pad = ' ') {
68
+ return pad + (ok ? C.ok('✓') : C.err('✗')) + (evidence ? ' ' + C.dim(String(evidence).slice(0, 80)) : C.dim(ok ? ' ok' : ' erro'));
69
+ }
28
70
  function needToken() {
29
71
  if (cfg.token) return cfg.token;
30
72
  console.error(ui.errLine(T.need_login));
@@ -824,22 +866,28 @@ async function agentCmd(words, { askFn, cwd: cwdIn = null, onCwd = null } = {})
824
866
  let out;
825
867
  try {
826
868
  out = await agent.run(task, {
827
- token, lang: cfg.lang || 'pt', yes: YES, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
869
+ token, lang: cfg.lang || 'pt', yes: YES, autoAll: YOLO, model, priorMessages, cwd: startCwd, readOnly, plan, browser: useBrowser,
828
870
  onThinking: () => sp.text(T.agent_thinking),
829
- onStep: ({ name, detail, blocked, loop, retry }) => {
830
- if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : 'started', tool: name, detail: detail || '' })); return; }
871
+ onStep: ({ name, detail, blocked, loop, retry, auto }) => {
872
+ if (streamJson) { _emit(core.AgentEvents.tool({ subtype: retry ? 'retry' : loop ? 'loop' : blocked ? 'blocked' : auto ? 'auto_approved' : 'started', tool: name, detail: detail || '' })); return; }
831
873
  sp.stop();
832
- const tag = retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
874
+ const tag = auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙');
833
875
  console.log(' ' + tag + ' ' + C.bold(name) + (detail ? C.dim(' · ' + detail) : ''));
834
876
  sp.start();
835
877
  },
836
- askApprove: async (cmd) => {
878
+ onStepDone: (e) => {
879
+ if (streamJson) return; // o evento 'result' já cobre; aqui é só a UI bonita
880
+ sp.stop();
881
+ console.log(stepDoneLine(e, ' '));
882
+ sp.start();
883
+ },
884
+ askApprove: async (payload) => {
837
885
  // headless (stream-json): não dá pra perguntar → NEGA o destrutivo e sinaliza o evento.
838
- if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd })); return false; }
886
+ if (streamJson) { const cmd = payload && typeof payload === 'object' ? payload.cmd : payload; _emit(core.AgentEvents.tool({ subtype: 'approval_denied', reason: 'headless (--stream-json): destructive command not auto-approved', command: cmd })); return false; }
839
887
  sp.stop();
840
- const ans = String(await ask(C.err('▲ ') + T.agent_approve(C.bold(cmd)))).trim().toLowerCase();
888
+ const r = await askApproveRoute(payload);
841
889
  sp.start();
842
- return ['s', 'sim', 'y', 'yes'].includes(ans);
890
+ return r;
843
891
  },
844
892
  onRemote: ({ ttl }) => {
845
893
  if (streamJson) { _emit(core.AgentEvents.tool({ subtype: 'approval_remote', ttl: ttl || 120 })); return; }
@@ -1276,7 +1324,7 @@ async function metaCmd() {
1276
1324
  const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
1277
1325
  try {
1278
1326
  st = await metaMod.run(goalArg, {
1279
- token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
1327
+ token, lang: cfg.lang || 'pt', yes, autoAll: YOLO, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
1280
1328
  onAlert: ({ type, text }) => {
1281
1329
  sp.stop();
1282
1330
  const ic = type === 'human' ? C.warn('🙋') : type === 'escalate' ? C.indigo('🧠') : type === 'stagnated' ? C.err('🛑') : type === 'design' ? C.cyan('🎨') : type === 'retry' || type === 'conn' ? C.warn('📡') : C.warn('⏱');
@@ -1290,12 +1338,13 @@ async function metaCmd() {
1290
1338
  onChecklist: (itens) => { sp.stop(); console.log(_metaChecklistBox(itens) + '\n'); sp.start(); },
1291
1339
  onRound: ({ n, item, attempt }) => { sp.stop(); console.log(' ' + C.indigo('◆') + ' ' + C.bold(T.meta_round(n, item.slice(0, 70), attempt))); sp.start(); },
1292
1340
  onThinking: () => sp.text(T.agent_thinking),
1293
- onStep: ({ name, detail, blocked, loop, retry }) => {
1341
+ onStep: ({ name, detail, blocked, loop, retry, auto }) => {
1294
1342
  sp.stop();
1295
- console.log(' ' + (retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙')) + ' ' + name + (detail ? C.dim(' · ' + detail) : ''));
1343
+ console.log(' ' + (auto ? C.warn('▲ auto') : retry ? C.warn('⟳') : loop ? C.warn('↻ loop') : blocked ? C.err('■ ' + T.agent_blocked) : C.cyan('⚙')) + ' ' + name + (detail ? C.dim(' · ' + detail) : ''));
1296
1344
  sp.start();
1297
1345
  },
1298
- askApprove: async (cmd) => { sp.stop(); const a = String(await ui.ask(C.err('▲ ') + T.agent_approve(C.bold(cmd)))).trim().toLowerCase(); sp.start(); return ['s', 'sim', 'y', 'yes'].includes(a); },
1346
+ onStepDone: (e) => { sp.stop(); console.log(stepDoneLine(e, ' ')); sp.start(); },
1347
+ askApprove: async (payload) => { sp.stop(); const r = await askApproveRoute(payload); sp.start(); return r; },
1299
1348
  onRemote: ({ ttl }) => { sp.stop(); console.log(' ' + C.warn('▲') + ' ' + C.dim(T.agent_remote_wait(ttl || 120))); sp.start(); },
1300
1349
  onRoundDone: ({ checklist, spent }) => {
1301
1350
  sp.stop();
@@ -1812,8 +1861,8 @@ async function runbookCmd(args) {
1812
1861
  cwd: process.cwd(),
1813
1862
  askApprove: async (cmd) => {
1814
1863
  if (YES || !process.stdin.isTTY) return false; // não-interativo não aprova destrutivo
1815
- const a = await ui.ask(C.warn(' ▲ ' + (en ? 'destructive step: ' : 'passo destrutivo: ')) + cmd + C.dim(' [s/N] '));
1816
- return ['s', 'sim', 'y', 'yes'].includes(String(a).trim().toLowerCase());
1864
+ const d = await askDestructive(String(cmd), { en }); // resumo curto + ver completo (v)
1865
+ return d === true || d === 'all'; // runbook aprova passo-a-passo (sem sessão full-auto)
1817
1866
  },
1818
1867
  onStep: (e) => {
1819
1868
  if (e.phase === 'start') console.log(' ' + C.indigo('◆') + ' ' + (e.desc ? C.bold(e.desc) + C.dim(' · ') : '') + C.dim(e.cmd.slice(0, 60)));
package/lib/agent.js CHANGED
@@ -153,6 +153,15 @@ function loopDecision({ nSig, cycling, warnedThis, loopWarned, WARN, BREAK }) {
153
153
  return 'ok';
154
154
  }
155
155
 
156
+ // Decisão de aprovação de passo destrutivo (PURO/testável, fonte única do mapeamento).
157
+ // decision vem do askApprove interativo: true = aprovou este · 'all' = aprovar TUDO na sessão
158
+ // (full-auto) · false = recusou. autoAll = a sessão já está em full-auto (aprova sem perguntar).
159
+ function decideApproval({ autoAll, decision }) {
160
+ if (autoAll) return { approved: true, enableAll: true };
161
+ if (decision === 'all') return { approved: true, enableAll: true };
162
+ return { approved: decision === true, enableAll: false };
163
+ }
164
+
156
165
  // SUB-AGENTE de exploração (padrão Claude Code): contexto PRÓPRIO, só-leitura, poucos passos.
157
166
  // A leitura pesada acontece AQUI e só o RESUMO volta pro agente principal → economia de contexto.
158
167
  async function _subAgent({ task, k, model, cwd, lang, onStep }) {
@@ -215,10 +224,17 @@ async function _remoteApprove(comando, token, onRemote) {
215
224
  /**
216
225
  * Roda o agente. opts:
217
226
  * token (sessão ts) · lang · yes (auto-aprova NÃO-destrutivos; destrutivo recusa)
218
- * onStep({name, detail}) passo executado (pra UI) · askApprove(cmd) Promise<boolean>
227
+ * autoAll (FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos o gate anti-catástrofe
228
+ * de AUTO-destrutivos segue recusando na hora, full-auto não abre mão dele)
229
+ * onStep({name, detail, auto}) — passo iniciado (pra UI; auto=destrutivo aprovado em full-auto)
230
+ * onStepDone({name, ok, status, evidence}) — passo concluído (✓/✗ por etapa)
231
+ * askApprove(cmd|{kind,cmd}) → Promise<boolean|'all'> ('all' liga o full-auto na sessão)
219
232
  */
220
233
  async function run(task, opts = {}) {
221
- const { token, lang = 'pt', yes = false, model = null, confineDir = null, onStep = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
234
+ const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
235
+ // FULL-AUTO (autoridade total): aprova destrutivos sem perguntar. Começa por --yolo/--full-auto
236
+ // (opts.autoAll) e pode ser LIGADO no meio da sessão quando o humano responde "a" (aprovar tudo).
237
+ let autoApproveDestructive = !!autoAll;
222
238
  // CWD da SESSÃO: base de todo caminho relativo e do shell. Persiste entre
223
239
  // mensagens (o chamador passa opts.cwd e lê o out.cwd de volta). Missão usa a
224
240
  // pasta confinada. É MUTÁVEL: a ferramenta mudar_diretorio e o "cd" no início
@@ -465,6 +481,7 @@ async function run(task, opts = {}) {
465
481
  const name = (tc.function && tc.function.name) || '';
466
482
  let input = {}; try { input = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
467
483
  let result;
484
+ let _ran = false; // true só quando uma ferramenta REALMENTE executou (não gate/bloqueio) → status ✓/✗
468
485
 
469
486
  // ── WATCHDOG anti-loop: mede repetição ANTES de qualquer gate/execução ──
470
487
  const _sig = loopSig(name, input);
@@ -517,9 +534,28 @@ async function run(task, opts = {}) {
517
534
  // Interativo pergunta no terminal; em --yes (cron) tenta APROVAÇÃO REMOTA no Telegram do dono.
518
535
  if (result === undefined && (name === 'executar_comando' || name === 'executar_remoto') && tools.isDestructive(input.comando)) {
519
536
  let approved = false, remoteTried = false;
520
- if (!yes) approved = await askApprove(String(input.comando || ''));
521
- else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
522
- if (!approved) {
537
+ if (autoApproveDestructive) {
538
+ // FULL-AUTO não abre mão do anti-catástrofe: os auto-destrutivos de máquina/processo/
539
+ // pasta já caíram no gate acima (selfDestructiveReason). O que sobra é ~/.ts por shell
540
+ // (bypass dos gates dedicados skill_gerenciar/hooks) — esse NUNCA é auto-aprovado nem em
541
+ // --yolo: exige o humano. Todo o resto: aprova sem perguntar, mas VISÍVEL (auto:true).
542
+ if (core.touchesTsConfig(String(input.comando || ''))) {
543
+ onStep({ name, detail: argsShort(name, input), blocked: true });
544
+ result = { erro: lang !== 'en'
545
+ ? 'FULL-AUTO (--yolo) NÃO auto-aprova comando que mexe em ~/.ts (skills/hooks/mcp/config do ts via shell) — isso burlaria os gates dedicados. Use a ferramenta própria (skill_gerenciar) ou rode SEM --yolo pra aprovar manualmente.'
546
+ : 'FULL-AUTO (--yolo) will NOT auto-approve a command that touches ~/.ts (ts skills/hooks/mcp/config via shell) — it would bypass the dedicated gates. Use the proper tool (skill_gerenciar) or run WITHOUT --yolo to approve manually.' };
547
+ } else {
548
+ approved = true;
549
+ onStep({ name, detail: argsShort(name, input), auto: true });
550
+ }
551
+ } else if (!yes) {
552
+ // Passa payload ESTRUTURADO → a UI mostra um RESUMO curto (não o heredoc inteiro).
553
+ const dec = await askApprove({ kind: 'destructive', cmd: String(input.comando || '') });
554
+ const d = decideApproval({ autoAll: false, decision: dec });
555
+ approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
556
+ } else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
557
+ // result === undefined evita sobrescrever a mensagem específica do bloqueio ~/.ts do full-auto
558
+ if (!approved && result === undefined) {
523
559
  result = { erro: yes
524
560
  ? (remoteTried
525
561
  ? 'RECUSADO: o dono NEGOU (ou não respondeu em 2 min) a aprovação remota deste comando destrutivo. Não repita; siga sem ele e relate no resumo.'
@@ -549,7 +585,8 @@ async function run(task, opts = {}) {
549
585
  const prev = input.acao === 'criar'
550
586
  ? `criar skill "${_cl(input.slug)}" — ${_cl(input.descricao || input.nome).slice(0, 120)}\n motivo: ${_cl(input.motivo).slice(0, 200)}\n instruções:\n ${_full.slice(0, CAP)}${_nota}`
551
587
  : `melhorar skill "${_cl(input.slug)}"\n motivo: ${_cl(input.motivo).slice(0, 200)}\n trocar: ${_cl(input.buscar || (lang !== 'en' ? '(adicionar seção no fim)' : '(append section at the end)')).slice(0, 400)}\n por: ${_full.slice(0, Math.min(CAP, 400))}${_nota}`;
552
- const okS = await askApprove((lang !== 'en' ? 'O agente quer ATUALIZAR as próprias skills:\n ' : 'The agent wants to UPDATE its own skills:\n ') + prev);
588
+ const okS = autoApproveDestructive ? true : await askApprove((lang !== 'en' ? 'O agente quer ATUALIZAR as próprias skills:\n ' : 'The agent wants to UPDATE its own skills:\n ') + prev);
589
+ if (okS === 'all') autoApproveDestructive = true;
553
590
  if (!okS) {
554
591
  result = { erro: lang !== 'en'
555
592
  ? 'O usuário RECUSOU a mudança de skill. Não repita nesta sessão; siga com a tarefa normalmente.'
@@ -566,8 +603,12 @@ async function run(task, opts = {}) {
566
603
  const _js = JSON.stringify(input);
567
604
  const _lbl = 'MCP ' + _mcp.route[name].server + ' → ' + _mcp.route[name].realName + ' ' + (_js.length > 1500 ? _js.slice(0, 1500) + ' …(+' + (_js.length - 1500) + ' chars)' : _js);
568
605
  let approved = false, remoteTried = false;
569
- if (!yes) approved = await askApprove(_lbl);
570
- else ({ approved, remoteTried } = await _remoteApprove(_lbl, token, onRemote));
606
+ if (autoApproveDestructive) { approved = true; onStep({ name, detail: argsShort(name, input), auto: true }); }
607
+ else if (!yes) {
608
+ const dec = await askApprove(_lbl);
609
+ const d = decideApproval({ autoAll: false, decision: dec });
610
+ approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
611
+ } else ({ approved, remoteTried } = await _remoteApprove(_lbl, token, onRemote));
571
612
  if (!approved) {
572
613
  result = { erro: yes
573
614
  ? (remoteTried ? 'RECUSADO: o dono NEGOU (ou não respondeu) a aprovação remota desta ferramenta MCP mutante. Não repita; siga sem ela.' : 'RECUSADO automaticamente: ferramenta MCP mutante não roda em --yes sem aprovação remota (Telegram). Siga sem ela.')
@@ -589,8 +630,9 @@ async function run(task, opts = {}) {
589
630
  // 1ª chamada a ESTE servidor na sessão pede um OK (mesmo tool de leitura): os ARGUMENTOS
590
631
  // saem da máquina pro servidor externo, e podem ter sido influenciados por prompt-injection
591
632
  // de algo que o agente leu. Em --yes (cron) o dono já pré-autorizou os servers → não pergunta.
592
- if (!rt.needsApproval && !yes && !_mcpSeen.has(rt.server)) {
633
+ if (!rt.needsApproval && !yes && !autoApproveDestructive && !_mcpSeen.has(rt.server)) {
593
634
  const okFirst = await askApprove('1ª chamada ao servidor MCP "' + rt.server + '" (tool ' + rt.realName + '). Dados sairão pra ele. Permitir este servidor nesta sessão?');
635
+ if (okFirst === 'all') autoApproveDestructive = true;
594
636
  if (!okFirst) { result = { erro: 'O usuário NÃO autorizou o servidor MCP "' + rt.server + '" nesta sessão. Não use tools desse servidor; siga sem elas.' }; onStep({ name, detail: argsShort(name, input), blocked: true }); }
595
637
  }
596
638
  if (result === undefined) {
@@ -598,7 +640,7 @@ async function run(task, opts = {}) {
598
640
  onStep({ name: 'mcp:' + rt.realName, detail: rt.server });
599
641
  try { result = { resultado: await require('./mcp').callTool(rt.endpoint, rt.auth, rt.realName, input) }; }
600
642
  catch (e) { result = { erro: 'MCP falhou: ' + String((e && e.message) || e).slice(0, 200) }; }
601
- steps++;
643
+ steps++; _ran = true;
602
644
  }
603
645
  }
604
646
  // LIMITE DE PESQUISA (convergência): conta buscar_web + navegador abrir/ler. Ao passar o
@@ -636,7 +678,7 @@ async function run(task, opts = {}) {
636
678
  }
637
679
  else result = { erro: 'acao inválida — use abrir|clicar|digitar|ler|print.' };
638
680
  } catch (e) { result = { erro: String((e && e.message) || e).slice(0, 300) }; }
639
- steps++;
681
+ steps++; _ran = true;
640
682
  }
641
683
  // SUB-AGENTE: 'explorar' roda em contexto próprio (só-leitura) e devolve só o resumo.
642
684
  if (result === undefined && name === 'explorar') {
@@ -644,12 +686,12 @@ async function run(task, opts = {}) {
644
686
  const sub = await _subAgent({ task: input.tarefa || input.pergunta || '', k, model, cwd, lang, onStep });
645
687
  acc.inTok += sub._tin; acc.outTok += sub._tout; acc.cachedTok += sub._tcach || 0;
646
688
  result = { resumo: sub.resumo };
647
- steps++;
689
+ steps++; _ran = true;
648
690
  }
649
691
  if (result === undefined) {
650
692
  onStep({ name, detail: argsShort(name, input) });
651
693
  result = await tools.execute(name, input, { confineDir, baseDir: cwd, token });
652
- steps++;
694
+ steps++; _ran = true;
653
695
  // TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
654
696
  // sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
655
697
  // não falha de execução — não conta pro ledger nem pro sinal de erro da run.
@@ -681,6 +723,10 @@ async function run(task, opts = {}) {
681
723
  && _researchCalls >= Math.ceil(RESEARCH_MAX * 2 / 3) && _researchCalls <= RESEARCH_MAX) {
682
724
  result = Object.assign({}, result, { _aviso: 'Você já pesquisou ' + _researchCalls + 'x (limite ' + RESEARCH_MAX + '). Se já tem o suficiente, PARE de pesquisar e ENTREGUE o resultado agora.' });
683
725
  }
726
+ // STATUS POR ETAPA (UX): ✓ verde / ✗ vermelho por passo, a partir do ToolResult TIPADO
727
+ // (core.classifyToolResult) — o usuário vê na hora o que deu certo/errado (antes as ⚙
728
+ // não indicavam resultado nenhum). Só para passos que REALMENTE rodaram (não gate/bloqueio).
729
+ if (_ran) { const _c = core.classifyToolResult(result); onStepDone({ name, ok: _c.ok, status: _c.status, evidence: _c.evidence }); }
684
730
  // BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz →
685
731
  // encerra o turno devolvendo o pedido; a missão pausa e chama o usuário.
686
732
  if (result && result._needHuman) {
@@ -731,4 +777,4 @@ async function run(task, opts = {}) {
731
777
  return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr };
732
778
  }
733
779
 
734
- module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision } };
780
+ module.exports = { run, llm, _test: { winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval } };
package/lib/core.js CHANGED
@@ -12,6 +12,14 @@
12
12
  // — só funções puras e constantes, pra ser importável por qualquer superfície e testável.
13
13
 
14
14
  // ── 1) GATE DE DESTRUTIVO ─────────────────────────────────────────────────────
15
+ // ~/.ts = config do agente (skills viram INSTRUÇÕES futuras; hooks executam comandos;
16
+ // mcp.json guarda auth). Tocar nisso por shell burlaria os gates dedicados
17
+ // (skill_gerenciar/lembrar) → prompt-injection persistente. Exige a barra antes de ".ts/"
18
+ // — não casa arquivos TypeScript (utils.ts). É o subconjunto "endurecido" do gate destrutivo:
19
+ // o ÚNICO destrutivo que o FULL-AUTO (--yolo) NÃO libera sozinho — sempre exige o humano.
20
+ const TS_CONFIG_RE = /[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i;
21
+ function touchesTsConfig(cmd) { return TS_CONFIG_RE.test(String(cmd || '')); }
22
+
15
23
  // Padrões de comando destrutivo/irreversível — SEMPRE pedem aprovação humana.
16
24
  // (fonte única: antes vivia em tools.js; App e Web tinham cópias divergentes.)
17
25
  const DESTRUCTIVE = [
@@ -25,11 +33,7 @@ const DESTRUCTIVE = [
25
33
  /\btaskkill\b.*\/im\b/i, /\btaskkill\b.*\/f\b.*\/im/i, // taskkill /IM mata TODOS os processos daquele nome (ex: node.exe = a própria missão)
26
34
  /\bStop-Process\b[^|;&]*-Name\b/i, /\bGet-Process\b[^|]*\|[^|]*\bStop-Process\b/i, // PowerShell: matar por NOME mata todos (=taskkill /IM); Stop-Process -Id <pid> segue liberado
27
35
  /\bsystemctl\s+(stop|disable|mask)\b/i, /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm)\b/i,
28
- // ~/.ts = config do agente (skills viram INSTRUÇÕES futuras; hooks executam comandos;
29
- // mcp.json guarda auth). Tocar nisso por shell burlaria os gates dedicados
30
- // (skill_gerenciar/lembrar) → prompt-injection persistente. Sempre pede aprovação.
31
- // Exige a barra antes de ".ts/" — não casa arquivos TypeScript (utils.ts).
32
- /[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i,
36
+ TS_CONFIG_RE, // ~/.ts por shell = bypass dos gates dedicados (ver comentário acima)
33
37
  ];
34
38
  function isDestructive(cmd) { return DESTRUCTIVE.some(re => re.test(String(cmd || ''))); }
35
39
 
@@ -109,7 +113,20 @@ function classifyError(msg) {
109
113
  // NÃO muta o raw. Convenções do TS: {erro} = falha; {codigo|exitCode}≠0 = falha de shell;
110
114
  // {written|created|resumo|stdout|...} = evidência de sucesso.
111
115
  function classifyToolResult(raw) {
112
- if (raw == null || typeof raw !== 'object') return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: raw != null ? String(raw).slice(0, 200) : '' };
116
+ if (raw == null) return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: '' };
117
+ // Alguns adaptadores legados retornam falhas como texto. Tratar todo texto como sucesso
118
+ // permitia que "ERRO ao executar..." virasse etapa concluída. Mantém strings normais como
119
+ // sucesso, mas reconhece prefixos inequívocos de falha/bloqueio.
120
+ if (typeof raw !== 'object') {
121
+ const text = String(raw);
122
+ const failedText = /^\s*(?:(?:erro|error|falha|failed|bloquead[oa])(?:\s*[:\-]|\s+(?:ao|a|de|do|da|em|no|na|to|while|executing|calling|opening|reading|writing|request|operation)\b|\s*$)|operação cancelada\b|o usuário recusou\b)/i.test(text);
123
+ if (failedText) {
124
+ const errorClass = classifyError(text);
125
+ return { ok: false, status: errorClass === 'blocked' ? 'blocked' : 'failed', errorClass,
126
+ retryable: RETRYABLE_CLASSES.has(errorClass), evidence: text.replace(/\s+/g, ' ').slice(0, 300) };
127
+ }
128
+ return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: text.slice(0, 200) };
129
+ }
113
130
  const exit = raw.codigo != null ? raw.codigo : (raw.exitCode != null ? raw.exitCode : null);
114
131
  const errMsg = raw.erro || raw.error || (exit != null && exit !== 0 ? (raw.stderr || raw.stdout || ('exit ' + exit)) : '');
115
132
  const failed = !!raw.erro || !!raw.error || (exit != null && exit !== 0);
@@ -128,7 +145,7 @@ function classifyToolResult(raw) {
128
145
  }
129
146
 
130
147
  module.exports = {
131
- DESTRUCTIVE, isDestructive,
148
+ DESTRUCTIVE, isDestructive, touchesTsConfig,
132
149
  MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
133
150
  AgentEvents, EVENT_TYPES,
134
151
  ERROR_CLASSES, RETRYABLE_CLASSES, classifyError, classifyToolResult,
package/lib/i18n.js CHANGED
@@ -41,6 +41,7 @@ const STR = {
41
41
  ['ts sentinela add "nome" --cmd "..."', 'VIGIA determinístico (regra do if, ZERO token); --escalar diagnostica se falhar; --autofix conserta (com aprovação no Telegram)'],
42
42
  ['ts sentinela instalar', 'agenda os checks no SO (cron/Task Scheduler) e avisa no Telegram'],
43
43
  ['ts agente --continuar "..."', 'retoma o trabalho anterior desta pasta'],
44
+ ['ts agente "..." --yolo', 'FULL-AUTO: aprova TUDO na sessão, inclusive destrutivos (o gate anti-catástrofe ~/.ts/máquina segue ativo)'],
44
45
  ['ts agente "..." --navegador', 'EXPERIMENTAL: dá um Chrome real ao agente (abrir/ler/clicar/print+visão)'],
45
46
  ['ts meta "objetivo grande"', 'MISSÃO: checklist + rodadas até terminar (noturno)'],
46
47
  ['ts meta "..." --criterios prova.json', 'só declara "verificada" se os critérios executáveis (verify) passarem de verdade'],
@@ -197,6 +198,7 @@ const STR = {
197
198
  ['ts agente "task"', 'actually does it here: commands, files, diagnosis'],
198
199
  ['ts agente "..." --yes', 'autonomous (destructive asks on Telegram)'],
199
200
  ['ts agente --continuar "..."', 'resume this folder\'s previous work'],
201
+ ['ts agente "..." --yolo', 'FULL-AUTO: approves EVERYTHING this session, destructive included (~/.ts & machine anti-catastrophe still on)'],
200
202
  ['ts agente "..." --navegador', 'EXPERIMENTAL: gives the agent a real Chrome (open/read/click/print+vision)'],
201
203
  ['ts sentinela add "name" --cmd "..."', 'DETERMINISTIC watch (if-rule, ZERO tokens); escalates to AI only on failure'],
202
204
  ['ts sentinela instalar', 'schedule checks on the OS (cron/Task Scheduler), alert on Telegram'],
package/lib/meta.js CHANGED
@@ -1011,7 +1011,8 @@ async function makeChecklist(goal, token) {
1011
1011
  // CRITÉRIOS EXECUTÁVEIS (verify): a prova FINAL da missão. Rodados quando os gates de
1012
1012
  // build/run passam, ANTES de declarar 'verified'. Se algum critério do usuário falha, a
1013
1013
  // missão NÃO é verificada (honestidade — não basta compilar/abrir; tem que CUMPRIR o pedido).
1014
- // Sem critérios nada a provar (retorna true). runAll nunca lança.
1014
+ // Sem critérios explícitos, os gates de build/run continuam sendo a prova. Erro interno do
1015
+ // verificador é fail-closed: nunca transforma ausência de prova em `verified`.
1015
1016
  async function _checkCriteria(st, dir) {
1016
1017
  if (!st.criteria || !st.criteria.length) return true;
1017
1018
  try {
@@ -1019,11 +1020,15 @@ async function _checkCriteria(st, dir) {
1019
1020
  st.verifyReport = { passed: rep.passed, total: rep.total, allOk: rep.allOk, results: rep.results.map(r => ({ ok: r.ok, label: r.label, detail: r.detail })) };
1020
1021
  if (!rep.allOk) st.runNote = (st.runNote || '') + ` critérios: ${rep.passed}/${rep.total} passaram · falharam: ${rep.results.filter(r => !r.ok).map(r => r.label).slice(0, 3).join(' | ')}`;
1021
1022
  return rep.allOk;
1022
- } catch (_) { return true; }
1023
+ } catch (e) {
1024
+ st.verifyReport = { passed: 0, total: st.criteria.length, allOk: false, results: [], error: String((e && e.message) || e).slice(0, 300) };
1025
+ st.runNote = (st.runNote || '') + ' verificador falhou internamente; missão não marcada como verified';
1026
+ return false;
1027
+ }
1023
1028
  }
1024
1029
 
1025
1030
  async function run(goal, opts = {}) {
1026
- const { token, lang = 'pt', yes = false, budget = 400, maxRounds = 20, dir = process.cwd(), model = null, thinker = null, maxMinutes = 0, designer = null, design = 'auto', runGate = true, eye = null, visualLadder = true, prove = false } = opts;
1031
+ const { token, lang = 'pt', yes = false, autoAll = false, budget = 400, maxRounds = 20, dir = process.cwd(), model = null, thinker = null, maxMinutes = 0, designer = null, design = 'auto', runGate = true, eye = null, visualLadder = true, prove = false } = opts;
1027
1032
  const onChecklist = opts.onChecklist || (() => {});
1028
1033
  const onRound = opts.onRound || (() => {});
1029
1034
  const onRoundDone = opts.onRoundDone || (() => {});
@@ -1064,12 +1069,14 @@ async function run(goal, opts = {}) {
1064
1069
  }
1065
1070
  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);
1066
1071
  // CRITÉRIOS (TaskSpec): usuário (--criterios) manda; senão, com --provar, combina o que o
1067
- // PLANNER propôs (mk.criteria, já validado) + os TESTES da stack — tudo recompilado. Sem
1068
- // --provar, guarda o que o planner propôs (plannedCriteria) como referência, sem bloquear.
1072
+ // PLANNER propôs (mk.criteria, já validado) + os TESTES da stack — tudo recompilado.
1073
+ // Mesmo sem --provar, critérios determinísticos derivados da stack rodam por padrão;
1074
+ // critérios inventados pelo planner ficam apenas como referência até o usuário pedir prova ampla.
1069
1075
  const _V = require('./verify');
1070
1076
  let _criteria = [], _planned = mk.criteria || [];
1071
1077
  if (Array.isArray(opts.criteria) && opts.criteria.length) _criteria = opts.criteria;
1072
1078
  else if (prove) _criteria = _V.compileCriteria([..._V.deriveFromStack(dir), ..._planned]).criteria.slice(0, 8);
1079
+ else _criteria = _V.compileCriteria(_V.deriveFromStack(dir)).criteria.slice(0, 8);
1073
1080
  st = { goal: String(goal).slice(0, 4000), mockup: opts.mockup || null, archBrief, feasBrief, checklist: mk.itens, rounds: [], creditsSpent: (mk.credits || 0) + designCred + archCred + feasCred,
1074
1081
  budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, criteria: _criteria, plannedCriteria: _planned, created_at: new Date().toISOString() };
1075
1082
  save(st, dir);
@@ -1313,7 +1320,7 @@ async function run(goal, opts = {}) {
1313
1320
  }
1314
1321
  let out = null, connErr = null;
1315
1322
  for (let att = 0; att < 3 && !out; att++) {
1316
- try { out = await agent.run(task, { token, lang, yes, model: roundModel, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking }); }
1323
+ try { out = await agent.run(task, { token, lang, yes, autoAll, model: roundModel, confineDir: dir, skipSessionStart: true, onStep: opts.onStep, onStepDone: opts.onStepDone, askApprove: opts.askApprove, onRemote: opts.onRemote, onThinking: opts.onThinking }); }
1317
1324
  catch (e) {
1318
1325
  connErr = e;
1319
1326
  // teto de IA estourado → PAUSA limpa e resumível com CTA de upgrade (nunca segue em silêncio)
@@ -1346,14 +1353,16 @@ async function run(goal, opts = {}) {
1346
1353
  // (Bug real do teste MultiApps: flash-lite bloqueou itens cujos arquivos existiam no disco.)
1347
1354
  let marks = [];
1348
1355
  const written = (out.actions || []).filter(a => a.name === 'escrever_arquivo' || a.name === 'editar_arquivo').map(a => String(a.target || ''));
1349
- const ranOk = (out.actions || []).some(a => a.name === 'executar_comando');
1350
1356
  const _basename = (p) => String(p).replace(/\\/g, '/').split('/').pop().toLowerCase();
1351
1357
  const _fileHints = (txt) => (String(txt).match(/[\w.\-]+\.(kt|java|xml|gradle|json|md|txt|kts|properties|pro|png|webp|py|js|ts|html|css|sh)\b/gi) || []).map(s => s.toLowerCase());
1352
1358
  const itemFiles = _fileHints(item.desc);
1353
- const wroteForItem = itemFiles.length
1354
- ? itemFiles.some(f => written.some(w => _basename(w) === f))
1355
- : written.length > 0; // item sem nome de arquivo explícito mas a rodada produziu algo
1356
- if (wroteForItem || (ranOk && !itemFiles.length)) marks.push(item.id);
1359
+ // Uma escrita/comando genérico não prova um item sem vínculo. Só há marcação automática
1360
+ // quando o próprio texto do item identifica o artefato e a rodada gravou esse artefato.
1361
+ // Itens abstratos continuam podendo ser avaliados pelo marcador, mas não "vazam" sucesso
1362
+ // para outros itens porque algum arquivo/comando apareceu na mesma rodada.
1363
+ const wroteForItem = itemFiles.length > 0
1364
+ && itemFiles.some(f => written.some(w => _basename(w) === f));
1365
+ if (wroteForItem) marks.push(item.id);
1357
1366
 
1358
1367
  // Item de correção de build: NÃO usa marcador — o PORTÃO DE BUILD é a única
1359
1368
  // autoridade (recompila de verdade). Marca provisório pra reabrir o gate; se o
@@ -1368,15 +1377,14 @@ async function run(goal, opts = {}) {
1368
1377
  continue;
1369
1378
  }
1370
1379
 
1371
- // Marcador IA como REFORÇO: confirma o alvo se a evidência objetiva não pegou, e
1372
- // credita OUTROS itens do checklist que a rodada também concluiu.
1380
+ // Marcador IA como REFORÇO do ITEM ALVO. Não aceita mais `tambem_concluidos`: uma
1381
+ // narrativa de uma rodada não é evidência suficiente para concluir itens não executados.
1373
1382
  try {
1374
1383
  const evid = written.length ? '\n\nARQUIVOS REALMENTE GRAVADOS nesta rodada (evidência objetiva):\n' + written.map(_basename).join(', ') : '';
1375
1384
  const mk = await _llmJson({ token, system: MARK_SYS,
1376
1385
  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}` });
1377
1386
  st.creditsSpent += mk.credits || 0;
1378
1387
  if (mk.json && mk.json.passou === true) marks.push(item.id);
1379
- if (Array.isArray(mk.json?.tambem_concluidos)) marks.push(...mk.json.tambem_concluidos.map(String));
1380
1388
  } catch (_) {}
1381
1389
  marks = [...new Set(marks)];
1382
1390
  for (const it of st.checklist) if (marks.includes(it.id)) it.passes = true;
package/lib/ui.js CHANGED
@@ -99,8 +99,20 @@ function ask(question) {
99
99
  });
100
100
  }
101
101
 
102
+ // Resumo de 1 linha de um comando pro prompt de aprovação: a 1ª linha SIGNIFICATIVA (que
103
+ // num heredoc é o comando externo, ex "mysql -u root pw <<'SQL'") + quantas linhas ficam
104
+ // escondidas. PURO/testável — a cor e as teclas do prompt ficam no bin. Evita despejar 50+
105
+ // linhas de um heredoc SQL antes do usuário decidir.
106
+ function cmdSummary(cmd, max = 72) {
107
+ const raw = String(cmd == null ? '' : cmd);
108
+ const lines = raw.split('\n');
109
+ const first = (lines.find(l => l.trim()) || raw).trim();
110
+ const shown = first.length > max ? first.slice(0, max - 1) + '…' : first;
111
+ return { shown, hidden: Math.max(0, lines.length - 1), lines: lines.length };
112
+ }
113
+
102
114
  const okLine = (s) => ' ' + C.ok('✔') + ' ' + s;
103
115
  const errLine = (s) => ' ' + C.err('✖') + ' ' + s;
104
116
  const infoLine = (s) => ' ' + C.cyan('⌁') + ' ' + s;
105
117
 
106
- module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, okLine, errLine, infoLine };
118
+ module.exports = { C, TTY, gradient, banner, box, spinner, md, bar, ask, cmdSummary, okLine, errLine, infoLine };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.74.0",
3
+ "version": "0.76.0",
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"