ai-test-loop 1.0.0 → 1.0.2
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/README.md +61 -5
- package/bin/cli.js +153 -5
- package/package.json +2 -2
- package/scripts/__pycache__/loop_runner.cpython-311.pyc +0 -0
package/README.md
CHANGED
|
@@ -11,10 +11,19 @@ optional output patterns decide pass/fail. The model never gets to *say* the wor
|
|
|
11
11
|
to *show* it.
|
|
12
12
|
|
|
13
13
|
> **Quer isso pronto para o seu time?** Os packs com entrega por e-mail, o gate com relatório
|
|
14
|
-
> e o guia de verificação estão em **https://skills.seu.pet/gratis/ai-test-loop**
|
|
14
|
+
> e o guia de verificação estão em **https://skills.seu.pet/gratis/ai-test-loop?utm_source=github&utm_medium=readme&utm_campaign=ai-test-loop**
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## Install as a package
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install agent-test-loop
|
|
22
|
+
agent-test-loop --config loop.json --dry-run
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
PyPI: https://pypi.org/project/agent-test-loop/
|
|
26
|
+
|
|
18
27
|
## The problem
|
|
19
28
|
|
|
20
29
|
Ask an agent "is this done?" and it answers about its own reasoning. The same context that produced
|
|
@@ -29,6 +38,26 @@ the artifact also decides it is correct, so real defects survive to delivery:
|
|
|
29
38
|
Running "one more review pass" does not help, because the reviewer and the builder are the same
|
|
30
39
|
process. Separating them is the whole trick.
|
|
31
40
|
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
Two packages, same engine — pick the ecosystem you already have:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install agent-test-loop # Python 3.9+ (PyPI)
|
|
47
|
+
npx ai-test-loop --help # Node 18+ (npm)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Both ship the same `loop_runner.py` and the same example config, so the gate you run locally is the
|
|
51
|
+
gate CI runs. Verify with a dry run — it prints the checks and weights without executing anything:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
agent-test-loop --config templates/loop.example.json --dry-run
|
|
55
|
+
npx ai-test-loop --config templates/loop.example.json --dry-run
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Need the full QA gate — visual diff, training guard, delivery evidence — instead of the free runner?
|
|
59
|
+
See the [full pack](https://skills.seu.pet/p/pack-completo?utm_source=github&utm_medium=readme&utm_campaign=ai-test-loop).
|
|
60
|
+
|
|
32
61
|
## Quickstart
|
|
33
62
|
|
|
34
63
|
```bash
|
|
@@ -105,6 +134,33 @@ user-facing surface. Each returns strict JSON:
|
|
|
105
134
|
Consensus → deliver. Majority disagree → reopen the loop with the combined gaps. Prompt in
|
|
106
135
|
`references/reviewer-prompt.md`, starting rubric in `references/scoring-rubric.md`.
|
|
107
136
|
|
|
137
|
+
## Telemetry (opt-out, and what it is not)
|
|
138
|
+
|
|
139
|
+
The `npx ai-test-loop` wrapper sends **one anonymous event per run** so the project knows whether
|
|
140
|
+
the tool is actually used — downloads alone are not usage (npm counts mirrors and CI, and the
|
|
141
|
+
publish-day spike was 270 of 555 downloads in a month).
|
|
142
|
+
|
|
143
|
+
What is sent: event type (`run`/`help`/`version`/`erro`), package version, Node major, Python major,
|
|
144
|
+
OS name, duration in ms, and — only on a real run — the final score and the number of gaps.
|
|
145
|
+
|
|
146
|
+
What is **never** sent: your IP (not logged server-side), e-mail, project path, file names, the
|
|
147
|
+
content of your checks, the command output, or anything from your code.
|
|
148
|
+
|
|
149
|
+
The install id is a random UUID written to `~/.ai-test-loop/telemetry-id` once. It counts machines,
|
|
150
|
+
not people, and is not linked to anything else. Delete that file and you are a new machine.
|
|
151
|
+
|
|
152
|
+
Turn it off however you prefer:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
ai-test-loop --no-telemetry ... # that run only
|
|
156
|
+
export AI_TEST_LOOP_NO_TELEMETRY=1 # permanently, in your shell profile
|
|
157
|
+
export DO_NOT_TRACK=1 # the ecosystem-wide convention, also respected
|
|
158
|
+
# CI=1 is detected automatically: pipelines are not people
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Telemetry never blocks and never waits: if the network is down the CLI runs exactly the same, with
|
|
162
|
+
no delay and no message.
|
|
163
|
+
|
|
108
164
|
## When not to use it
|
|
109
165
|
|
|
110
166
|
Throwaway exploration, spike branches, and one-line answers where the cost of the loop exceeds the
|
|
@@ -126,9 +182,9 @@ cost of being wrong. It is for artifacts a human will rely on.
|
|
|
126
182
|
This repo is the free, complete tool. The store has the ready-made packs for the cases where
|
|
127
183
|
writing the checks is the slow part — CI gates, visual regression, MCP servers, MLOps guards:
|
|
128
184
|
|
|
129
|
-
- Free download / e-mail delivery: **https://skills.seu.pet/gratis/ai-test-loop**
|
|
130
|
-
- Catalog: **https://skills.seu.pet**
|
|
131
|
-
- Want it installed and wired into your repo: **https://skills.seu.pet/servico**
|
|
185
|
+
- Free download / e-mail delivery: **https://skills.seu.pet/gratis/ai-test-loop?utm_source=github&utm_medium=readme&utm_campaign=ai-test-loop**
|
|
186
|
+
- Catalog: **https://skills.seu.pet/?utm_source=github&utm_medium=readme&utm_campaign=ai-test-loop**
|
|
187
|
+
- Want it installed and wired into your repo: **https://skills.seu.pet/servico?utm_source=github&utm_medium=readme&utm_campaign=ai-test-loop**
|
|
132
188
|
|
|
133
189
|
## License
|
|
134
190
|
|
|
@@ -142,4 +198,4 @@ O loop de teste para agentes: o construtor não assina o próprio trabalho. Um c
|
|
|
142
198
|
de verdade, dá nota 0–100, repete até o limite e barra a entrega se houver achado de segurança.
|
|
143
199
|
`python3 scripts/loop_runner.py --config loop.json` — sem chave de API e sem dependência.
|
|
144
200
|
|
|
145
|
-
Versão com entrega por e-mail e os packs prontos: https://skills.seu.pet/gratis/ai-test-loop
|
|
201
|
+
Versão com entrega por e-mail e os packs prontos: https://skills.seu.pet/gratis/ai-test-loop?utm_source=github&utm_medium=readme&utm_campaign=ai-test-loop
|
package/bin/cli.js
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Wrapper do critic determinístico (Python) para uso via `npx ai-test-loop`.
|
|
3
|
+
//
|
|
3
4
|
// Por que existe: o runner é Python puro (stdlib) e a porta de entrada que dev
|
|
4
|
-
// usa é `npx`. O wrapper
|
|
5
|
-
|
|
5
|
+
// usa é `npx`. O wrapper acha o interpretador, repassa os argumentos e — desde
|
|
6
|
+
// 1.0.2 — registra UM evento anônimo por execução, para o pacote deixar de ser
|
|
7
|
+
// cego (555 downloads no mês e nenhum sinal de uso real).
|
|
8
|
+
//
|
|
9
|
+
// Privacidade: nada de IP gravado, nada de e-mail, nada de caminho de projeto,
|
|
10
|
+
// NUNCA o conteúdo do comando ou do código do usuário. Só: evento
|
|
11
|
+
// (run/help/erro/version), versão, versão de node/python, SO, duração e — quando
|
|
12
|
+
// é um run de verdade — score e número de gaps.
|
|
13
|
+
//
|
|
14
|
+
// Opt-out, nesta ordem:
|
|
15
|
+
// --no-telemetry naquele run
|
|
16
|
+
// AI_TEST_LOOP_NO_TELEMETRY=1 permanente
|
|
17
|
+
// DO_NOT_TRACK=1 padrão do ecossistema
|
|
18
|
+
// CI=1 integração contínua não é pessoa: não conta
|
|
19
|
+
// Se a rede falhar ou o servidor estiver fora, o CLI segue normal: telemetria
|
|
20
|
+
// nunca bloqueia, nunca espera e nunca aparece na saída.
|
|
21
|
+
const { spawn, execSync } = require("node:child_process");
|
|
6
22
|
const path = require("node:path");
|
|
7
23
|
const fs = require("node:fs");
|
|
24
|
+
const os = require("node:os");
|
|
25
|
+
const https = require("node:https");
|
|
26
|
+
const crypto = require("node:crypto");
|
|
27
|
+
|
|
28
|
+
const VERSAO = require(path.join(__dirname, "..", "package.json")).version;
|
|
29
|
+
const URL_EVENTO = process.env.AI_TEST_LOOP_TELEMETRY_URL
|
|
30
|
+
|| "https://skills.seu.pet/api/cli";
|
|
31
|
+
const TIMEOUT_MS = 2500;
|
|
8
32
|
|
|
9
33
|
const script = path.join(__dirname, "..", "scripts", "loop_runner.py");
|
|
10
34
|
if (!fs.existsSync(script)) {
|
|
@@ -12,18 +36,142 @@ if (!fs.existsSync(script)) {
|
|
|
12
36
|
process.exit(2);
|
|
13
37
|
}
|
|
14
38
|
|
|
39
|
+
const argv = process.argv.slice(2);
|
|
40
|
+
|
|
41
|
+
// `--no-telemetry` e flag do WRAPPER, nao do runner Python: repassar faz o
|
|
42
|
+
// argparse morrer com "unrecognized arguments" (achado no smoke test, 08/11).
|
|
43
|
+
// Por isso ele sai da lista antes de qualquer spawn.
|
|
44
|
+
const argvRunner = argv.filter((a) => a !== "--no-telemetry");
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------- telemetria
|
|
47
|
+
function desligada() {
|
|
48
|
+
if (argv.includes("--no-telemetry")) return true;
|
|
49
|
+
const v = (k) => String(process.env[k] || "").toLowerCase();
|
|
50
|
+
if (["1", "true", "yes"].includes(v("AI_TEST_LOOP_NO_TELEMETRY"))) return true;
|
|
51
|
+
if (["1", "true", "yes"].includes(v("DO_NOT_TRACK"))) return true;
|
|
52
|
+
if (process.env.CI) return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Id aleatório local: conta MÁQUINAS, não pessoas, e não liga a nada do usuário.
|
|
57
|
+
// Fica em ~/.ai-test-loop/telemetry-id. Apagar o arquivo = id novo.
|
|
58
|
+
function idInstalacao() {
|
|
59
|
+
try {
|
|
60
|
+
const dir = path.join(os.homedir(), ".ai-test-loop");
|
|
61
|
+
const arq = path.join(dir, "telemetry-id");
|
|
62
|
+
if (fs.existsSync(arq)) {
|
|
63
|
+
const lido = fs.readFileSync(arq, "utf8").trim();
|
|
64
|
+
if (/^[0-9a-f-]{36}$/.test(lido)) return lido;
|
|
65
|
+
}
|
|
66
|
+
const novo = crypto.randomUUID();
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
fs.writeFileSync(arq, novo + "\n", { mode: 0o600 });
|
|
69
|
+
return novo;
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return ""; // sem id, o servidor grava o evento como anônimo
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function modoDe(args) {
|
|
76
|
+
if (args.includes("--help") || args.includes("-h")) return "help";
|
|
77
|
+
if (args.includes("--version") || args.includes("-v")) return "version";
|
|
78
|
+
if (args.includes("--dry-run")) return "dry-run";
|
|
79
|
+
if (args.includes("--show-state")) return "show-state";
|
|
80
|
+
return "run";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function enviar(payload) {
|
|
84
|
+
// Devolve Promise: `process.exit()` mata o processo antes do POST terminar, e
|
|
85
|
+
// o evento se perdia (medido 08/11: dry-run nao gravava nada). Quem chama
|
|
86
|
+
// espera com `.finally` antes de sair.
|
|
87
|
+
if (desligada()) return Promise.resolve();
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
try {
|
|
90
|
+
const corpo = Buffer.from(JSON.stringify(payload));
|
|
91
|
+
const u = new URL(URL_EVENTO);
|
|
92
|
+
const lib = u.protocol === "http:" ? require("node:http") : https;
|
|
93
|
+
const req = lib.request({
|
|
94
|
+
hostname: u.hostname,
|
|
95
|
+
port: u.port || (u.protocol === "http:" ? 80 : 443),
|
|
96
|
+
path: u.pathname + u.search,
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
"content-type": "application/json",
|
|
100
|
+
"content-length": corpo.length,
|
|
101
|
+
"user-agent": `ai-test-loop/${VERSAO}`,
|
|
102
|
+
},
|
|
103
|
+
timeout: TIMEOUT_MS,
|
|
104
|
+
}, (res) => { res.resume(); res.on("end", resolve); });
|
|
105
|
+
req.on("error", () => resolve()); // rede ruim nunca afeta o usuário
|
|
106
|
+
req.on("timeout", () => { req.destroy(); resolve(); });
|
|
107
|
+
req.end(corpo);
|
|
108
|
+
} catch (_) { resolve(); } // telemetria é acessório: falha em silêncio
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function base(evento, modo) {
|
|
113
|
+
return {
|
|
114
|
+
instalacao: idInstalacao(),
|
|
115
|
+
evento,
|
|
116
|
+
versao: VERSAO,
|
|
117
|
+
node: process.versions.node.split(".")[0],
|
|
118
|
+
so: process.platform,
|
|
119
|
+
modo,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------- execução
|
|
124
|
+
const modo = modoDe(argv);
|
|
125
|
+
|
|
126
|
+
// --help e --version saem antes de qualquer run: o evento mostra que a pessoa
|
|
127
|
+
// chegou a perguntar como usar, que é o degrau mais barato do funil.
|
|
128
|
+
const envioInicial = (modo === "help" || modo === "version")
|
|
129
|
+
? enviar(base(modo, modo))
|
|
130
|
+
: Promise.resolve();
|
|
131
|
+
|
|
15
132
|
const candidatos = process.env.AI_TEST_LOOP_PYTHON
|
|
16
133
|
? [process.env.AI_TEST_LOOP_PYTHON]
|
|
17
134
|
: ["python3", "python"];
|
|
18
135
|
|
|
136
|
+
const inicio = Date.now();
|
|
137
|
+
let saidaScore = ""; // o runner imprime SCORE=<n> na saída
|
|
138
|
+
let saidaGaps = "";
|
|
139
|
+
|
|
19
140
|
function tentar(i) {
|
|
20
141
|
if (i >= candidatos.length) {
|
|
21
142
|
console.error("ai-test-loop: Python 3 não encontrado (tente AI_TEST_LOOP_PYTHON=/caminho/python3)");
|
|
22
|
-
|
|
143
|
+
enviar(Object.assign(base("erro", modo), { python: "ausente" }))
|
|
144
|
+
.finally(() => process.exit(2));
|
|
145
|
+
return;
|
|
23
146
|
}
|
|
24
|
-
const p = spawn(candidatos[i], [script, ...
|
|
147
|
+
const p = spawn(candidatos[i], [script, ...argvRunner], { stdio: ["inherit", "pipe", "pipe"] });
|
|
148
|
+
|
|
149
|
+
// Repassa a saída ao usuário E lê SCORE/GAPS para o evento. Precisa ser pipe:
|
|
150
|
+
// com stdio herdado não daria para ler, e o evento ficaria sem o resultado.
|
|
151
|
+
p.stdout.on("data", (b) => {
|
|
152
|
+
process.stdout.write(b);
|
|
153
|
+
const t = b.toString();
|
|
154
|
+
if (!saidaScore) { const m = t.match(/SCORE=(\d+)/); if (m) saidaScore = m[1]; }
|
|
155
|
+
if (!saidaGaps) { const m = t.match(/GAPS=(\d+)/); if (m) saidaGaps = m[1]; }
|
|
156
|
+
});
|
|
157
|
+
p.stderr.on("data", (b) => process.stderr.write(b));
|
|
158
|
+
|
|
25
159
|
p.on("error", () => tentar(i + 1));
|
|
26
|
-
p.on("exit", (code, signal) =>
|
|
160
|
+
p.on("exit", (code, signal) => {
|
|
161
|
+
if (modo !== "help" && modo !== "version") {
|
|
162
|
+
let pyMajor = "";
|
|
163
|
+
try { pyMajor = execSync(`${candidatos[i]} -V`, { encoding: "utf8" }).trim().split(" ")[1].split(".")[0]; }
|
|
164
|
+
catch (_) { /* sem python legível: campo fica vazio */ }
|
|
165
|
+
enviar(Object.assign(base(code === 0 ? "run" : "erro", modo), {
|
|
166
|
+
duracao_ms: Date.now() - inicio,
|
|
167
|
+
score: saidaScore || undefined,
|
|
168
|
+
gaps: saidaGaps || undefined,
|
|
169
|
+
python: pyMajor || undefined,
|
|
170
|
+
})).finally(() => process.exit(signal ? 1 : (code ?? 0)));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
process.exit(signal ? 1 : (code ?? 0));
|
|
174
|
+
});
|
|
27
175
|
}
|
|
28
176
|
|
|
29
177
|
tentar(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-test-loop",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Deterministic critic for AI agents: runs weighed checks for real, scores 0-100 and blocks delivery below the threshold. Stdlib-only runner.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Samuel Medeiros",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"node": ">=16"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"smoke": "node bin/cli.js --config templates/loop.example.json --dry-run"
|
|
37
|
+
"smoke": "node bin/cli.js --config templates/loop.example.json --dry-run --no-telemetry"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
Binary file
|