terminal-smart-cli 0.97.28 → 0.97.30

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/lib/core.js CHANGED
@@ -136,10 +136,10 @@ const EVENT_TYPES = ['system', 'tool', 'assistant', 'result'];
136
136
  // raw (retrocompatível). Classe de erro alimenta o Recovery Engine e o ledger; `ok:false`
137
137
  // deve barrar a marcação de sucesso a montante (meta/orquestrador).
138
138
  const ERROR_CLASSES = ['permission_denied', 'not_found', 'port_in_use', 'missing_dep', 'package_lock',
139
- 'timeout', 'compile_error', 'test_failed', 'network', 'auth', 'git_conflict', 'invalid_schema',
140
- 'blocked', 'unknown'];
139
+ 'timeout', 'compile_error', 'test_failed', 'network', 'auth', 'git_conflict', 'invalid_schema', 'resource_locked',
140
+ 'invalid_command', 'invalid_workdir', 'blocked', 'unknown'];
141
141
  // classes transitórias → vale re-tentar (mesma abordagem); as demais exigem mudar de abordagem.
142
- const RETRYABLE_CLASSES = new Set(['network', 'timeout', 'package_lock', 'port_in_use']);
142
+ const RETRYABLE_CLASSES = new Set(['network', 'timeout', 'package_lock', 'resource_locked', 'port_in_use']);
143
143
 
144
144
  function classifyError(msg) {
145
145
  const s = String(msg || '').toLowerCase();
@@ -147,8 +147,15 @@ function classifyError(msg) {
147
147
  if (/\bbloquead|\brecus|read-only|só-leitura|loop detectad|\bloop:|aprova(ção|r) neg|negou.*aprova/i.test(s)) return 'blocked';
148
148
  if (/eacces|permission denied|access is denied|operation not permitted|acesso negado|permissão negada/i.test(s)) return 'permission_denied';
149
149
  if (/could not get lock|dpkg was interrupted|resource temporarily unavailable.*lock|another (app|process) .*lock|unable to (acquire|lock)/i.test(s)) return 'package_lock';
150
+ if (/arquivo[_ ]ocupado|file[_ ]locked|resource[_ ]locked|another terminal smart process.*(?:editing|altering)|eexist.*\.lock/i.test(s)) return 'resource_locked';
150
151
  if (/eaddrinuse|address already in use|porta .*(ocupad|em uso)|already running/i.test(s)) return 'port_in_use';
151
- if (/cannot find module|command not found|not recognized as|modulenotfounderror|no module named|no such command|is not installed|não (foi )?encontrad[oa] o comando/i.test(s)) return 'missing_dep';
152
+ // Binário/comando inválido pede dialeto/sintaxe correto; módulo ausente pode justificar instalar pacote.
153
+ if (/cannot find module|modulenotfounderror|no module named|is not installed/i.test(s)) return 'missing_dep';
154
+ // cmd.exe pode devolver texto OEM/UTF-8 corrompido ("não" vira "n�o").
155
+ // A classe precisa sobreviver a isso: o recovery correto depende dela e não
156
+ // pode cair em unknown só por causa do encoding do terminal.
157
+ if (/is not recognized as (an )?(internal|the name of)|command not found|no such command|n[^a-z\s]{0,3}o (?:foi )?encontrad[oa] o comando|n[^a-z\s]{0,3}o [^\r\n]{0,80}reconhecid[oa]|invalid switch|unrecognized option|syntax of the command is incorrect|unexpectedtoken|parsererror|terminator.*string/i.test(s)) return 'invalid_command';
158
+ if (/working directory|invalid directory|directory name is invalid|system cannot find the path specified|n[^a-z\s]{0,3}o pode encontrar o caminho especificado|enoent.*(?:spawn|chdir)|chdir.*enoent/i.test(s)) return 'invalid_workdir';
152
159
  if (/enoent|no such file|não encontrad|not found|arquivo inexistente|404/i.test(s)) return 'not_found';
153
160
  if (/etimedout|timed out|timeout|excedeu o tempo/i.test(s)) return 'timeout';
154
161
  if (/econnrefused|enotfound|getaddrinfo|network|connection refused|conexão recusada|conn|dns/i.test(s)) return 'network';
package/lib/eval.js CHANGED
@@ -152,7 +152,7 @@ async function judge({ key, baseUrl, model, caseObj, evidence }) {
152
152
  // opts: { token, lang, model (executor), judgeModel, onCase({i,total,id,phase}) }
153
153
  // Roda os casos EM SÉRIE (sandbox isolado por caso; evita colisão e rate-limit).
154
154
  async function runSuite(suite, opts = {}) {
155
- const { token, lang = 'pt', model = null, judgeModel = DEFAULT_JUDGE } = opts;
155
+ const { token, lang = 'pt', model = null, judgeModel = DEFAULT_JUDGE, maxIter, maxDurationMs, maxCredits } = opts;
156
156
  const onCase = opts.onCase || (() => {});
157
157
  // chave sk-hub (mesma do agente) — reusada pra chamar o juiz direto no gateway.
158
158
  const k = await keyring.resolve(token, { feature: 'cli_agent' });
@@ -188,7 +188,7 @@ async function runSuite(suite, opts = {}) {
188
188
  out = await agent.run(c.task, {
189
189
  token, lang, yes: true, model,
190
190
  cwd: sandbox, confineDir: sandbox, readOnly: c.readonly,
191
- skipSessionStart: true,
191
+ skipSessionStart: true, maxIter, maxDurationMs, maxCredits,
192
192
  });
193
193
  } catch (e) {
194
194
  if (e && (e.code === 'no_credits' || e.status === 402)) throw e; // teto → aborta a suíte
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ // Autoevolução é deliberadamente separada da execução: incidentes geram uma
4
+ // PROPOSTA revisável, nunca um patch, comando ou deploy automático. Por decisão
5
+ // de produto, somente o GPT SOL pode assinar esse diagnóstico de harness.
6
+ const DIAGNOSTICIAN_MODEL = 'gpt-5.6-sol';
7
+ const telemetry = require('./evolution-telemetry');
8
+
9
+ function diagnosticianFor() { return DIAGNOSTICIAN_MODEL; }
10
+
11
+ function buildRequest(events = [], context = {}) {
12
+ const cleanEvents = telemetry.buildEvents(events, [], context).map(event => ({
13
+ tool: event.tool, errorClass: event.errorClass, pattern: event.pattern,
14
+ stage: event.stage, fingerprint: event.fingerprint, retryable: event.retryable,
15
+ recovered: event.recovered,
16
+ }));
17
+ return {
18
+ model: DIAGNOSTICIAN_MODEL,
19
+ mode: 'proposal_only',
20
+ instruction: 'Analise somente os incidentes sanitizados. Proponha uma hipótese, testes de regressão e uma mudança mínima para revisão humana. Não gere comandos de produção, não aplique patches, não faça deploy e não trate a proposta como correção aprovada.',
21
+ incidents: cleanEvents.slice(0, 20),
22
+ };
23
+ }
24
+
25
+ function normalizeProposal(value, model) {
26
+ const v = value && typeof value === 'object' ? value : {};
27
+ const bySol = String(model || '') === DIAGNOSTICIAN_MODEL;
28
+ const list = (items, limit, width) => (Array.isArray(items) ? items : [])
29
+ .map(item => String(item || '').replace(/\s+/g, ' ').trim().slice(0, width))
30
+ .filter(Boolean).slice(0, limit);
31
+ return {
32
+ accepted: bySol,
33
+ model: bySol ? DIAGNOSTICIAN_MODEL : '',
34
+ mode: 'proposal_only',
35
+ requiresHumanReview: true,
36
+ autoApply: false,
37
+ title: String(v.title || '').replace(/\s+/g, ' ').trim().slice(0, 160),
38
+ diagnosis: String(v.diagnosis || v.summary || '').replace(/\s+/g, ' ').trim().slice(0, 1200),
39
+ evidence: list(v.evidence, 8, 360),
40
+ proposedChanges: list(v.proposedChanges || v.recommendations, 8, 500),
41
+ regressionTests: list(v.regressionTests || v.tests, 8, 360),
42
+ };
43
+ }
44
+
45
+ function canAutoApply() { return false; }
46
+
47
+ module.exports = { DIAGNOSTICIAN_MODEL, diagnosticianFor, buildRequest, normalizeProposal, canAutoApply };
@@ -0,0 +1,42 @@
1
+ // Lock cooperativo entre processos do Terminal Smart. `wx` cria o arquivo de
2
+ // forma atômica: duas CLIs não conseguem adquirir o mesmo alvo ao mesmo tempo.
3
+ 'use strict';
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+
9
+ const DIR = path.join(os.homedir(), '.ts', 'locks');
10
+ const STALE_MS = 2 * 60 * 1000;
11
+ const keyFor = (target) => crypto.createHash('sha256').update(path.resolve(target).toLowerCase()).digest('hex').slice(0, 24);
12
+ const pathFor = (target) => path.join(DIR, keyFor(target) + '.lock');
13
+
14
+ function acquire(target, { staleMs = STALE_MS } = {}) {
15
+ const lockPath = pathFor(target);
16
+ try { fs.mkdirSync(DIR, { recursive: true }); } catch (_) { return null; }
17
+ for (let attempt = 0; attempt < 2; attempt++) {
18
+ try {
19
+ const fd = fs.openSync(lockPath, 'wx', 0o600);
20
+ try { fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, target: path.resolve(target), createdAt: new Date().toISOString() })); } finally { fs.closeSync(fd); }
21
+ let released = false;
22
+ return {
23
+ release() {
24
+ if (released) return;
25
+ released = true;
26
+ try { fs.unlinkSync(lockPath); } catch (_) {}
27
+ },
28
+ lockPath,
29
+ };
30
+ } catch (err) {
31
+ if (!err || err.code !== 'EEXIST') return null;
32
+ try {
33
+ const age = Date.now() - fs.statSync(lockPath).mtimeMs;
34
+ if (age > staleMs) { fs.unlinkSync(lockPath); continue; }
35
+ } catch (_) { continue; }
36
+ return null;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ module.exports = { DIR, STALE_MS, keyFor, pathFor, acquire };
@@ -0,0 +1,69 @@
1
+ // Gateways operacionais: aliases OpenSSH com comando forçado (não são shells
2
+ // remotos normais). A configuração guarda apenas alias e allowlist; chaves e
3
+ // credenciais continuam exclusivamente no OpenSSH do sistema.
4
+ const config = require('./config');
5
+
6
+ const OPS = Object.freeze(['status', 'logs', 'deploy']);
7
+
8
+ function normalized(value) {
9
+ return String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
10
+ }
11
+
12
+ function safeName(value) {
13
+ const raw = String(value || '').trim();
14
+ if (!raw || /[\\/]/.test(raw) || raw === '.' || raw === '..' || raw.includes('..')) return null;
15
+ const name = normalized(raw).replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
16
+ return /^[a-z0-9][a-z0-9_-]{0,63}$/.test(name) ? name : null;
17
+ }
18
+
19
+ function safeAlias(value) {
20
+ const alias = String(value || '').trim();
21
+ return /^[A-Za-z0-9_.-]{1,128}$/.test(alias) ? alias : null;
22
+ }
23
+
24
+ function normalizeOps(value) {
25
+ const values = Array.isArray(value) ? value : String(value || '').split(',');
26
+ const ops = [...new Set(values.map(x => String(x).trim().toLowerCase()).filter(x => OPS.includes(x)))];
27
+ return ops.length ? ops : ['status'];
28
+ }
29
+
30
+ function list() {
31
+ const raw = config.load().gateways || {};
32
+ return Object.entries(raw).map(([name, item]) => {
33
+ const alias = safeAlias(item && item.sshAlias);
34
+ if (!alias) return null;
35
+ return { name, label: String(item.label || name), sshAlias: alias, operations: normalizeOps(item.operations) };
36
+ }).filter(Boolean);
37
+ }
38
+
39
+ function save(name, input = {}) {
40
+ const id = safeName(name);
41
+ const alias = safeAlias(input.sshAlias);
42
+ if (!id) throw new Error('Nome do gateway inválido. Use letras, números, hífen ou sublinhado.');
43
+ if (!alias) throw new Error('Alias OpenSSH inválido. Use apenas letras, números, ponto, hífen ou sublinhado.');
44
+ const cfg = config.load();
45
+ const gateways = { ...(cfg.gateways || {}) };
46
+ gateways[id] = { label: String(input.label || id).trim().slice(0, 80), sshAlias: alias, operations: normalizeOps(input.operations) };
47
+ config.save({ gateways });
48
+ return { name: id, ...gateways[id] };
49
+ }
50
+
51
+ function remove(name) {
52
+ const id = safeName(name);
53
+ const cfg = config.load();
54
+ const gateways = { ...(cfg.gateways || {}) };
55
+ if (!id || !gateways[id]) return false;
56
+ delete gateways[id];
57
+ config.save({ gateways });
58
+ return true;
59
+ }
60
+
61
+ function findInText(text, entries = list()) {
62
+ const normalizedText = normalized(text);
63
+ return entries.find(entry => {
64
+ const terms = [entry.name, entry.label, entry.sshAlias].map(normalized).filter(Boolean);
65
+ return terms.some(term => normalizedText.includes(term));
66
+ }) || null;
67
+ }
68
+
69
+ module.exports = { OPS, normalized, safeName, safeAlias, normalizeOps, list, save, remove, findInText };
@@ -53,11 +53,6 @@ const MODEL_CATALOG = Object.freeze({
53
53
  provider: 'moonshot', contextWindow: 262144, price: { input: 0.75, output: 3.50, cachedInput: 0.15 },
54
54
  capabilities: ['tools', 'code', 'vision', 'long-context'], toolReliability: 0.86, quality: 0.84,
55
55
  },
56
- 'kimi-k3': {
57
- upstreamId: 'moonshotai/kimi-k3',
58
- provider: 'moonshot', contextWindow: 262144, price: { input: 3.00, output: 15.00, cachedInput: 0.30 },
59
- capabilities: ['tools', 'code', 'planning', 'long-context'], toolReliability: 0.88, quality: 0.91,
60
- },
61
56
  'glm-4.6': {
62
57
  upstreamId: 'z-ai/glm-4.6',
63
58
  provider: 'zai', contextWindow: 204800, price: { input: 0.50, output: 2.00, cachedInput: 0.10 },
@@ -109,7 +104,9 @@ const MODEL_CATALOG = Object.freeze({
109
104
  capabilities: ['tools', 'code', 'planning', 'review'], toolReliability: 0.96, quality: 0.95,
110
105
  },
111
106
  'claude-haiku-4-5': {
112
- upstreamId: 'anthropic/claude-haiku-4.5',
107
+ // ID canônico exposto pelo OpenRouter. O alias interno permanece estável
108
+ // para planos e histórico, mas o gateway precisa receber este identificador.
109
+ upstreamId: 'anthropic/claude-4.5-haiku-20251001',
113
110
  provider: 'anthropic', contextWindow: 200000, price: { input: 1.00, output: 5.00, cachedInput: 0.10 },
114
111
  capabilities: ['tools', 'code', 'planning', 'execution', 'review', 'vision'], toolReliability: 0.94, quality: 0.88,
115
112
  },
@@ -176,7 +173,6 @@ const MODEL_CATALOG = Object.freeze({
176
173
  const MODEL_ALIASES = Object.freeze({
177
174
  deepseek: 'deepseek-v4-flash',
178
175
  'deepseek-v4': 'deepseek-v4-flash',
179
- kimi: 'kimi-k2.7-code-highspeed',
180
176
  glm: 'glm-4.6',
181
177
  mimo: 'mimo-v2.5',
182
178
  'gemini-lite': 'gemini-2.5-flash-lite',
@@ -193,35 +189,35 @@ const AGENT_ROLES = Object.freeze({
193
189
  }),
194
190
  planner: Object.freeze({
195
191
  prompt: 'Decomponha o objetivo em etapas executáveis, dependências, riscos, critérios de aceite e provas. Não execute a tarefa.',
196
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'glm-5.2'] }),
192
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['gpt-5.6-luna', 'deepseek-v4-pro'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'glm-5.2'] }),
197
193
  }),
198
194
  executor: Object.freeze({
199
195
  prompt: 'Execute somente a etapa recebida com as ferramentas autorizadas. Não declare sucesso sem resultado verificável.',
200
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'glm-5.2', 'claude-haiku-4-5'] }),
196
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
201
197
  }),
202
198
  inspector: Object.freeze({
203
199
  prompt: 'Inspecione em modo somente leitura e compare evidências com os critérios de aceite. Não altere nada e não elogie por cortesia.',
204
- models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash', 'gpt-4o-mini'], pro: ['gpt-5.6-luna', 'claude-haiku-4-5', 'gemini-2.5-flash'] }),
200
+ models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gpt-5.6-luna', 'gemini-2.5-flash'], pro: ['gpt-5.6-luna', 'gemini-2.5-flash'] }),
205
201
  }),
206
202
  corrector: Object.freeze({
207
203
  prompt: 'Corrija apenas falhas confirmadas pelo inspetor, preserve o que funciona e repita as provas afetadas. Não amplie o escopo.',
208
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['deepseek-v4-pro', 'claude-haiku-4-5', 'glm-5.2'] }),
204
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'] }),
209
205
  }),
210
206
  vision: Object.freeze({
211
207
  prompt: 'Extraia fatos observáveis da imagem e separe leitura, inferência e incerteza. Não invente texto ilegível.',
212
- models: Object.freeze({ free: ['smart'], basic: ['gemini-2.5-flash'], pro: ['gpt-5.6-luna', 'gemini-2.5-flash', 'claude-haiku-4-5'] }),
208
+ models: Object.freeze({ free: ['gemini-2.5-flash'], basic: ['mimo-v2.5', 'gemini-2.5-flash'], pro: ['mimo-v2.5', 'gemini-2.5-flash', 'gpt-5.6-luna'] }),
213
209
  }),
214
210
  document: Object.freeze({
215
211
  prompt: 'Produza conteúdo estruturado e use o motor nativo do formato solicitado. Preserve editabilidade e valide o arquivo gerado.',
216
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'deepseek-v4-flash'], pro: ['gpt-5.6-luna', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
212
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'mimo-v2.5', 'gemini-2.5-flash'], pro: ['mimo-v2.5', 'deepseek-v4-pro', 'gpt-5.6-luna'] }),
217
213
  }),
218
214
  site_builder: Object.freeze({
219
215
  prompt: 'Crie ou edite o site completo, responsivo e acessível. Preserve o existente quando editar e valide a URL publicada.',
220
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash', 'deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'deepseek-v4-flash', 'claude-haiku-4-5'] }),
216
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
221
217
  }),
222
218
  cloud_box: Object.freeze({
223
219
  prompt: 'Opere a box com mudanças mínimas, registre comandos e valide serviço, porta e URL. Nunca exponha segredos.',
224
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-flash'], pro: ['deepseek-v4-flash', 'deepseek-v4-pro', 'claude-haiku-4-5'] }),
220
+ models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
225
221
  }),
226
222
  });
227
223
 
@@ -338,6 +334,74 @@ function _cleanText(value, max) {
338
334
  .slice(0, max);
339
335
  }
340
336
 
337
+ // Dados que cruzam a fronteira da máquina (handoff, painel e telemetria) não
338
+ // precisam de e-mails, URLs, caminhos locais ou comandos literais. Mantemos o
339
+ // sinal operacional, mas removemos detalhes que pertencem somente à sessão local.
340
+ function cleanOperationalText(value, max) {
341
+ return _cleanText(value, max || 300)
342
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '<email>')
343
+ .replace(/https?:\/\/[^\s"']+/gi, '<url>')
344
+ .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '<ip>')
345
+ .replace(/[A-Za-z]:\\[^\s"']+/g, '<path>')
346
+ .replace(/\/(?:home|root|opt|var|tmp|Users|mnt)\/[^\s"']+/g, '<path>')
347
+ .replace(/\b(?:nvapi-|AIza)[A-Za-z0-9._-]{12,}\b/gi, '<secret>')
348
+ .replace(/\b[0-9a-f]{32,}\b/gi, '<id>')
349
+ .replace(/\s+/g, ' ').trim().slice(0, max || 300);
350
+ }
351
+
352
+ function _safeToolName(value) {
353
+ const name = String(value || 'unknown').replace(/[^a-z0-9_-]/gi, '_').slice(0, 80);
354
+ return name || 'unknown';
355
+ }
356
+
357
+ function _safeModelId(value) {
358
+ const normalized = normalizeModelId(value);
359
+ return MODEL_CATALOG[normalized] || normalized === 'smart' ? normalized : _safeToolName(value);
360
+ }
361
+
362
+ // Contrato compacto para painéis e comparação de execuções. É deliberadamente
363
+ // agregado: o painel sabe se a missão funcionou e onde falhou, sem receber o
364
+ // pedido do usuário, conteúdo de e-mail, arquivo, comando ou diretório.
365
+ function buildOperationalSummary(input) {
366
+ const x = input || {};
367
+ const usage = x.tokens || x.usage || {};
368
+ const failures = new Map();
369
+ for (const item of (Array.isArray(x.toolErrors) ? x.toolErrors : []).slice(-50)) {
370
+ const tool = _safeToolName(item && item.tool);
371
+ const errorClass = _safeToolName(item && (item.errorClass || item.class || 'unknown'));
372
+ const key = tool + ':' + errorClass;
373
+ const row = failures.get(key) || { tool, errorClass, count: 0, retryable: false };
374
+ row.count++;
375
+ row.retryable = row.retryable || item.retryable === true;
376
+ failures.set(key, row);
377
+ }
378
+ const actions = {};
379
+ for (const action of (Array.isArray(x.actions) ? x.actions : []).slice(0, 200)) {
380
+ const name = _safeToolName(action && action.name);
381
+ actions[name] = (actions[name] || 0) + 1;
382
+ }
383
+ const verification = x.verification || {};
384
+ return {
385
+ version: 1,
386
+ surface: _safeToolName(x.surface || 'cli'),
387
+ model: _safeModelId(x.model || 'smart'),
388
+ status: x.completion && x.completion.ok ? 'completed' : (x.guard ? 'stopped_by_guard' : 'blocked'),
389
+ completionReason: _safeToolName(x.completion && x.completion.reason || 'unknown'),
390
+ steps: Math.max(0, Number(x.steps) || 0),
391
+ durationMs: Math.max(0, Number(x.durationMs) || 0),
392
+ credits: Math.max(0, Number(x.credits) || 0),
393
+ tokens: {
394
+ input: Math.max(0, Number(usage.inTok || usage.input_tokens) || 0),
395
+ output: Math.max(0, Number(usage.outTok || usage.output_tokens) || 0),
396
+ cached: Math.max(0, Number(usage.cachedTok || usage.cacheTok || usage.cached_tokens) || 0),
397
+ },
398
+ actions,
399
+ failures: [...failures.values()].sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)).slice(0, 20),
400
+ verification: { total: Math.max(0, Number(verification.total) || 0), passed: Math.max(0, Number(verification.passed) || 0), allOk: verification.allOk === true },
401
+ cache: Object.assign({ hits: 0, misses: 0, entries: 0 }, x.missionCache || {}),
402
+ };
403
+ }
404
+
341
405
  function createMemoryRecord(input) {
342
406
  const x = input || {};
343
407
  const type = MEMORY_TYPES.includes(x.type) ? x.type : 'episodic';
@@ -385,16 +449,16 @@ function createHandoff(input) {
385
449
  return {
386
450
  version: 1,
387
451
  workstreamId: _cleanText(x.workstreamId || 'default', 160),
388
- goal: _cleanText(x.goal, 2000),
452
+ goal: cleanOperationalText(x.goal, 600),
389
453
  status: ['running', 'blocked', 'done', 'paused'].includes(x.status) ? x.status : 'running',
390
- decisions: (x.decisions || []).slice(0, 20).map(v => _cleanText(v, 800)),
454
+ decisions: (x.decisions || []).slice(0, 20).map(v => cleanOperationalText(v, 400)),
391
455
  changedFiles: (x.changedFiles || []).slice(0, 100).map(v => typeof v === 'string'
392
- ? { path: _cleanText(v, 500), hash: '' }
393
- : { path: _cleanText(v.path, 500), hash: _cleanText(v.hash, 128) }),
394
- evidence: (x.evidence || []).slice(0, 30).map(v => _cleanText(v, 1000)),
395
- failedApproaches: (x.failedApproaches || []).slice(0, 20).map(v => _cleanText(v, 1000)),
396
- openQuestions: (x.openQuestions || []).slice(0, 20).map(v => _cleanText(v, 800)),
397
- nextActions: (x.nextActions || []).slice(0, 20).map(v => _cleanText(v, 800)),
456
+ ? { path: cleanOperationalText(v, 180), hash: '' }
457
+ : { path: cleanOperationalText(v.path, 180), hash: _cleanText(v.hash, 128) }),
458
+ evidence: (x.evidence || []).slice(0, 30).map(v => cleanOperationalText(v, 300)),
459
+ failedApproaches: (x.failedApproaches || []).slice(0, 20).map(v => cleanOperationalText(v, 300)),
460
+ openQuestions: (x.openQuestions || []).slice(0, 20).map(v => cleanOperationalText(v, 300)),
461
+ nextActions: (x.nextActions || []).slice(0, 20).map(v => cleanOperationalText(v, 300)),
398
462
  budget: Object.assign({ currency: 'USD', spent: 0, remaining: null }, x.budget || {}),
399
463
  createdAt: x.createdAt || new Date().toISOString(),
400
464
  };
@@ -411,8 +475,14 @@ function agentRoleContract(role, plan, options) {
411
475
  const id = Object.prototype.hasOwnProperty.call(AGENT_ROLES, role) ? role : 'executor';
412
476
  const planId = normalizeAgentPlan(plan);
413
477
  const spec = AGENT_ROLES[id];
414
- const requested = spec.models[planId] || spec.models.free;
415
- const allowedRaw = options && Array.isArray(options.allowedModels) ? options.allowedModels : null;
478
+ // Tarefas complexas nunca usam modelos Flash nos papeis que decidem ou
479
+ // alteram o projeto. Isto vale inclusive no Free: o limite desse plano e de
480
+ // tentativas/orcamento, nao uma degradacao silenciosa da qualidade do papel.
481
+ const complexCore = options && options.complex === true && ['planner', 'executor', 'corrector'].includes(id)
482
+ ? (id === 'planner' ? ['gpt-5.6-luna', 'deepseek-v4-pro'] : ['deepseek-v4-pro', 'gpt-5.6-luna'])
483
+ : null;
484
+ const requested = complexCore || spec.models[planId] || spec.models.free;
485
+ const allowedRaw = complexCore && planId === 'free' ? null : (options && Array.isArray(options.allowedModels) ? options.allowedModels : null);
416
486
  const allowAll = !allowedRaw || allowedRaw.includes('*');
417
487
  const allowed = allowAll ? null : new Set(allowedRaw.map(normalizeModelId));
418
488
  const candidates = requested.map(normalizeModelId).filter(model => model === 'smart' || MODEL_CATALOG[model])
@@ -436,23 +506,26 @@ function routeModel(task, reliability) {
436
506
  if (t.kind === 'vision') required.add('vision');
437
507
  const candidates = (t.candidates || Object.keys(MODEL_CATALOG)).map(normalizeModelId).filter(id => MODEL_CATALOG[id] && id !== 'smart');
438
508
  const rel = reliability || {};
509
+ const plan = String(t.plan || '').toLowerCase();
510
+ const isPaid = plan && plan !== 'free' && plan !== 'gratuito';
511
+
439
512
  const scored = candidates.map(id => {
440
513
  const m = MODEL_CATALOG[id];
441
514
  const missing = [...required].filter(c => !m.capabilities.includes(c)).length;
442
515
  const observed = rel[id] && Number.isFinite(rel[id].successRate) ? rel[id].successRate : m.toolReliability;
443
516
  const price = m.price.input + m.price.output;
444
- const qualityWeight = t.risk === 'high' ? 0.55 : 0.35;
445
- const costWeight = t.risk === 'high' ? 0.10 : 0.30;
517
+ const qualityWeight = isPaid ? 0.75 : (t.risk === 'high' ? 0.55 : 0.35);
518
+ const costWeight = isPaid ? 0.05 : (t.risk === 'high' ? 0.10 : 0.30);
446
519
  const score = (observed * 0.35) + (m.quality * qualityWeight) + ((1 / (1 + price)) * costWeight) - (missing * 2);
447
520
  return { id, score: +score.toFixed(6), missing };
448
521
  }).sort((a, b) => b.score - a.score);
449
- return { model: scored[0] ? scored[0].id : 'deepseek-v4-flash', ranked: scored };
522
+ return { model: scored[0] ? scored[0].id : (isPaid ? 'deepseek-v4-pro' : 'deepseek-v4-flash'), ranked: scored };
450
523
  }
451
524
 
452
525
  module.exports = {
453
526
  INTELLIGENCE_CONTRACT, PRICE_REVISION, MODEL_CATALOG, MODEL_ALIASES, DEFAULT_PRICE,
454
527
  MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES, AGENT_ROLES,
455
528
  normalizeModelId, modelInfo, legacyPriceMap, normalizeUsage, estimateCostUsd, catalogFreshness,
456
- createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff,
529
+ createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff, cleanOperationalText, buildOperationalSummary,
457
530
  normalizeAgentPlan, agentRoleContract, routeModel,
458
531
  };
package/lib/meta.js CHANGED
@@ -973,6 +973,10 @@ async function _llmText({ token, model, system, user, maxTokens = 1200, creditBu
973
973
  // "pensador" numa análise FOCADA (sem ferramentas, sem loop) com o erro + o código
974
974
  // dos arquivos citados, e devolve um diagnóstico + fix EXATO pro executor aplicar.
975
975
  async function escalate(buildErr, projDir, thinker, token) {
976
+ // Diagnóstico de falha repetida não segue a escolha genérica de "pensador":
977
+ // somente o GPT SOL pode propor causa/fix. O executor continua sendo quem
978
+ // aplica a alteração e os gates continuam exigindo prova posterior.
979
+ const diagnostician = require('./evolution-proposal').diagnosticianFor(thinker);
976
980
  // ── DIAGNÓSTICO ANTES DE ESCALAR (v0.52): em vez de só chutar pelo TEXTO do erro, o TS
977
981
  // INVESTIGA o sistema real com sondas só-leitura (hipótese→sonda→verificação adversarial)
978
982
  // e entrega a CAUSA RAIZ ao pensador. Só na escalação (já estamos travados) → custo contido.
@@ -980,7 +984,7 @@ async function escalate(buildErr, projDir, thinker, token) {
980
984
  let diagBlock = '', diagCred = 0;
981
985
  if (process.env.TS_META_DIAGNOSE !== '0') {
982
986
  try {
983
- const dr = await require('./diagnose').diagnose(String(buildErr).slice(0, 1400), { token, cwd: projDir, model: thinker, maxRounds: Number(process.env.TS_META_DIAGNOSE_ROUNDS) || 4 });
987
+ const dr = await require('./diagnose').diagnose(String(buildErr).slice(0, 1400), { token, cwd: projDir, model: diagnostician, maxRounds: Number(process.env.TS_META_DIAGNOSE_ROUNDS) || 4 });
984
988
  diagCred = dr.credits || 0;
985
989
  if (dr.status === 'solved' && dr.rootCause) diagBlock = `\n\nDIAGNÓSTICO (o TS investigou o sistema com sondas só-leitura${dr.verified ? ', causa VERIFICADA' : ''}):\nCAUSA RAIZ: ${dr.rootCause}\nFIX apontado: ${dr.fix || '—'}\n(Use isto como base — é evidência do sistema real, não palpite.)`;
986
990
  } catch (_) {}
@@ -991,7 +995,7 @@ async function escalate(buildErr, projDir, thinker, token) {
991
995
  const sys = 'Você é um engenheiro sênior de DEBUG. Recebe o ERRO REAL de um build, um DIAGNÓSTICO do sistema (se houver) e o código dos arquivos citados. '
992
996
  + 'Se houver DIAGNÓSTICO, PARTA DELE (é evidência do sistema real). Diga a CAUSA-RAIZ em 1-2 linhas e depois o FIX EXATO e mínimo: em QUAL arquivo, QUAL linha/trecho, e o QUE trocar (mostre o antes→depois curto). '
993
997
  + 'NÃO reescreva o app inteiro, só o necessário pra compilar. Seja preciso e direto.';
994
- const r = await _llmText({ token, model: thinker, system: sys, user: `ERRO DO BUILD:\n${String(buildErr).slice(0, 2500)}${diagBlock}\n\nCÓDIGO RELEVANTE:${ctx.slice(0, 6000)}`, maxTokens: 1200 });
998
+ const r = await _llmText({ token, model: diagnostician, system: sys, user: `ERRO DO BUILD:\n${String(buildErr).slice(0, 2500)}${diagBlock}\n\nCÓDIGO RELEVANTE:${ctx.slice(0, 6000)}`, maxTokens: 1200 });
995
999
  return { hint: (diagBlock ? diagBlock.trim() + '\n\n' : '') + r.text, credits: (r.credits || 0) + diagCred };
996
1000
  }
997
1001
 
@@ -1167,6 +1171,16 @@ async function _checkCriteria(st, dir) {
1167
1171
  }
1168
1172
  }
1169
1173
 
1174
+ // Fast fail-closed gate used after every implementation round. File/content
1175
+ // criteria may belong to later checklist items, but command criteria (tests,
1176
+ // lint, typecheck) describe repository health and must stay green throughout.
1177
+ async function _checkRoundCommandCriteria(st, dir) {
1178
+ const criteria = (st.criteria || []).filter(c => c && c.type === 'command');
1179
+ if (!criteria.length) return { allOk: true, passed: 0, total: 0, results: [] };
1180
+ try { return await require('./verify').runAll(criteria, { cwd: dir }); }
1181
+ catch (e) { return { allOk: false, passed: 0, total: criteria.length, results: [], error: String(e && e.message || e) }; }
1182
+ }
1183
+
1170
1184
  async function run(goal, opts = {}) {
1171
1185
  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;
1172
1186
  const onChecklist = opts.onChecklist || (() => {});
@@ -1693,6 +1707,18 @@ async function run(goal, opts = {}) {
1693
1707
  return st;
1694
1708
  }
1695
1709
 
1710
+ const roundCommandProof = await _checkRoundCommandCriteria(st, dir);
1711
+ if (!roundCommandProof.allOk) {
1712
+ item.attempts = Math.max(0, (item.attempts || 1) - 1);
1713
+ st.status = 'paused'; st.pause_reason = 'verification_failed';
1714
+ st.rounds.push({ item: item.desc, id: item.id,
1715
+ result: `Prova determinística falhou: ${roundCommandProof.passed}/${roundCommandProof.total} comandos passaram. O item permanece aberto para correção.`,
1716
+ credits: out.credits || 0, steps: out.steps || 0, at: new Date().toISOString() });
1717
+ save(st, dir);
1718
+ onAlert({ type: 'verification', text: `Os testes/comandos obrigatórios falharam (${roundCommandProof.passed}/${roundCommandProof.total}). Não marquei o item como concluído; a próxima janela deve corrigir a regressão.` });
1719
+ return st;
1720
+ }
1721
+
1696
1722
  // ── BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz ──
1697
1723
  // A missão PAUSA (gasto zero enquanto espera) e chama o usuário. Retoma com "ts meta".
1698
1724
  if (out.needHuman) {
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const core = require('./core');
5
+
6
+ /**
7
+ * Módulo de Auditoria Privada do Dono para sessões de teste e homologação.
8
+ * Opt-in, temporário, estritamente sanitizado.
9
+ */
10
+
11
+ function sanitizeAuditString(value, allowContent = false) {
12
+ if (typeof value !== 'string') return '';
13
+ let str = core.redactSecrets ? core.redactSecrets(value) : value;
14
+
15
+ // Remoção incondicional de segredos e tokens sensíveis
16
+ str = str
17
+ .replace(/\b(?:Bearer\s+)[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [REDACTED]')
18
+ .replace(/\b(?:ya29\.[A-Za-z0-9_-]+)/gi, '[GOOGLE_OAUTH_TOKEN]')
19
+ .replace(/\b(?:ghp_|gho_|github_pat_)[A-Za-z0-9_]{16,}/gi, '[GITHUB_TOKEN]')
20
+ .replace(/\b(?:nvapi-|AIza|sk-|xox[baprs]-)[A-Za-z0-9._-]{10,}/gi, '[API_KEY]')
21
+ .replace(/-----BEGIN[ A-Z0-9_-]*PRIVATE KEY-----[\s\S]*?-----END[ A-Z0-9_-]*PRIVATE KEY-----/gi, '[PRIVATE_KEY]')
22
+ .replace(/(?:password|passwd|secret|token|authorization|auth_token)\s*[:=]\s*["']?[^"',\s]+["']?/gi, '$1=[REDACTED]');
23
+
24
+ if (!allowContent) {
25
+ // Mascara URLs completas com parâmetros e e-mails se não for incidente com autorização explícita
26
+ str = str
27
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '<email>')
28
+ .replace(/https?:\/\/[^\s"']+/gi, '<url>');
29
+ }
30
+
31
+ return str.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 1000);
32
+ }
33
+
34
+ function isOwnerAuditActive(options = {}, env = process.env) {
35
+ if (options.ownerAudit === true) return true;
36
+ const envVal = String(env.TS_OWNER_AUDIT || '').toLowerCase();
37
+ return envVal === '1' || envVal === 'true' || envVal === 'on';
38
+ }
39
+
40
+ class OwnerAuditSession {
41
+ constructor(options = {}) {
42
+ this.sessionId = options.sessionId || crypto.randomBytes(8).toString('hex');
43
+ this.startedAt = Date.now();
44
+ this.appVersion = options.appVersion || '0.97.29';
45
+ this.surface = options.surface || 'cli';
46
+ this.accountType = 'owner';
47
+ this.explicitContentAuthorization = options.explicitContentAuthorization === true;
48
+ this.records = [];
49
+ }
50
+
51
+ recordStep({ stage, tool, durationMs, result, recovered, costTokens, costUsd, meta = {} }) {
52
+ const sanitizedMeta = {};
53
+ if (meta && typeof meta === 'object') {
54
+ for (const [k, v] of Object.entries(meta)) {
55
+ if (typeof v === 'string') {
56
+ sanitizedMeta[k] = sanitizeAuditString(v, this.explicitContentAuthorization);
57
+ } else if (typeof v === 'number' || typeof v === 'boolean') {
58
+ sanitizedMeta[k] = v;
59
+ }
60
+ }
61
+ }
62
+
63
+ const entry = {
64
+ id: crypto.randomBytes(6).toString('hex'),
65
+ timestamp: Date.now(),
66
+ stage: String(stage || 'step').slice(0, 50),
67
+ tool: String(tool || 'system').slice(0, 80),
68
+ durationMs: Number(durationMs) || 0,
69
+ result: result === 'success' || result === true ? 'success' : (result === 'blocked' ? 'blocked' : 'failed'),
70
+ recovered: recovered === true,
71
+ version: this.appVersion,
72
+ costTokens: Number(costTokens) || 0,
73
+ costUsd: Number(costUsd) || 0,
74
+ meta: sanitizedMeta
75
+ };
76
+
77
+ this.records.push(entry);
78
+ if (this.records.length > 500) {
79
+ this.records.shift();
80
+ }
81
+ return entry;
82
+ }
83
+
84
+ getSummary() {
85
+ const totalDuration = this.records.reduce((sum, r) => sum + r.durationMs, 0);
86
+ const totalCostTokens = this.records.reduce((sum, r) => sum + r.costTokens, 0);
87
+ const totalCostUsd = this.records.reduce((sum, r) => sum + r.costUsd, 0);
88
+ const successCount = this.records.filter(r => r.result === 'success').length;
89
+ const failedCount = this.records.filter(r => r.result === 'failed').length;
90
+ const recoveredCount = this.records.filter(r => r.recovered).length;
91
+
92
+ return {
93
+ sessionId: this.sessionId,
94
+ appVersion: this.appVersion,
95
+ surface: this.surface,
96
+ totalSteps: this.records.length,
97
+ successCount,
98
+ failedCount,
99
+ recoveredCount,
100
+ totalDurationMs: totalDuration,
101
+ totalCostTokens,
102
+ totalCostUsd: Number(totalCostUsd.toFixed(6)),
103
+ records: [...this.records]
104
+ };
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ isOwnerAuditActive,
110
+ sanitizeAuditString,
111
+ OwnerAuditSession
112
+ };
package/lib/providers.js CHANGED
@@ -59,7 +59,10 @@ const PROVEDORES = {
59
59
  openrouter: {
60
60
  nome: 'OpenRouter',
61
61
  baseUrl: 'https://openrouter.ai/api/v1',
62
- modeloPadrao: 'deepseek/deepseek-chat',
62
+ // O padrão precisa refletir o produto: Flash fica restrito ao Free no
63
+ // roteador do TS; para uma conexão OpenRouter sem modelo explícito, use o
64
+ // executor oficial pago em vez do legado deepseek-chat.
65
+ modeloPadrao: 'deepseek/deepseek-v4-pro',
63
66
  prefixo: /^sk-or-/,
64
67
  gratuito: false,
65
68
  limite: 'free tier com rate limit frequente',