terminal-smart-cli 0.67.0 → 0.73.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
@@ -1007,6 +1007,37 @@ async function evalCmd(words) {
1007
1007
  process.exit(2);
1008
1008
  }
1009
1009
 
1010
+ // ── MODEL LAB: --modelos m1,m2,m3 → bake-off automático (mesma suíte em cada executor) ──
1011
+ const _modelsArg = _val(['--modelos', '--models']);
1012
+ if (_modelsArg) {
1013
+ const models = _modelsArg.split(',').map(s => s.trim()).filter(Boolean);
1014
+ if (models.length < 2) { _die(en ? '--modelos needs at least 2 models (comma-separated)' : '--modelos precisa de pelo menos 2 modelos (separados por vírgula)', 2); }
1015
+ if (!JSON_OUT) {
1016
+ console.log('\n ' + ui.gradient('⌁ ' + (en ? 'Model Lab' : 'Model Lab')) + ' ' + C.bold(suite.name) + C.dim(' · ' + suite.cases.length + (en ? ' case(s) × ' : ' caso(s) × ') + models.length + (en ? ' models' : ' modelos')));
1017
+ console.log(' ' + C.dim(en ? 'same suite on each executor; a fixed judge scores all (fair). Costs credits.' : 'mesma suíte em cada executor; um juiz fixo pontua todos (justo). Gasta créditos.') + '\n');
1018
+ }
1019
+ const spM = JSON_OUT ? { start() {}, stop() {}, text() {} } : ui.spinner('…').start();
1020
+ let mx;
1021
+ try {
1022
+ mx = await evalMod.runMatrix(suite, {
1023
+ token, lang: cfg.lang || 'pt', judgeModel, models,
1024
+ onModel: ({ model, phase, row }) => { if (JSON_OUT) return; if (phase === 'start') spM.text((en ? 'testing ' : 'testando ') + model + '…'); else if (phase === 'done') { spM.stop(); console.log(' ' + C.cyan('◆') + ' ' + C.bold(model) + C.dim(' ' + row.passed + '/' + row.total + ' · score ' + row.avgScore + ' · ' + row.credits + ' cr') + (row.error ? C.err(' ' + row.error) : '')); spM.start(); } },
1025
+ onCase: ({ phase, id, pass, score }) => { if (!JSON_OUT && phase === 'done') { spM.stop(); console.log(' ' + (pass ? C.ok('✔') : C.err('✗')) + C.dim(' ' + id + ' ' + score)); spM.start(); } },
1026
+ });
1027
+ } catch (e) { spM.stop(); if (e && (e.code === 'no_credits' || e.status === 402)) { console.error('\n' + ui.errLine(en ? 'AI limit reached — top up to run the bake-off.' : 'Limite de IA atingido — recarregue pra rodar o bake-off.')); process.exit(1); } throw e; }
1028
+ spM.stop();
1029
+ if (JSON_OUT) { console.log(JSON.stringify(mx)); return; }
1030
+ // placar ordenado (qualidade → custo)
1031
+ console.log('\n' + ui.box([
1032
+ C.bold(en ? 'Bake-off ranking' : 'Placar do bake-off') + C.dim(' (' + (en ? 'quality then cost' : 'qualidade depois custo') + ')'),
1033
+ '',
1034
+ ...mx.ranked.map((r, i) => (i === 0 ? C.ok('①') : C.dim(' ' + (i + 1) + ' ')) + ' ' + C.bold(r.model.padEnd(20)) + C.dim(r.passed + '/' + r.total + ' ok · score ' + r.avgScore + ' · ' + r.credits + ' cr · ' + (r.ms / 1000).toFixed(1) + 's' + (r.error ? ' ⚠ ' + r.error : ''))),
1035
+ '',
1036
+ C.dim(en ? 'winner: ' : 'vencedor: ') + C.ok(C.bold(mx.winner || '—')),
1037
+ ], { title: 'ts model lab' }));
1038
+ return;
1039
+ }
1040
+
1010
1041
  if (!JSON_OUT) {
1011
1042
  console.log('\n ' + ui.gradient('⌁ ' + (en ? 'Eval' : 'Avaliação')) + ' ' + C.bold(suite.name) + C.dim(' · ' + suite.cases.length + (en ? ' case(s)' : ' caso(s)')));
1012
1043
  console.log(' ' + C.dim(en ? 'runs the real agent per case (costs credits), an AI judge scores each' : 'roda o agente de verdade por caso (gasta créditos); um juiz de IA pontua cada um') + '\n');
@@ -1185,21 +1216,22 @@ async function metaCmd() {
1185
1216
  const maxJanelasEff = maxJanelas || ((budgetTotal || maxHoras) ? 0 : 20);
1186
1217
  // CRITÉRIOS EXECUTÁVEIS (verify): --criterios <arquivo.json> = a PROVA de que a missão
1187
1218
  // cumpriu o pedido (não basta compilar/abrir). Falhou → missão sai "NÃO verificada".
1219
+ // CRITÉRIOS (TaskSpec): --criterios <arquivo> (usuário manda) OU --provar (o meta auto-monta:
1220
+ // planner propõe + stack dá os testes, tudo dentro do run). Sem nada = comportamento atual.
1188
1221
  let criteria = [];
1222
+ const prove = FLAGS.has('--provar') || FLAGS.has('--prove');
1189
1223
  const _verify = require('../lib/verify');
1190
1224
  const _critFile = (() => { const i = rawArgs.findIndex(a => a === '--criterios' || a === '--criteria'); return i >= 0 ? rawArgs[i + 1] : null; })();
1191
1225
  if (_critFile) {
1192
1226
  try { const j = JSON.parse(require('fs').readFileSync(_critFile, 'utf8')); const comp = _verify.compileCriteria(j); criteria = comp.criteria;
1193
1227
  if (comp.dropped.length) console.log(' ' + C.dim((cfg.lang === 'en' ? 'ignored ' : 'ignorei ') + comp.dropped.length + (cfg.lang === 'en' ? ' invalid criterion(s)' : ' critério(s) inválido(s)'))); }
1194
1228
  catch (e) { console.error(ui.errLine((cfg.lang === 'en' ? 'criteria file: ' : 'arquivo de critérios: ') + (e.message || e))); process.exit(2); }
1195
- } else if (FLAGS.has('--provar') || FLAGS.has('--prove')) {
1196
- // TaskSpec automático: deriva os critérios da stack (testes) sem json na mão.
1197
- criteria = _verify.deriveFromStack(dir);
1198
- if (criteria.length) console.log(' ' + C.dim((cfg.lang === 'en' ? 'auto-derived proof: ' : 'prova auto-derivada: ') + criteria.map(c => c.label || c.type).join(' · ')) + '\n');
1199
- else console.log(' ' + C.dim(cfg.lang === 'en' ? '--provar: no test command detected in this project (nothing to auto-prove)' : '--provar: nenhum comando de teste detectado neste projeto (nada a auto-provar)') + '\n');
1229
+ } else if (prove) {
1230
+ console.log(' ' + C.dim(cfg.lang === 'en' ? 'proof mode: the planner proposes criteria + the stack tests will be verified at the end' : 'modo prova: o planner propõe critérios + os testes da stack serão verificados no fim') + '\n');
1200
1231
  }
1201
1232
 
1202
1233
  const existing = metaMod.load(dir);
1234
+ if (FLAGS.has('--trilha') || FLAGS.has('--trail')) return trilhaCmd();
1203
1235
  if (FLAGS.has('--status')) {
1204
1236
  if (!existing) { console.log(ui.infoLine(T.meta_none)); return; }
1205
1237
  const done = existing.checklist.filter(i => i.passes).length;
@@ -1244,7 +1276,7 @@ async function metaCmd() {
1244
1276
  const goalArg = janela === 1 ? (goal || null) : null; // janelas seguintes só retomam
1245
1277
  try {
1246
1278
  st = await metaMod.run(goalArg, {
1247
- token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria,
1279
+ token, lang: cfg.lang || 'pt', yes, budget, maxRounds, dir, model: forcedModel, thinker, maxMinutes, designer, design, eye, visualLadder, mockup, arch, criteria, prove,
1248
1280
  onAlert: ({ type, text }) => {
1249
1281
  sp.stop();
1250
1282
  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('⏱');
@@ -1734,6 +1766,130 @@ async function verificarCmd(args) {
1734
1766
  process.exit(rep.allOk ? 0 : 1);
1735
1767
  }
1736
1768
 
1769
+ // ── ts runbook — procedimentos DevOps reexecutáveis e auto-verificáveis ──────
1770
+ async function runbookCmd(args) {
1771
+ const en = (cfg.lang === 'en');
1772
+ const rb = require('../lib/runbook');
1773
+ const sub = String(args[0] || 'listar').toLowerCase();
1774
+ const nome = args.slice(1).find(a => !a.startsWith('-'));
1775
+
1776
+ if (sub === 'listar' || sub === 'ls' || sub === 'list') {
1777
+ const l = rb.list();
1778
+ if (!l.length) { console.log('\n' + ui.infoLine(en ? 'no runbooks yet — create one: ts runbook novo <name>' : 'nenhum runbook ainda — crie um: ts runbook novo <nome>')); return; }
1779
+ console.log('\n' + ui.box([
1780
+ C.bold(en ? 'Runbooks' : 'Runbooks') + C.dim(' (' + l.length + ')'),
1781
+ ...l.map(n => { const r = rb.load(n) || {}; return ' ' + C.bold(n) + C.dim(' ' + (r.passos ? r.passos.length : 0) + ' ' + (en ? 'step(s)' : 'passo(s)') + ' · ' + (r.criterios ? r.criterios.length : 0) + ' ' + (en ? 'check(s)' : 'checagem(ns)') + (r.descricao ? ' — ' + String(r.descricao).slice(0, 40) : '')); }),
1782
+ '', C.dim(en ? 'run: ts runbook rodar <name> · edit the .json in ~/.ts/runbooks/' : 'rodar: ts runbook rodar <nome> · edite o .json em ~/.ts/runbooks/'),
1783
+ ], { title: 'ts runbook' }));
1784
+ return;
1785
+ }
1786
+ if (sub === 'novo' || sub === 'new' || sub === 'criar') {
1787
+ if (!nome) { console.log(ui.errLine(en ? 'usage: ts runbook novo <name>' : 'uso: ts runbook novo <nome>')); return; }
1788
+ const o = rb.save(nome, rb.template(nome));
1789
+ console.log('\n' + ui.box([C.ok(en ? 'Runbook created' : 'Runbook criado') + C.dim(' ' + o.nome), C.dim(require('../lib/runbook').DIR + require('path').sep + o.nome + '.json'), C.dim(en ? 'edit it (passos + criterios), then: ts runbook rodar ' + o.nome : 'edite (passos + criterios) e rode: ts runbook rodar ' + o.nome)], { title: 'ts runbook' }));
1790
+ return;
1791
+ }
1792
+ if (sub === 'ver' || sub === 'show' || sub === 'cat') {
1793
+ const r = rb.load(nome); if (!r) { console.log(ui.errLine(en ? 'no such runbook' : 'runbook não encontrado')); return; }
1794
+ const v = rb.validate(r);
1795
+ console.log('\n' + ui.box([
1796
+ C.bold(r.nome) + (r.descricao ? C.dim(' — ' + r.descricao) : '') + (v.ok ? C.ok(' ✓') : C.err(' ✗ ' + v.erros.join('; '))),
1797
+ '', C.bold(en ? 'STEPS' : 'PASSOS') + ':',
1798
+ ...(r.passos || []).map((p, i) => C.dim(' ' + (i + 1) + '. ') + (p.desc ? C.bold(p.desc) + C.dim(' ') : '') + C.cyan(String(p.cmd).slice(0, 60)) + (p.remoto ? C.dim(' @' + String(p.remoto).split(' ').pop()) : '') + (require('../lib/core').isDestructive(p.cmd) ? C.warn(' ⚠ destrutivo') : '')),
1799
+ '', C.bold(en ? 'PROOF' : 'PROVA') + ':',
1800
+ ...(r.criterios || []).map(c => C.dim(' • ') + C.dim(require('../lib/verify')._labelFor(c))),
1801
+ ], { title: 'ts runbook' }));
1802
+ return;
1803
+ }
1804
+ if (sub === 'remover' || sub === 'rm' || sub === 'remove' || sub === 'del') {
1805
+ console.log('\n' + (rb.remove(nome) ? ui.infoLine((en ? 'removed ' : 'removido ') + nome) : ui.errLine(en ? 'no such runbook' : 'runbook não encontrado')));
1806
+ return;
1807
+ }
1808
+ if (sub === 'rodar' || sub === 'run' || sub === 'exec') {
1809
+ const r = rb.load(nome); if (!r) { console.log(ui.errLine(en ? 'no such runbook' : 'runbook não encontrado')); return; }
1810
+ console.log('\n' + C.dim(en ? 'running runbook ' : 'rodando runbook ') + C.bold(r.nome) + C.dim(' — ' + (r.passos || []).length + (en ? ' step(s)' : ' passo(s)')) + '\n');
1811
+ const out = await rb.run(r, {
1812
+ cwd: process.cwd(),
1813
+ askApprove: async (cmd) => {
1814
+ 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());
1817
+ },
1818
+ onStep: (e) => {
1819
+ if (e.phase === 'start') console.log(' ' + C.indigo('◆') + ' ' + (e.desc ? C.bold(e.desc) + C.dim(' · ') : '') + C.dim(e.cmd.slice(0, 60)));
1820
+ else if (e.phase === 'done') console.log(' ' + (e.ok ? C.ok('✔ exit 0') : C.err('✗ exit ' + e.code)) + (e.ok ? '' : C.dim(' ' + String(e.out || '').split('\n').slice(-1)[0].slice(0, 80))));
1821
+ else if (e.phase === 'refused' || e.phase === 'denied') console.log(' ' + C.err('■ ' + (e.motivo || 'bloqueado')));
1822
+ else if (e.phase === 'verify') console.log('\n ' + C.dim((en ? 'proving result — ' : 'provando o resultado — ') + e.total + (en ? ' check(s)…' : ' checagem(ns)…')));
1823
+ },
1824
+ });
1825
+ if (out.verify) for (const vr of out.verify.results) console.log(' ' + (vr.ok ? C.ok('✔') : C.err('✗')) + ' ' + C.dim((vr.label || '') + (vr.ok ? '' : ' — ' + (vr.detail || ''))));
1826
+ if (out.ok) {
1827
+ console.log('\n' + ui.okLine(C.bold('RUNBOOK OK') + C.dim(en ? ' — steps ran and the result is proven' : ' — passos rodaram e o resultado está provado')));
1828
+ } else {
1829
+ const _why = out.erro || (out.verify && !out.verify.allOk ? (en ? 'proof failed' : 'a prova falhou') : '');
1830
+ console.log('\n' + ui.errLine(C.bold(en ? 'RUNBOOK FAILED' : 'RUNBOOK FALHOU') + C.dim(' ' + _why)));
1831
+ }
1832
+ process.exit(out.ok ? 0 : 1);
1833
+ }
1834
+ console.log(ui.infoLine(en ? 'ts runbook: listar | novo <name> | ver <name> | rodar <name> | remover <name>' : 'ts runbook: listar | novo <nome> | ver <nome> | rodar <nome> | remover <nome>'));
1835
+ }
1836
+
1837
+ // ── ts trilha — timeline AUDITÁVEL da missão (Evidence Ledger: por que está done) ──
1838
+ function trilhaCmd() {
1839
+ const en = (cfg.lang === 'en');
1840
+ const metaMod = require('../lib/meta');
1841
+ const st = metaMod.load(process.cwd());
1842
+ if (!st) { console.log('\n' + ui.infoLine(en ? 'no mission in this folder (run ts meta first)' : 'nenhuma missão nesta pasta (rode ts meta primeiro)')); return; }
1843
+ const t = metaMod.trailSummary(st, cfg.lang);
1844
+ if (JSON_OUT) { console.log(JSON.stringify(t)); return; }
1845
+ const _mark = { done: C.ok('✔'), blocked: C.err('!'), pending: C.dim('○') };
1846
+ const lines = [
1847
+ C.bold((en ? 'Goal: ' : 'Objetivo: ')) + C.dim(String(t.goal).slice(0, 66)),
1848
+ (t.verification.ok ? C.ok : C.warn)(t.verification.icon + ' ' + t.verification.txt) + C.dim(' · ' + t.status + ' · ' + t.progress.done + '/' + t.progress.total + (en ? ' items' : ' itens')),
1849
+ '',
1850
+ C.bold(en ? 'CHECKLIST' : 'CHECKLIST') + ':',
1851
+ ...t.checklist.map(i => ' ' + (_mark[i.state] || '○') + ' ' + C.dim(String(i.desc).slice(0, 70))),
1852
+ '',
1853
+ C.bold(en ? 'TIMELINE' : 'LINHA DO TEMPO') + C.dim(' (' + t.rounds.length + (en ? ' rounds)' : ' rodadas)')),
1854
+ ...t.rounds.slice(-8).map(r => ' ' + C.indigo('R' + r.n) + ' ' + C.dim(String(r.item).slice(0, 44)) + C.dim(' · ' + r.steps + 'p · ' + r.credits + 'cr')),
1855
+ '',
1856
+ C.bold(en ? 'GATES' : 'GATES') + ': ' + C.dim((t.gates.build ? C.ok('build✔') : 'build○') + ' ' + (t.gates.run ? C.ok('run✔') : 'run○') + ' ' + (t.gates.visual ? C.ok('visual✔') : 'visual○')),
1857
+ ];
1858
+ if (t.proof) {
1859
+ lines.push('', C.bold(en ? 'PROOF (criteria)' : 'PROVA (critérios)') + C.dim(' ' + t.proof.passed + '/' + t.proof.total));
1860
+ for (const r of t.proof.results) lines.push(' ' + (r.ok ? C.ok('✔') : C.err('✗')) + ' ' + C.dim((r.label || '') + (r.ok ? '' : ' — ' + (r.detail || ''))));
1861
+ }
1862
+ lines.push('', C.dim(en ? 'cost: ' : 'custo: ') + t.cost.credits + ' cr · ' + t.cost.rounds + (en ? ' rounds' : ' rodadas') + (t.cost.escalations ? ' · ' + t.cost.escalations + ' esc' : '') + (t.cost.minutes != null ? ' · ' + t.cost.minutes + ' min' : ''));
1863
+ lines.push('', (t.verification.ok ? C.ok : C.warn)(t.verdict));
1864
+ console.log('\n' + ui.box(lines, { title: 'ts trilha' }));
1865
+ }
1866
+
1867
+ // ── ts doctor — auto-diagnóstico do ambiente (Node/login/gateway/versão/deps) ──
1868
+ async function doctorCmd() {
1869
+ const en = (cfg.lang === 'en');
1870
+ const doc = require('../lib/doctor');
1871
+ const sp = JSON_OUT ? { stop() {} } : ui.spinner(en ? 'checking your setup…' : 'checando seu ambiente…').start();
1872
+ const out = await doc.run({
1873
+ cfg, version: pkg.version, base: base(),
1874
+ // backend no ar? 401 conta como "respondeu" (só não autenticado).
1875
+ pingBackend: async () => { try { await api('/api/auth/check', { token: cfg.token, timeoutMs: 8000 }); return true; } catch (e) { return (e && e.status === 401); } },
1876
+ // última versão publicada no npm (best-effort).
1877
+ latestVersion: async () => { try { const r = await fetch('https://registry.npmjs.org/terminal-smart-cli/latest', { signal: AbortSignal.timeout(8000) }); const j = await r.json(); return j && j.version; } catch (_) { return null; } },
1878
+ });
1879
+ sp.stop();
1880
+ if (JSON_OUT) { console.log(JSON.stringify(out)); process.exit(out.summary.healthy ? 0 : 1); }
1881
+ const ic = { ok: C.ok('✔'), warn: C.warn('⚠'), fail: C.err('✗') };
1882
+ const lines = [C.bold(en ? 'Environment check' : 'Diagnóstico do ambiente'), ''];
1883
+ for (const r of out.results) {
1884
+ lines.push(' ' + (ic[r.level] || '·') + ' ' + C.bold(r.nome.padEnd(16)) + C.dim(r.detail || ''));
1885
+ if (r.dica) lines.push(' ' + C.dim('→ ' + r.dica));
1886
+ }
1887
+ const s = out.summary;
1888
+ lines.push('', (s.healthy ? C.ok : C.warn)((s.healthy ? (en ? 'All good' : 'Tudo certo') : (en ? 'Attention needed' : 'Precisa de atenção')) + C.dim(' · ' + s.ok + ' ok · ' + s.warn + ' ' + (en ? 'warn' : 'avisos') + ' · ' + s.fail + ' ' + (en ? 'fail' : 'falhas'))));
1889
+ console.log('\n' + ui.box(lines, { title: 'ts doctor' }));
1890
+ process.exit(s.healthy ? 0 : 1);
1891
+ }
1892
+
1737
1893
  // ── ts perfil — personas com COFRE DE MEMÓRIA isolado (Mempalace/Hermes) ──────
1738
1894
  function perfilCmd(args) {
1739
1895
  const en = (cfg.lang === 'en');
@@ -1997,6 +2153,9 @@ function recallCmd(args) {
1997
2153
  case 'buscar': case 'search': case 'procurar': return buscarCmd(POS.slice(1));
1998
2154
  case 'perfil': case 'persona': case 'profile': return perfilCmd(POS.slice(1));
1999
2155
  case 'verificar': case 'verify': case 'provar': return verificarCmd(POS.slice(1));
2156
+ case 'runbook': case 'runbooks': case 'procedimento': return runbookCmd(POS.slice(1));
2157
+ case 'trilha': case 'trail': case 'auditoria': return trilhaCmd();
2158
+ case 'doctor': case 'diagnostico-ambiente': case 'checkup': return doctorCmd();
2000
2159
  case 'meta': case 'missao': case 'mission': return metaCmd();
2001
2160
  case 'runs': return runsCmd();
2002
2161
  case 'status': return statusCmd(POS[1]);
package/lib/agent.js CHANGED
@@ -658,6 +658,9 @@ async function run(task, opts = {}) {
658
658
  _toolErrs.push({ tool: name, class: _tr.errorClass, retryable: _tr.retryable, evidence: _tr.evidence });
659
659
  // LEDGER DE ERROS (Evolve 3): registro determinístico, zero IA.
660
660
  try { require('./memoria').logErro(confineDir || cwd, name, String(result.erro || result.stderr || ('exit ' + result.codigo))); } catch (_) {}
661
+ // RECOVERY ENGINE: erro de classe CONHECIDA → injeta a estratégia de conserto no
662
+ // resultado (o modelo aplica o padrão DevOps em vez de chutar/entrar em loop).
663
+ try { const _rh = require('./recovery').recoveryHint(_tr.errorClass); if (_rh && result && typeof result === 'object') result = Object.assign({}, result, { _recovery: _rh }); } catch (_) {}
661
664
  }
662
665
  // HOOK PostToolUse (determinístico): feedback (ex: lint/format) vai pro modelo ver.
663
666
  if (_hooks._any) {
package/lib/doctor.js ADDED
@@ -0,0 +1,82 @@
1
+ // ts doctor — AUTO-DIAGNÓSTICO do ambiente do CLI (confiabilidade visível aplicada a si mesmo).
2
+ // Checa o que o ts precisa pra funcionar (Node, login, gateway, versão) e o que habilita
3
+ // recursos opcionais (git, Chrome p/ --navegador, yt-dlp p/ ts video, monolith p/ ts arquivar).
4
+ // Cada check devolve { nome, level: 'ok'|'warn'|'fail', detail, dica? }. Puro onde dá (testável);
5
+ // as checagens de rede degradam gracioso (nunca lançam).
6
+ 'use strict';
7
+ const os = require('os');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execFileSync } = require('child_process');
11
+
12
+ // ── helpers PUROS (testáveis) ────────────────────────────────────────────────
13
+ function _nodeMajor(v) { const m = String(v || '').replace(/^v/, '').split('.'); return Number(m[0]) || 0; }
14
+ function _nodeOk(v) { return _nodeMajor(v) >= 18; }
15
+ // estado de autenticação a partir do config carregado
16
+ function _authLevel(cfg) {
17
+ if (!cfg || !cfg.token) return { level: 'fail', detail: 'não logado', dica: 'rode: ts login' };
18
+ return { level: 'ok', detail: 'logado' + (cfg.username ? ' como ' + cfg.username : '') + (cfg.plan ? ' · plano ' + cfg.plan : '') };
19
+ }
20
+ // compara versão instalada × última do npm → ok / warn (desatualizado)
21
+ function _versionLevel(current, latest) {
22
+ if (!latest) return { level: 'warn', detail: 'v' + current + ' (não deu pra checar o npm)' };
23
+ if (current === latest) return { level: 'ok', detail: 'v' + current + ' (última)' };
24
+ return { level: 'warn', detail: 'v' + current + ' — última é v' + latest, dica: 'atualize: npm i -g terminal-smart-cli' };
25
+ }
26
+ // resumo agregado
27
+ function _summary(results) {
28
+ return { total: results.length, ok: results.filter(r => r.level === 'ok').length,
29
+ warn: results.filter(r => r.level === 'warn').length, fail: results.filter(r => r.level === 'fail').length,
30
+ healthy: results.every(r => r.level !== 'fail') };
31
+ }
32
+
33
+ // binário presente? (cross-platform, best-effort — nunca lança)
34
+ function hasBin(name) {
35
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
36
+ try { const out = String(execFileSync(cmd, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 })).trim(); return out ? out.split(/\r?\n/)[0] : null; }
37
+ catch (_) { return null; }
38
+ }
39
+
40
+ // ── checks ────────────────────────────────────────────────────────────────────
41
+ // opcionais: quando ausentes → warn (recurso desligado), não fail.
42
+ const OPTIONAL_BINS = [
43
+ { bin: 'git', para: 'worktrees, versionamento', alt: [] },
44
+ { bin: 'yt-dlp', para: 'ts video (transcrição)', alt: ['youtube-dl'] },
45
+ { bin: 'monolith', para: 'ts arquivar (página→HTML)', alt: [] },
46
+ ];
47
+
48
+ // run(opts): { cfg, version, base, api } — api opcional (fetch do backend/npm). Sempre resolve.
49
+ async function run(opts = {}) {
50
+ const cfg = opts.cfg || {};
51
+ const results = [];
52
+
53
+ // 1) Node
54
+ const nv = process.version;
55
+ results.push({ nome: 'Node.js', level: _nodeOk(nv) ? 'ok' : 'fail', detail: nv + (_nodeOk(nv) ? '' : ' (precisa 18+)'), dica: _nodeOk(nv) ? undefined : 'instale Node 18 ou superior' });
56
+
57
+ // 2) config ~/.ts + login
58
+ const auth = _authLevel(cfg); results.push(Object.assign({ nome: 'Login' }, auth));
59
+ const dir = path.join(os.homedir(), '.ts');
60
+ results.push({ nome: 'Config (~/.ts)', level: fs.existsSync(dir) ? 'ok' : 'warn', detail: fs.existsSync(dir) ? dir : 'ainda não criado (normal se nunca rodou nada)' });
61
+
62
+ // 3) gateway/backend (rede, best-effort)
63
+ if (typeof opts.pingBackend === 'function') {
64
+ try { const ok = await opts.pingBackend(); results.push({ nome: 'Backend', level: ok ? 'ok' : 'warn', detail: ok ? (opts.base || 'acessível') : 'sem resposta', dica: ok ? undefined : 'cheque a conexão / status em terminalsmart.com.br' }); }
65
+ catch (_) { results.push({ nome: 'Backend', level: 'warn', detail: 'sem resposta' }); }
66
+ }
67
+
68
+ // 4) versão vs npm latest (rede, best-effort)
69
+ let latest = null;
70
+ if (typeof opts.latestVersion === 'function') { try { latest = await opts.latestVersion(); } catch (_) {} }
71
+ results.push(Object.assign({ nome: 'Versão' }, _versionLevel(opts.version || '?', latest)));
72
+
73
+ // 5) deps opcionais
74
+ for (const o of OPTIONAL_BINS) {
75
+ let found = hasBin(o.bin); for (const a of o.alt) if (!found) found = hasBin(a);
76
+ results.push({ nome: o.bin, level: found ? 'ok' : 'warn', detail: found ? found : ('ausente — ' + o.para + ' fica indisponível'), dica: found ? undefined : 'opcional; instale se for usar ' + o.para });
77
+ }
78
+
79
+ return { results, summary: _summary(results) };
80
+ }
81
+
82
+ module.exports = { run, hasBin, OPTIONAL_BINS, _test: { _nodeMajor, _nodeOk, _authLevel, _versionLevel, _summary } };
package/lib/eval.js CHANGED
@@ -239,5 +239,38 @@ async function runSuite(suite, opts = {}) {
239
239
  return { summary, results };
240
240
  }
241
241
 
242
- module.exports = { loadSuite, normCases, runSuite, judge, smokeSuite, EXAMPLE,
243
- _test: { _diff, _snap, _extractJson, normCases } };
242
+ // ── MODEL LAB: roda a MESMA suíte em VÁRIOS executores e compara (bake-off automático) ──
243
+ // Automatiza o "veredito modelo X" que antes era manual. O JUIZ é fixo (mesmo pra todos =
244
+ // comparação justa); só o EXECUTOR muda. Ranqueia por qualidade VERIFICADA e depois por custo.
245
+ function _rankModels(rows) {
246
+ // ordena: mais casos aprovados (passRate) → maior score médio → MENOR custo → mais rápido.
247
+ const sorted = [...rows].sort((a, b) =>
248
+ (b.passRate - a.passRate) || (b.avgScore - a.avgScore) || (a.credits - b.credits) || (a.ms - b.ms));
249
+ return { ranked: sorted, winner: sorted[0] ? sorted[0].model : null };
250
+ }
251
+
252
+ async function runMatrix(suite, opts = {}) {
253
+ const models = (opts.models || []).filter(Boolean);
254
+ if (!models.length) throw new Error('runMatrix precisa de opts.models (lista de executores)');
255
+ const onModel = opts.onModel || (() => {});
256
+ const rows = [];
257
+ for (const model of models) {
258
+ onModel({ model, phase: 'start' });
259
+ let sum;
260
+ try {
261
+ const r = await runSuite(suite, Object.assign({}, opts, { model, onCase: opts.onCase }));
262
+ sum = r.summary;
263
+ } catch (e) {
264
+ if (e && (e.code === 'no_credits' || e.status === 402)) throw e; // teto → aborta o bake-off
265
+ sum = { name: suite.name, total: suite.cases.length, passed: 0, failed: suite.cases.length, passRate: 0, avgScore: 0, credits: 0, judgeTokens: 0, ms: 0, error: String((e && e.message) || e).slice(0, 160) };
266
+ }
267
+ const row = { model, passRate: sum.passRate, avgScore: sum.avgScore, passed: sum.passed, total: sum.total, credits: sum.credits, judgeTokens: sum.judgeTokens, ms: sum.ms, error: sum.error || null };
268
+ rows.push(row);
269
+ onModel({ model, phase: 'done', row });
270
+ }
271
+ const rank = _rankModels(rows);
272
+ return { suite: suite.name, cases: suite.cases.length, rows, ranked: rank.ranked, winner: rank.winner };
273
+ }
274
+
275
+ module.exports = { loadSuite, normCases, runSuite, runMatrix, judge, smokeSuite, EXAMPLE,
276
+ _test: { _diff, _snap, _extractJson, normCases, _rankModels } };
package/lib/i18n.js CHANGED
@@ -46,8 +46,11 @@ const STR = {
46
46
  ['ts meta "..." --criterios prova.json', 'só declara "verificada" se os critérios executáveis (verify) passarem de verdade'],
47
47
  ['ts meta "..." --provar', 'auto-deriva a prova da stack (roda os testes do projeto) — sem escrever json'],
48
48
  ['ts meta --status', 'estado da missão deste diretório'],
49
+ ['ts trilha', 'TIMELINE AUDITÁVEL da missão: checklist + rodadas + gates + PROVA + veredito (por que está pronta)'],
49
50
  ['ts eval suite.json', 'avalia o agente numa suíte de casos (juiz de IA + nota)'],
51
+ ['ts eval suite.json --modelos a,b,c', 'MODEL LAB: bake-off automático — mesma suíte em cada modelo + placar (qualidade→custo)'],
50
52
  ['ts verificar --cmd "npm test" --url ... --porta N', 'PROVA determinística de conclusão (build/http/porta/arquivo); exit 1 pra CI'],
53
+ ['ts runbook rodar <nome>', 'procedimento DevOps reexecutável: roda os passos (destrutivo pede OK) e PROVA o resultado'],
51
54
  ['ts acp', 'servidor Agent Client Protocol (conecta o ts a editores tipo Zed)'],
52
55
  ['ts mcp', 'servidores MCP: as ferramentas deles entram no agente'],
53
56
  ] },
@@ -61,6 +64,7 @@ const STR = {
61
64
  ['ts login · ts logout', 'conecta / desconecta este terminal'],
62
65
  ['ts uso', 'créditos e consumo do mês'],
63
66
  ['ts quem', 'conta conectada'],
67
+ ['ts doctor', 'diagnóstico do ambiente (Node, login, gateway, versão, deps opcionais)'],
64
68
  ['ts idioma pt|en', 'idioma (padrão pt-BR)'],
65
69
  ] },
66
70
  ],
package/lib/meta.js CHANGED
@@ -977,7 +977,11 @@ function _tolerantJson(text) {
977
977
 
978
978
  const CHECKLIST_SYS = 'Quebre o OBJETIVO em itens VERIFICÁVEIS de entrega (3 a 15). Cada item deve ser '
979
979
  + 'concreto e conferível (arquivo criado, comando executado com sucesso, seção escrita) — nada vago tipo "melhorar". '
980
- + 'Responda APENAS JSON: {"itens":[{"id":"i1","desc":"..."},...]}. Ordene na sequência lógica de execução.';
980
+ + 'Responda APENAS JSON: {"itens":[{"id":"i1","desc":"..."},...],"criterios":[...]}. Ordene os itens na sequência lógica de execução.\n'
981
+ + 'Em "criterios" (OPCIONAL, 0 a 6), proponha PROVAS EXECUTÁVEIS de que o objetivo foi cumprido — que uma MÁQUINA cheque sem julgamento. '
982
+ + 'Tipos válidos: {"type":"command","cmd":"npm test","exit":0} · {"type":"file_exists","path":"src/x.js"} · {"type":"file_contains","path":"server.js","text":"/api/login"} · {"type":"http","url":"http://localhost:3000/health","status":200} · {"type":"port","port":3000}. '
983
+ + 'PREFIRA file_exists/file_contains/command (seguros e baratos); só use http/port se o objetivo é claramente subir um servidor. '
984
+ + 'NÃO invente critério que possa FALHAR mesmo com a missão pronta. Na dúvida, "criterios":[].';
981
985
 
982
986
  // Pergunta BINÁRIA sobre o item alvo (modelo pequeno erra listas, mas acerta sim/não).
983
987
  const MARK_SYS = 'Você é o VERIFICADOR de uma missão. Recebe UM item do checklist e o RESULTADO da rodada que tentou executá-lo. '
@@ -987,11 +991,14 @@ const MARK_SYS = 'Você é o VERIFICADOR de uma missão. Recebe UM item do check
987
991
  const fmtChecklist = (itens) => itens.map(it => `[${it.passes ? 'x' : it.blocked ? '!' : ' '}] (${it.id}) ${it.desc}`).join('\n');
988
992
 
989
993
  async function makeChecklist(goal, token) {
990
- const r = await _llmJson({ token, system: CHECKLIST_SYS, user: 'OBJETIVO:\n' + goal, maxTokens: 1600 });
994
+ const r = await _llmJson({ token, system: CHECKLIST_SYS, user: 'OBJETIVO:\n' + goal, maxTokens: 1800 });
991
995
  const itens = (Array.isArray(r.json?.itens) ? r.json.itens : []).slice(0, 15)
992
996
  .map((it, i) => ({ id: String(it.id || 'i' + (i + 1)), desc: String(it.desc || '').slice(0, 300), passes: false, attempts: 0 }))
993
997
  .filter(it => it.desc);
994
- return { itens: itens.length ? itens : [{ id: 'i1', desc: String(goal).slice(0, 300), passes: false, attempts: 0 }], credits: r.credits };
998
+ // Critérios propostos pelo planner VALIDADOS pelo compilador (formato conhecido; malformados caem fora).
999
+ let criteria = [];
1000
+ try { criteria = require('./verify').compileCriteria(r.json?.criterios || []).criteria.slice(0, 6); } catch (_) {}
1001
+ return { itens: itens.length ? itens : [{ id: 'i1', desc: String(goal).slice(0, 300), passes: false, attempts: 0 }], criteria, credits: r.credits };
995
1002
  }
996
1003
 
997
1004
  /**
@@ -1016,7 +1023,7 @@ async function _checkCriteria(st, dir) {
1016
1023
  }
1017
1024
 
1018
1025
  async function run(goal, opts = {}) {
1019
- 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 } = 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;
1020
1027
  const onChecklist = opts.onChecklist || (() => {});
1021
1028
  const onRound = opts.onRound || (() => {});
1022
1029
  const onRoundDone = opts.onRoundDone || (() => {});
@@ -1056,8 +1063,15 @@ async function run(goal, opts = {}) {
1056
1063
  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; }
1057
1064
  }
1058
1065
  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
+ // 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, só guarda o que o planner propôs (plannedCriteria) como referência, sem bloquear.
1069
+ const _V = require('./verify');
1070
+ let _criteria = [], _planned = mk.criteria || [];
1071
+ if (Array.isArray(opts.criteria) && opts.criteria.length) _criteria = opts.criteria;
1072
+ else if (prove) _criteria = _V.compileCriteria([..._V.deriveFromStack(dir), ..._planned]).criteria.slice(0, 8);
1059
1073
  st = { goal: String(goal).slice(0, 4000), mockup: opts.mockup || null, archBrief, feasBrief, checklist: mk.itens, rounds: [], creditsSpent: (mk.credits || 0) + designCred + archCred + feasCred,
1060
- budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, criteria: Array.isArray(opts.criteria) ? opts.criteria : [], created_at: new Date().toISOString() };
1074
+ budget, maxRounds, status: 'running', model: model || null, designBrief: designBrief || null, criteria: _criteria, plannedCriteria: _planned, created_at: new Date().toISOString() };
1061
1075
  save(st, dir);
1062
1076
  } else {
1063
1077
  st.status = 'running';
@@ -1396,4 +1410,32 @@ function verificationLabel(ver, lang) {
1396
1410
  }
1397
1411
  }
1398
1412
 
1399
- module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, verificationLabel, _checkCriteria };
1413
+ // TRILHA AUDITÁVEL (Evidence Ledger da missão): responde "por que está concluída e como
1414
+ // cheguei aqui" a partir do estado salvo — SEM confiar na narrativa. Função PURA/testável.
1415
+ function trailSummary(st, lang) {
1416
+ const en = lang === 'en';
1417
+ const cl = Array.isArray(st.checklist) ? st.checklist : [];
1418
+ const rounds = Array.isArray(st.rounds) ? st.rounds : [];
1419
+ const ver = verificationLabel(st.verification, lang);
1420
+ const done = cl.filter(i => i.passes).length;
1421
+ const mins = (st.created_at && st.finished_at) ? Math.round((new Date(st.finished_at) - new Date(st.created_at)) / 60000) : null;
1422
+ let verdict;
1423
+ if (st.status !== 'done') verdict = (en ? 'Not finished — status: ' : 'Não concluída — status: ') + st.status + (st.pause_reason ? ' (' + st.pause_reason + ')' : '');
1424
+ else if (st.verification === 'verified') verdict = en ? 'Done and VERIFIED: gates and criteria passed.' : 'Concluída e VERIFICADA: gates e critérios passaram.';
1425
+ else if (st.verification === 'unverified') verdict = en ? 'Ended WITHOUT full proof (fix limit / gate) — check before relying.' : 'Encerrada SEM prova completa (teto de correções / gate) — confira antes de confiar.';
1426
+ else if (st.verification === 'built_not_run') verdict = en ? 'Built, but execution was NOT verified.' : 'Compilou, mas a execução NÃO foi verificada.';
1427
+ else verdict = en ? 'Done (no build/run gate applied for this task).' : 'Concluída (sem gate de build/execução p/ esta tarefa).';
1428
+ return {
1429
+ goal: st.goal, status: st.status, verification: ver,
1430
+ checklist: cl.map(i => ({ id: i.id, desc: i.desc, state: i.passes ? 'done' : i.blocked ? 'blocked' : 'pending' })),
1431
+ progress: { done, total: cl.length },
1432
+ rounds: rounds.map((r, i) => ({ n: i + 1, item: r.item, steps: r.steps || 0, credits: r.credits || 0, snippet: String(r.result || '').replace(/\s+/g, ' ').slice(0, 120) })),
1433
+ gates: { build: !!st.buildVerified, run: !!st.runVerified, visual: !!st.visualOk },
1434
+ proof: st.verifyReport && st.verifyReport.total ? { passed: st.verifyReport.passed, total: st.verifyReport.total, results: st.verifyReport.results } : null,
1435
+ cost: { credits: st.creditsSpent || 0, rounds: rounds.length, escalations: st.escalations || 0, minutes: mins },
1436
+ note: st.runNote || null,
1437
+ verdict,
1438
+ };
1439
+ }
1440
+
1441
+ module.exports = { webRunGate, run, load, notify, stateFile, detectBuild, ensureToolchain, runBuild, escalate, designPhase, archPhase, looksVisual, looksComplex, runApp, visualGate, ensureEmulator, _projKind, _llmVision, verificationLabel, _checkCriteria, trailSummary };
@@ -0,0 +1,72 @@
1
+ // RECOVERY ENGINE — conhecimento de conserto codificado POR CLASSE de erro.
2
+ // Insight das análises externas (GPT): "cada classe de erro deve ter sondas e estratégias
3
+ // conhecidas; só escalar pro modelo forte após 2 estratégias falharem". Aqui, quando uma
4
+ // ferramenta falha com classe CONHECIDA (core.classifyToolResult), injetamos a estratégia
5
+ // certa no resultado que o modelo lê — ele aplica o conserto DevOps padrão em vez de chutar
6
+ // (ex.: porta ocupada → matar SÓ o PID, nunca por nome; que era justo o loop do incidente).
7
+ 'use strict';
8
+
9
+ // Cada classe: como DIAGNOSTICAR (sonda só-leitura) e como CONSERTAR (ação segura e cirúrgica).
10
+ const STRATEGIES = {
11
+ port_in_use: {
12
+ diag: 'ache o PID que segura a porta — Linux: `ss -ltnp | grep :<porta>` ou `lsof -i:<porta>`; Windows: `netstat -ano | findstr :<porta>`',
13
+ fix: 'mate SÓ aquele PID (`kill <pid>` / `taskkill /F /PID <pid>`) — NUNCA por nome (mata processos demais, inclusive o seu). OU suba o serviço em outra porta.',
14
+ },
15
+ missing_dep: {
16
+ diag: 'confirme o nome EXATO do módulo/binário que faltou na mensagem de erro',
17
+ fix: 'instale-o: Node `npm i <pkg>` · Python `pip install <pkg>` · sistema `sudo DEBIAN_FRONTEND=noninteractive apt-get install -y <pkg>` (timeout_s alto). Depois re-tente o comando original.',
18
+ },
19
+ package_lock: {
20
+ diag: 'veja qual processo segura o lock (apt/dpkg/npm) — NÃO o mate no meio',
21
+ fix: 'espere o processo dono do lock terminar e re-tente. Se o dpkg foi interrompido antes: `sudo dpkg --configure -a`. Nunca apague o lockfile na força.',
22
+ },
23
+ permission_denied: {
24
+ diag: 'cheque dono e modo do alvo: `ls -l <caminho>` (Linux) / atributos do arquivo',
25
+ fix: 'ajuste o dono/modo do arquivo ESPECÍFICO (`chown user:grp <arquivo>` / `chmod u+w <arquivo>`) ou use sudo só onde apropriado. NUNCA `chmod 777` nem recursivo na raiz.',
26
+ },
27
+ not_found: {
28
+ diag: 'confirme o caminho/rota EXATO com `listar_diretorio`/`buscar_arquivos` (maiúsculas e plural contam)',
29
+ fix: 'crie o arquivo/rota que falta OU corrija a referência pro caminho certo. Não invente o caminho — verifique antes.',
30
+ },
31
+ network: {
32
+ diag: 'teste a conectividade: `curl -I <url>` / `ping <host>` / cheque DNS e se o serviço-alvo está no ar',
33
+ fix: 're-tente com backoff; confirme URL/porta/firewall. Se o alvo estiver fora, reporte isso ao usuário em vez de insistir.',
34
+ },
35
+ timeout: {
36
+ diag: 'o comando demorou mais que o limite — veja se é build/install pesado ou algo travado',
37
+ fix: 'aumente o timeout (`timeout_s` maior) OU quebre em passos menores. Para build/install use timeout longo (300s+).',
38
+ },
39
+ compile_error: {
40
+ diag: 'leia a mensagem: ela dá ARQUIVO e LINHA do erro',
41
+ fix: 'vá DIRETO à linha (`buscar_codigo`/`ler_arquivo`) e conserte só o trecho (`editar_arquivo` por âncora) — não reescreva o arquivo inteiro. Confira sintaxe/tipos ao redor.',
42
+ },
43
+ test_failed: {
44
+ diag: 'rode o teste específico que falhou e leia o assert (esperado × obtido)',
45
+ fix: 'corrija a CAUSA no código (não o teste, a menos que o teste esteja claramente errado). Re-rode só aquele teste até passar.',
46
+ },
47
+ auth: {
48
+ diag: 'cheque token/credencial/variável de ambiente exigida (sem logar o segredo)',
49
+ fix: 'corrija a credencial/escopo. Se depende de um segredo do usuário, PARE e peça a ele — não chute nem exponha valores.',
50
+ },
51
+ git_conflict: {
52
+ diag: 'veja os arquivos em conflito: `git status`',
53
+ fix: 'abra cada arquivo, resolva os marcadores `<<<<<<<`/`=======`/`>>>>>>>`, `git add` e conclua o merge/rebase. Não faça `push --force`.',
54
+ },
55
+ invalid_schema: {
56
+ diag: 'valide a estrutura (JSON/YAML): vírgulas, aspas, chaves balanceadas',
57
+ fix: 'corrija o campo apontado; valide de novo antes de reusar.',
58
+ },
59
+ };
60
+
61
+ const RECOVERABLE = Object.keys(STRATEGIES); // classes com estratégia conhecida (unknown/blocked não têm)
62
+
63
+ function strategiesFor(errorClass) { return STRATEGIES[errorClass] || null; }
64
+
65
+ // Texto acionável pra INJETAR no resultado da ferramenta que o modelo lê. '' se classe sem estratégia.
66
+ function recoveryHint(errorClass) {
67
+ const s = STRATEGIES[errorClass];
68
+ if (!s) return '';
69
+ return `ESTRATÉGIA CONHECIDA (${errorClass}) — não chute:\n1) DIAGNOSTIQUE: ${s.diag}\n2) CONSERTO: ${s.fix}`;
70
+ }
71
+
72
+ module.exports = { STRATEGIES, RECOVERABLE, strategiesFor, recoveryHint };
package/lib/runbook.js ADDED
@@ -0,0 +1,93 @@
1
+ // RUNBOOKS EXECUTÁVEIS — procedimentos DevOps reexecutáveis e AUTO-VERIFICÁVEIS.
2
+ // Insight das análises externas (GPT): "transforme uma execução aprovada e bem-sucedida em
3
+ // runbook tipado; na próxima vez o harness roda determinístico e só chama IA se houver desvio".
4
+ // Um runbook = PASSOS (comandos) + CRITÉRIOS (verify). Roda os passos (destrutivo pede
5
+ // aprovação; auto-destrutivo é recusado na hora) e PROVA o resultado com os verificadores.
6
+ // Zero IA por padrão. Ex.: "deploy", "backup do banco", "configurar nginx", "renovar SSL".
7
+ 'use strict';
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const { execSync } = require('child_process');
12
+ const core = require('./core');
13
+ const tools = require('./tools');
14
+ const verify = require('./verify');
15
+
16
+ const DIR = path.join(os.homedir(), '.ts', 'runbooks');
17
+ const file = (nome) => path.join(DIR, _slug(nome) + '.json');
18
+ function _slug(n) { return String(n || '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'runbook'; }
19
+
20
+ // ── persistência ─────────────────────────────────────────────────────────────
21
+ function list() {
22
+ try { return fs.readdirSync(DIR).filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, '')); } catch (_) { return []; }
23
+ }
24
+ function load(nome) { try { return JSON.parse(fs.readFileSync(file(nome), 'utf8')); } catch (_) { return null; } }
25
+ function save(nome, rb) {
26
+ fs.mkdirSync(DIR, { recursive: true });
27
+ const obj = Object.assign({}, rb, { nome: _slug(nome) });
28
+ fs.writeFileSync(file(nome), JSON.stringify(obj, null, 2));
29
+ return obj;
30
+ }
31
+ function remove(nome) { try { fs.unlinkSync(file(nome)); return true; } catch (_) { return false; } }
32
+
33
+ // template inicial pro `ts runbook novo`
34
+ function template(nome) {
35
+ return {
36
+ nome: _slug(nome), descricao: '',
37
+ passos: [{ desc: 'exemplo — troque pelo comando real', cmd: 'echo hello', remoto: '' }],
38
+ criterios: [{ type: 'command', cmd: 'echo ok', exit: 0, label: 'prova de que deu certo' }],
39
+ };
40
+ }
41
+
42
+ // ── validação (determinística) ────────────────────────────────────────────────
43
+ function validate(rb) {
44
+ const erros = [];
45
+ if (!rb || typeof rb !== 'object') return { ok: false, erros: ['runbook vazio/ inválido'] };
46
+ const passos = Array.isArray(rb.passos) ? rb.passos : [];
47
+ if (!passos.length) erros.push('sem passos');
48
+ passos.forEach((p, i) => { if (!p || !String(p.cmd || '').trim()) erros.push('passo ' + (i + 1) + ' sem "cmd"'); });
49
+ const crit = Array.isArray(rb.criterios) ? rb.criterios : [];
50
+ const comp = verify.compileCriteria(crit);
51
+ if (crit.length && !comp.criteria.length) erros.push('nenhum critério válido (' + crit.length + ' descartado[s])');
52
+ return { ok: erros.length === 0, erros, criterios: comp.criteria, droppedCriterios: comp.dropped.length };
53
+ }
54
+
55
+ // ── execução ──────────────────────────────────────────────────────────────────
56
+ // Roda 1 comando (local ou ssh remoto), captura code/saída — nunca lança.
57
+ function _run(cmd, remoto) {
58
+ const full = remoto ? `${remoto} ${JSON.stringify(cmd)}` : cmd;
59
+ try { const out = execSync(full, { encoding: 'utf8', timeout: 600000, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }); return { code: 0, out }; }
60
+ catch (e) { return { code: typeof e.status === 'number' ? e.status : 1, out: String((e.stdout || '') + (e.stderr || '') || e.message || '').slice(0, 4000) }; }
61
+ }
62
+
63
+ // opts: { cwd, onStep({idx,total,desc,cmd,phase,code,ok}), askApprove(cmd)->bool, yes }
64
+ // Passo destrutivo (core.isDestructive) → askApprove; auto-destrutivo local → RECUSA na hora.
65
+ // Um passo que falha (code≠0) INTERROMPE a execução (não segue com o ambiente meio-quebrado).
66
+ // No fim, roda os CRITÉRIOS (verify) e devolve o veredito honesto.
67
+ async function run(rb, opts = {}) {
68
+ const cwd = opts.cwd || process.cwd();
69
+ const onStep = opts.onStep || (() => {});
70
+ const askApprove = opts.askApprove || (async () => false);
71
+ const v = validate(rb);
72
+ if (!v.ok) return { ok: false, erro: 'runbook inválido: ' + v.erros.join('; '), passos: [], verify: null };
73
+
74
+ const passos = rb.passos, total = passos.length, done = [];
75
+ for (let i = 0; i < total; i++) {
76
+ const p = passos[i]; const cmd = String(p.cmd).trim(); const remoto = p.remoto || '';
77
+ onStep({ idx: i, total, desc: p.desc || '', cmd, phase: 'start' });
78
+ // auto-destrutivo LOCAL (mata a própria máquina/missão) → recusa imediata, sem aprovação
79
+ if (!remoto) { const sd = tools.selfDestructiveReason(cmd, { cwd }); if (sd) { const r = { idx: i, cmd, ok: false, code: -1, motivo: sd }; done.push(r); onStep({ ...r, phase: 'refused' }); return { ok: false, erro: 'passo ' + (i + 1) + ' recusado: ' + sd, passos: done, verify: null }; } }
80
+ // destrutivo → precisa de aprovação (a menos de --yes, que o chamador decide via askApprove)
81
+ if (core.isDestructive(cmd)) { const okA = await askApprove(cmd); if (!okA) { const r = { idx: i, cmd, ok: false, code: -1, motivo: 'não aprovado' }; done.push(r); onStep({ ...r, phase: 'denied' }); return { ok: false, erro: 'passo ' + (i + 1) + ' (destrutivo) não aprovado', passos: done, verify: null }; } }
82
+ const res = _run(cmd, remoto);
83
+ const r = { idx: i, cmd, ok: res.code === 0, code: res.code, out: String(res.out).slice(-300) };
84
+ done.push(r); onStep({ ...r, phase: 'done' });
85
+ if (!r.ok) return { ok: false, erro: 'passo ' + (i + 1) + ' falhou (exit ' + res.code + ')', passos: done, verify: null };
86
+ }
87
+ // PROVA: roda os critérios de verificação
88
+ let rep = null;
89
+ if (v.criterios.length) { onStep({ phase: 'verify', total: v.criterios.length }); rep = await verify.runAll(v.criterios, { cwd }); }
90
+ return { ok: !rep || rep.allOk, passos: done, verify: rep };
91
+ }
92
+
93
+ module.exports = { list, load, save, remove, template, validate, run, DIR, _slug };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.67.0",
3
+ "version": "0.73.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"