ai-test-loop 1.0.1 → 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 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?utm_source=npm&utm_medium=readme&utm_campaign=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?utm_source=npm&utm_medium=readme&utm_campaign=ai-test-loop**
130
- - Catalog: **https://skills.seu.pet/?utm_source=npm&utm_medium=readme&utm_campaign=ai-test-loop**
131
- - Want it installed and wired into your repo: **https://skills.seu.pet/servico?utm_source=npm&utm_medium=readme&utm_campaign=ai-test-loop**
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?utm_source=npm&utm_medium=readme&utm_campaign=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 só acha o interpretador e repassa os argumentos.
5
- const { spawn } = require("node:child_process");
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
- process.exit(2);
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, ...process.argv.slice(2)], { stdio: "inherit" });
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) => process.exit(signal ? 1 : code ?? 0));
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.1",
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
+ }