terminal-smart-cli 0.97.65 → 0.97.66
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/agent.js +7 -1
- package/lib/core.js +46 -5
- package/lib/evolution-telemetry.js +58 -2
- package/package.json +2 -2
package/lib/agent.js
CHANGED
|
@@ -2296,7 +2296,13 @@ async function run(task, opts = {}) {
|
|
|
2296
2296
|
if (!_tr.ok && _tr.status !== 'blocked') {
|
|
2297
2297
|
const _shellHint = name === 'executar_comando' ? commandRecoveryHint(input.comando, _tr.evidence) : '';
|
|
2298
2298
|
if (_shellHint && result && typeof result === 'object') result = Object.assign({}, result, { _recovery: _shellHint });
|
|
2299
|
-
|
|
2299
|
+
// O sinal estruturado vem do ToolResult tipado — exitCode, errno e hadStderr, que
|
|
2300
|
+
// o core ja calculava e descartava — mais o pathKind, derivado AQUI a partir dos
|
|
2301
|
+
// argumentos da ferramenta: so o resultado ('relative' | 'absolute') viaja, o
|
|
2302
|
+
// caminho em si nunca sai da maquina.
|
|
2303
|
+
_toolErrs.push({ tool: name, class: _tr.errorClass, retryable: _tr.retryable, evidence: _tr.evidence,
|
|
2304
|
+
exitCode: _tr.exitCode, errno: _tr.errno, hadStderr: _tr.hadStderr,
|
|
2305
|
+
pathKind: require('./evolution-telemetry').pathKindOf(input) });
|
|
2300
2306
|
const _failureKey = failureFingerprint(name, _tr, result);
|
|
2301
2307
|
const _equivalentFailures = (_failureCounts.get(_failureKey) || 0) + 1;
|
|
2302
2308
|
_failureCounts.set(_failureKey, _equivalentFailures);
|
package/lib/core.js
CHANGED
|
@@ -167,11 +167,44 @@ function classifyError(msg) {
|
|
|
167
167
|
return 'unknown';
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
// raw = retorno de uma ferramenta (execute). Devolve
|
|
170
|
+
// raw = retorno de uma ferramenta (execute). Devolve
|
|
171
|
+
// { ok, status, errorClass, retryable, evidence, exitCode, errno, hadStderr }.
|
|
171
172
|
// NÃO muta o raw. Convenções do TS: {erro} = falha; {codigo|exitCode}≠0 = falha de shell;
|
|
172
173
|
// {written|created|resumo|stdout|...} = evidência de sucesso.
|
|
174
|
+
//
|
|
175
|
+
// Os três últimos campos entraram em 12/09/2026 e são o SINAL ESTRUTURADO que a telemetria
|
|
176
|
+
// do harness passou a aceitar. Eles já estavam aqui dentro: `exit` e `errMsg` eram
|
|
177
|
+
// calculados e descartados. Sem eles, três falhas com correções completamente diferentes
|
|
178
|
+
// chegavam ao banco como a mesma linha:
|
|
179
|
+
//
|
|
180
|
+
// ENOENT o arquivo não existe -> criar, ou corrigir o caminho
|
|
181
|
+
// EACCES existe, mas sem permissão -> permissão, ou outro caminho
|
|
182
|
+
// EISDIR pediram para ler um diretório -> usar listar_diretorio
|
|
183
|
+
//
|
|
184
|
+
// As três eram `not_found`. O mesmo vale para o código de saída: 127 é comando que não
|
|
185
|
+
// existe, 126 é arquivo sem permissão de execução, 1 é o programa rodou e reprovou.
|
|
186
|
+
//
|
|
187
|
+
// Nada aqui carrega conteúdo de usuário: errno é vocabulário do sistema operacional e sai
|
|
188
|
+
// de uma lista fechada, exitCode é um inteiro e hadStderr é booleano.
|
|
189
|
+
const ERRNOS_CONHECIDOS = new Set(['ENOENT', 'EACCES', 'EPERM', 'EISDIR', 'ENOTDIR', 'EEXIST',
|
|
190
|
+
'ENOTEMPTY', 'ENOSPC', 'EROFS', 'EMFILE', 'ENFILE', 'ENAMETOOLONG', 'EBUSY', 'EIO', 'EINVAL',
|
|
191
|
+
'EAGAIN', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'ENETUNREACH',
|
|
192
|
+
'EADDRINUSE', 'ENOTFOUND', 'EAI_AGAIN']);
|
|
193
|
+
|
|
194
|
+
// Lista fechada, e não um regex solto: `\bE[A-Z]+\b` casaria com a palavra "ERROR", que
|
|
195
|
+
// aparece em quase toda mensagem de falha, e o campo viraria ruído em vez de sinal.
|
|
196
|
+
function extractErrno(msg) {
|
|
197
|
+
const candidatos = String(msg || '').toUpperCase().match(/\bE[A-Z_]{2,11}\b/g);
|
|
198
|
+
if (!candidatos) return '';
|
|
199
|
+
for (const c of candidatos) if (ERRNOS_CONHECIDOS.has(c)) return c;
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
|
|
173
203
|
function classifyToolResult(raw) {
|
|
174
|
-
if (raw == null)
|
|
204
|
+
if (raw == null) {
|
|
205
|
+
return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: '',
|
|
206
|
+
exitCode: -1, errno: '', hadStderr: false };
|
|
207
|
+
}
|
|
175
208
|
// Alguns adaptadores legados retornam falhas como texto. Tratar todo texto como sucesso
|
|
176
209
|
// permitia que "ERRO ao executar..." virasse etapa concluída. Mantém strings normais como
|
|
177
210
|
// sucesso, mas reconhece prefixos inequívocos de falha/bloqueio.
|
|
@@ -181,13 +214,17 @@ function classifyToolResult(raw) {
|
|
|
181
214
|
if (failedText) {
|
|
182
215
|
const errorClass = classifyError(text);
|
|
183
216
|
return { ok: false, status: errorClass === 'blocked' ? 'blocked' : 'failed', errorClass,
|
|
184
|
-
retryable: RETRYABLE_CLASSES.has(errorClass), evidence: text.replace(/\s+/g, ' ').slice(0, 300)
|
|
217
|
+
retryable: RETRYABLE_CLASSES.has(errorClass), evidence: text.replace(/\s+/g, ' ').slice(0, 300),
|
|
218
|
+
exitCode: -1, errno: extractErrno(text), hadStderr: false };
|
|
185
219
|
}
|
|
186
|
-
return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: text.slice(0, 200)
|
|
220
|
+
return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: text.slice(0, 200),
|
|
221
|
+
exitCode: -1, errno: '', hadStderr: false };
|
|
187
222
|
}
|
|
188
223
|
const exit = raw.codigo != null ? raw.codigo : (raw.exitCode != null ? raw.exitCode : null);
|
|
189
224
|
const errMsg = raw.erro || raw.error || (exit != null && exit !== 0 ? (raw.stderr || raw.stdout || ('exit ' + exit)) : '');
|
|
190
225
|
const failed = !!raw.erro || !!raw.error || (exit != null && exit !== 0);
|
|
226
|
+
const exitCode = exit != null && Number.isFinite(Number(exit)) ? Number(exit) : -1;
|
|
227
|
+
const hadStderr = !!(raw.stderr && String(raw.stderr).trim());
|
|
191
228
|
if (failed) {
|
|
192
229
|
const errorClass = classifyError(errMsg);
|
|
193
230
|
return {
|
|
@@ -196,10 +233,14 @@ function classifyToolResult(raw) {
|
|
|
196
233
|
errorClass,
|
|
197
234
|
retryable: RETRYABLE_CLASSES.has(errorClass),
|
|
198
235
|
evidence: String(errMsg).replace(/\s+/g, ' ').slice(0, 300),
|
|
236
|
+
exitCode,
|
|
237
|
+
errno: extractErrno(errMsg),
|
|
238
|
+
hadStderr,
|
|
199
239
|
};
|
|
200
240
|
}
|
|
201
241
|
const ev = raw.written || raw.created || raw.resumo || raw.arquivo || (raw.stdout != null ? String(raw.stdout).slice(0, 200) : '') || (raw.total != null ? (raw.total + ' resultado(s)') : '');
|
|
202
|
-
return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: String(ev || '').replace(/\s+/g, ' ').slice(0, 200)
|
|
242
|
+
return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: String(ev || '').replace(/\s+/g, ' ').slice(0, 200),
|
|
243
|
+
exitCode, errno: '', hadStderr };
|
|
203
244
|
}
|
|
204
245
|
|
|
205
246
|
// ── SEGREDOS: nunca imprimir chave/token na saída (B18) ───────────────────────
|
|
@@ -18,6 +18,54 @@ function sanitizePattern(value) {
|
|
|
18
18
|
.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// ── Sinal estruturado, de vocabulário FECHADO ────────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// Estes quatro campos entraram em 12/09/2026, junto com as colunas correspondentes no
|
|
24
|
+
// servidor. Antes, o incidente carregava três categorias (ferramenta, classe, estágio) e
|
|
25
|
+
// um `pattern` derivado da própria classe — que, por derivar dela, não acrescentava
|
|
26
|
+
// entropia nenhuma ao fingerprint. ENOENT e EACCES na mesma ferramenta viravam a MESMA
|
|
27
|
+
// linha no banco, e o revisor automático respondia, com razão, que os dados agregados não
|
|
28
|
+
// permitiam determinar a causa.
|
|
29
|
+
//
|
|
30
|
+
// O vocabulário é fechado de propósito, e validado AQUI e de novo no servidor: são os dois
|
|
31
|
+
// únicos lugares em que um valor inesperado poderia virar texto livre, e texto livre nesta
|
|
32
|
+
// tabela é conteúdo de usuário vazando.
|
|
33
|
+
const ERRNOS_CONHECIDOS = new Set(['ENOENT', 'EACCES', 'EPERM', 'EISDIR', 'ENOTDIR', 'EEXIST',
|
|
34
|
+
'ENOTEMPTY', 'ENOSPC', 'EROFS', 'EMFILE', 'ENFILE', 'ENAMETOOLONG', 'EBUSY', 'EIO', 'EINVAL',
|
|
35
|
+
'EAGAIN', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'ENETUNREACH',
|
|
36
|
+
'EADDRINUSE', 'ENOTFOUND', 'EAI_AGAIN']);
|
|
37
|
+
|
|
38
|
+
function safeErrno(value) {
|
|
39
|
+
const code = String(value || '').trim().toUpperCase().replace(/[^A-Z_0-9]/g, '').slice(0, 20);
|
|
40
|
+
return ERRNOS_CONHECIDOS.has(code) ? code : '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safeExitCode(value) {
|
|
44
|
+
if (value === null || value === undefined || value === '') return -1;
|
|
45
|
+
const n = Number(value);
|
|
46
|
+
if (!Number.isFinite(n)) return -1;
|
|
47
|
+
return Math.max(-1, Math.min(255, Math.trunc(n)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Relativo ou absoluto responde a pergunta que o revisor fez em duas propostas diferentes:
|
|
51
|
+
// o harness errou o CAMINHO, ou errou o DIRETÓRIO DE TRABALHO? São correções distintas, e
|
|
52
|
+
// hoje as duas chegam como `not_found`.
|
|
53
|
+
//
|
|
54
|
+
// A derivação acontece aqui, no CLI, onde os argumentos da ferramenta existem — e só o
|
|
55
|
+
// resultado ('relative' | 'absolute') viaja. O caminho em si nunca sai da máquina.
|
|
56
|
+
function pathKindOf(args) {
|
|
57
|
+
if (!args || typeof args !== 'object') return '';
|
|
58
|
+
const alvo = args.caminho || args.arquivo || args.diretorio || args.origem || '';
|
|
59
|
+
const s = String(alvo).trim();
|
|
60
|
+
if (!s) return '';
|
|
61
|
+
return /^(?:[a-zA-Z]:[\\/]|\\\\|\/)/.test(s) ? 'absolute' : 'relative';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function safePathKind(value) {
|
|
65
|
+
const kind = String(value || '').trim().toLowerCase();
|
|
66
|
+
return kind === 'relative' || kind === 'absolute' ? kind : '';
|
|
67
|
+
}
|
|
68
|
+
|
|
21
69
|
function buildEvents(toolErrors = [], actions = [], context = {}) {
|
|
22
70
|
const recoveredTools = new Set((actions || []).map(action => action && action.name).filter(Boolean));
|
|
23
71
|
const seen = new Set(); const events = [];
|
|
@@ -26,10 +74,18 @@ function buildEvents(toolErrors = [], actions = [], context = {}) {
|
|
|
26
74
|
const errorClass = String(error.errorClass || error.class || 'unknown').slice(0, 80);
|
|
27
75
|
const pattern = sanitizePattern(error.evidence || error.message || '');
|
|
28
76
|
const stage = String(error.stage || 'execution').slice(0, 40);
|
|
29
|
-
const
|
|
77
|
+
const errno = safeErrno(error.errno);
|
|
78
|
+
const exitCode = safeExitCode(error.exitCode);
|
|
79
|
+
const pathKind = safePathKind(error.pathKind);
|
|
80
|
+
const hadStderr = error.hadStderr === true;
|
|
81
|
+
// O fingerprint inclui o sinal novo: sem isso, duas causas diferentes continuariam
|
|
82
|
+
// deduplicadas aqui antes mesmo de chegar ao servidor.
|
|
83
|
+
const fingerprint = crypto.createHash('sha256')
|
|
84
|
+
.update([tool, errorClass, stage, pattern, errno, String(exitCode), pathKind].join('|')).digest('hex');
|
|
30
85
|
if (seen.has(fingerprint)) continue;
|
|
31
86
|
seen.add(fingerprint);
|
|
32
87
|
events.push({ tool, errorClass, pattern, stage, fingerprint,
|
|
88
|
+
errno, exitCode, pathKind, hadStderr,
|
|
33
89
|
retryable: error.retryable === true, recovered: recoveredTools.has(tool),
|
|
34
90
|
surface: context.surface || 'cli', appVersion: context.appVersion || 'unknown', model: context.model || 'unknown' });
|
|
35
91
|
if (events.length >= 20) break;
|
|
@@ -42,4 +98,4 @@ function enabled(config = {}, env = process.env) {
|
|
|
42
98
|
return config.diagnosticsEnabled !== false;
|
|
43
99
|
}
|
|
44
100
|
|
|
45
|
-
module.exports = { sanitizePattern, buildEvents, enabled };
|
|
101
|
+
module.exports = { sanitizePattern, buildEvents, enabled, pathKindOf };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-smart-cli",
|
|
3
|
-
"version": "0.97.
|
|
3
|
+
"version": "0.97.66",
|
|
4
4
|
"description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ts": "bin/ts.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "node test/durable-operation.test.js && node test/mission-tool-cache.test.js && node test/content-quality-runtime-files.test.js && node test/personal-agent-llm.test.js && node test/personal-inspector-efficiency.test.js && node test/personal-inspector-context.test.js && node test/planner-fallback.test.js && node test/cli-agent-regressions.test.js && node test/harness-hardening.test.js"
|
|
9
|
+
"test": "node test/durable-operation.test.js && node test/mission-tool-cache.test.js && node test/content-quality-runtime-files.test.js && node test/personal-agent-llm.test.js && node test/personal-inspector-efficiency.test.js && node test/personal-inspector-context.test.js && node test/planner-fallback.test.js && node test/cli-agent-regressions.test.js && node test/harness-hardening.test.js && node test/evolution-telemetry.test.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin",
|