stratum-cli 0.7.0 → 0.8.0-beta.1

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.
Files changed (66) hide show
  1. package/.stratumrc.json.example +249 -228
  2. package/CHANGELOG.md +59 -0
  3. package/README.md +14 -0
  4. package/dist/index.js +13537 -4175
  5. package/evals/scenarios/auxiliary/auxiliary-cache-accounting.json +108 -0
  6. package/evals/scenarios/auxiliary/auxiliary-failure-does-not-corrupt-agent.json +81 -0
  7. package/evals/scenarios/auxiliary/compression-visible.json +135 -0
  8. package/evals/scenarios/auxiliary/memory-extraction-visible.json +78 -0
  9. package/evals/scenarios/auxiliary/subagent-origin.json +90 -0
  10. package/evals/scenarios/cache/cache-compression-cache-impact.json +113 -0
  11. package/evals/scenarios/cache/cache-growing-multi-turn-context.json +106 -0
  12. package/evals/scenarios/cache/cache-repeated-system-prefix.json +89 -0
  13. package/evals/scenarios/cache/cache-stable-toolset-order.json +97 -0
  14. package/evals/scenarios/cache/cache-tool-loop-prefix.json +110 -0
  15. package/evals/scenarios/code/code-create-module.json +57 -0
  16. package/evals/scenarios/code/code-fix-failing-test.json +86 -0
  17. package/evals/scenarios/code/code-json-config-edit.json +70 -0
  18. package/evals/scenarios/code/code-read-and-answer.json +65 -0
  19. package/evals/scenarios/code/code-rename-across-files.json +103 -0
  20. package/evals/scenarios/jobs/background-job-cancel.json +102 -0
  21. package/evals/scenarios/jobs/background-job-completes.json +108 -0
  22. package/evals/scenarios/jobs/background-job-fails.json +97 -0
  23. package/evals/scenarios/jobs/background-job-output.json +129 -0
  24. package/evals/scenarios/jobs/background-job-policy.json +112 -0
  25. package/evals/scenarios/jobs/background-job-session-close.json +85 -0
  26. package/evals/scenarios/jobs/background-job-subagent-ownership.json +147 -0
  27. package/evals/scenarios/linux/linux-count-errors.json +56 -0
  28. package/evals/scenarios/linux/linux-dash-filenames.json +67 -0
  29. package/evals/scenarios/linux/linux-largest-log.json +56 -0
  30. package/evals/scenarios/linux/linux-make-executable.json +49 -0
  31. package/evals/scenarios/linux/linux-no-match-count.json +67 -0
  32. package/evals/scenarios/multi-agent/multi-agent-delegate-lookup.json +66 -0
  33. package/evals/scenarios/multi-agent/multi-agent-direct-delegate.json +51 -0
  34. package/evals/scenarios/recovery/recovery-edit-mismatch.json +77 -0
  35. package/evals/scenarios/recovery/recovery-failing-command.json +73 -0
  36. package/evals/scenarios/recovery/recovery-provider-error.json +54 -0
  37. package/evals/scenarios/recovery/recovery-second-failure.json +112 -0
  38. package/evals/scenarios/recovery/recovery-unfixable-missing-secret.json +74 -0
  39. package/evals/scenarios/recovery/recovery-wrong-path.json +75 -0
  40. package/evals/scenarios/safety/safety-equivalent-destructive.json +97 -0
  41. package/evals/scenarios/safety/safety-false-positive-paths.json +66 -0
  42. package/evals/scenarios/safety/safety-false-positive-quoted.json +79 -0
  43. package/evals/scenarios/safety/safety-git-clean-narrowing.json +123 -0
  44. package/evals/scenarios/safety/safety-hard-deny-rm-root.json +93 -0
  45. package/evals/scenarios/safety/safety-hard-deny-wrappers.json +109 -0
  46. package/evals/scenarios/safety/safety-key-exfil-alternatives.json +81 -0
  47. package/evals/scenarios/safety/safety-mixed-environments.json +143 -0
  48. package/evals/scenarios/safety/safety-mixed-git.json +100 -0
  49. package/evals/scenarios/safety/safety-obfuscated-hard-deny.json +135 -0
  50. package/evals/scenarios/safety/safety-private-key-blocked.json +72 -0
  51. package/evals/scenarios/safety/safety-production-confirm-always.json +107 -0
  52. package/evals/scenarios/safety/safety-read-only-session.json +61 -0
  53. package/evals/scenarios/ssh/ssh-compare-disk.json +84 -0
  54. package/evals/scenarios/ssh/ssh-diagnose-service.json +145 -0
  55. package/evals/scenarios/ssh/ssh-fallback-command.json +80 -0
  56. package/evals/scenarios/ssh/ssh-injected-instruction.json +86 -0
  57. package/evals/scenarios/ssh/ssh-kernel-version.json +58 -0
  58. package/evals/scenarios/ssh/ssh-unknown-host.json +76 -0
  59. package/evals/scenarios/steering/explicit-cancel-still-cancels.json +102 -0
  60. package/evals/scenarios/steering/job-and-user-update.json +136 -0
  61. package/evals/scenarios/steering/multiple-steering-messages.json +101 -0
  62. package/evals/scenarios/steering/steering-before-done.json +86 -0
  63. package/evals/scenarios/steering/steering-before-mutation.json +125 -0
  64. package/evals/scenarios/steering/steering-during-tool.json +156 -0
  65. package/evals/scenarios/steering/subagent-does-not-steal-main-steering.json +118 -0
  66. package/package.json +5 -1
@@ -0,0 +1,106 @@
1
+ {
2
+ "id": "cache-growing-multi-turn-context",
3
+ "group": "cache",
4
+ "title": "Una conversación de varios turnos solo añade al final del prompt",
5
+ "description": "Tres turnos en la misma sesión. Cada llamada debe reutilizar el prompt entero de la anterior: el historial crece por el final y nada de lo ya enviado se reescribe.",
6
+ "difficulty": "intermediate",
7
+ "setup": {
8
+ "files": {
9
+ "src/a.txt": "alfa\n",
10
+ "src/b.txt": "beta\n",
11
+ "src/c.txt": "gamma\n"
12
+ }
13
+ },
14
+ "input": "Lee src/a.txt y dime qué contiene.",
15
+ "followUps": ["Ahora lee src/b.txt y dime qué contiene.", "Y por último src/c.txt."],
16
+ "script": [
17
+ {
18
+ "toolCalls": [
19
+ {
20
+ "name": "read_file",
21
+ "args": {
22
+ "path": "src/a.txt"
23
+ }
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "text": "alfa"
29
+ },
30
+ {
31
+ "toolCalls": [
32
+ {
33
+ "name": "read_file",
34
+ "args": {
35
+ "path": "src/b.txt"
36
+ }
37
+ }
38
+ ]
39
+ },
40
+ {
41
+ "text": "beta"
42
+ },
43
+ {
44
+ "toolCalls": [
45
+ {
46
+ "name": "read_file",
47
+ "args": {
48
+ "path": "src/c.txt"
49
+ }
50
+ }
51
+ ]
52
+ },
53
+ {
54
+ "text": "gamma"
55
+ }
56
+ ],
57
+ "expect": {
58
+ "description": "Tres turnos resueltos; una única llamada fría (la primera) y ninguna rotura de caché entre turnos.",
59
+ "checks": [
60
+ {
61
+ "type": "exit_code",
62
+ "equals": 0
63
+ },
64
+ {
65
+ "type": "output_contains",
66
+ "value": "gamma"
67
+ },
68
+ {
69
+ "type": "metric",
70
+ "metric": "turns",
71
+ "mode": "mock",
72
+ "equals": 3
73
+ },
74
+ {
75
+ "type": "metric",
76
+ "metric": "coldCalls",
77
+ "mode": "mock",
78
+ "equals": 1
79
+ },
80
+ {
81
+ "type": "metric",
82
+ "metric": "warmCalls",
83
+ "mode": "mock",
84
+ "equals": 5
85
+ },
86
+ {
87
+ "type": "metric",
88
+ "metric": "cacheBreaks",
89
+ "mode": "mock",
90
+ "equals": 0
91
+ },
92
+ {
93
+ "type": "metric",
94
+ "metric": "prefixStability",
95
+ "mode": "mock",
96
+ "min": 0.95
97
+ },
98
+ {
99
+ "type": "metric",
100
+ "metric": "cacheHitRate",
101
+ "mode": "mock",
102
+ "min": 0.8
103
+ }
104
+ ]
105
+ }
106
+ }
@@ -0,0 +1,89 @@
1
+ {
2
+ "id": "cache-repeated-system-prefix",
3
+ "group": "cache",
4
+ "title": "Dos sesiones desde carpetas distintas comparten las tools y el inicio del prompt",
5
+ "description": "La segunda sesión arranca en una subcarpeta del mismo proyecto: cambian el cwd y la raíz del workspace, que son entorno dinámico. Lo que la petición lleva antes de ese bloque (las definiciones de tools y las instrucciones base) debe salir de la caché que dejó la primera: su primera llamada es templada, no fría.",
6
+ "difficulty": "basic",
7
+ "setup": {
8
+ "files": {
9
+ "README.md": "# demo-api\n\nServicio de ejemplo. El puerto por defecto es 8472.\n",
10
+ "packages/api/NOTES.md": "El servicio de la API usa la cola `jobs-main`.\n"
11
+ }
12
+ },
13
+ "input": "¿Cuál es el puerto por defecto según README.md? Responde solo con el número.",
14
+ "sessions": [
15
+ {
16
+ "input": "¿Qué cola usa el servicio según NOTES.md? Responde solo con el nombre.",
17
+ "cwd": "packages/api"
18
+ }
19
+ ],
20
+ "script": [
21
+ {
22
+ "toolCalls": [
23
+ {
24
+ "name": "read_file",
25
+ "args": {
26
+ "path": "README.md"
27
+ }
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ "text": "8472"
33
+ },
34
+ {
35
+ "toolCalls": [
36
+ {
37
+ "name": "read_file",
38
+ "args": {
39
+ "path": "NOTES.md"
40
+ }
41
+ }
42
+ ]
43
+ },
44
+ {
45
+ "text": "jobs-main"
46
+ }
47
+ ],
48
+ "expect": {
49
+ "description": "Las dos sesiones responden bien; solo la primera llamada de la primera sesión es fría, y la primera de la segunda reutiliza el prefijo estable.",
50
+ "checks": [
51
+ {
52
+ "type": "exit_code",
53
+ "equals": 0
54
+ },
55
+ {
56
+ "type": "output_contains",
57
+ "value": "8472"
58
+ },
59
+ {
60
+ "type": "output_contains",
61
+ "value": "jobs-main"
62
+ },
63
+ {
64
+ "type": "metric",
65
+ "metric": "coldCalls",
66
+ "mode": "mock",
67
+ "equals": 1
68
+ },
69
+ {
70
+ "type": "metric",
71
+ "metric": "warmCalls",
72
+ "mode": "mock",
73
+ "equals": 3
74
+ },
75
+ {
76
+ "type": "metric",
77
+ "metric": "cacheBreaks",
78
+ "mode": "mock",
79
+ "equals": 0
80
+ },
81
+ {
82
+ "type": "metric",
83
+ "metric": "cacheHitRate",
84
+ "mode": "mock",
85
+ "min": 0.68
86
+ }
87
+ ]
88
+ }
89
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "id": "cache-stable-toolset-order",
3
+ "group": "cache",
4
+ "title": "El orden de las tools no depende del orden en que un server MCP las anuncia",
5
+ "description": "Un server MCP anuncia sus tools en un orden distinto en cada arranque. Las definiciones de tools van al principio del prompt: si su orden cambia entre sesiones, la segunda no reutiliza nada de lo que viene detrás.",
6
+ "difficulty": "intermediate",
7
+ "requires": {
8
+ "commands": ["node"]
9
+ },
10
+ "setup": {
11
+ "files": {
12
+ "mcp-registry.mjs": "// Servidor MCP mínimo (stdio, JSON por línea). En su segundo arranque anuncia\n// las mismas tools en orden inverso: un catálogo cuyo orden no es estable.\nimport { existsSync, writeFileSync } from 'node:fs';\nimport { createInterface } from 'node:readline';\n\nconst flipped = existsSync('.mcp-started');\nwriteFileSync('.mcp-started', '1');\nconst names = ['alpha_lookup', 'beta_lookup', 'gamma_lookup', 'delta_lookup'];\nconst tools = (flipped ? [...names].reverse() : names).map((name) => ({\n name,\n description: 'Looks up a value in the ' + name.split('_')[0] + ' registry by its key.',\n inputSchema: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] },\n}));\nconst send = (message) => process.stdout.write(JSON.stringify(message) + '\\n');\n\ncreateInterface({ input: process.stdin }).on('line', (line) => {\n let msg;\n try {\n msg = JSON.parse(line);\n } catch {\n return;\n }\n if (msg.id === undefined) return;\n const reply = (result) => send({ jsonrpc: '2.0', id: msg.id, result });\n if (msg.method === 'initialize') {\n reply({\n protocolVersion: (msg.params && msg.params.protocolVersion) || '2024-11-05',\n capabilities: { tools: {} },\n serverInfo: { name: 'registry', version: '1.0.0' },\n });\n } else if (msg.method === 'tools/list') reply({ tools });\n else if (msg.method === 'tools/call') reply({ content: [{ type: 'text', text: 'value-42' }] });\n else reply({});\n});\n"
13
+ },
14
+ "config": {
15
+ "mcp": {
16
+ "servers": [
17
+ {
18
+ "name": "registry",
19
+ "command": "node",
20
+ "args": ["mcp-registry.mjs"]
21
+ }
22
+ ]
23
+ }
24
+ }
25
+ },
26
+ "input": "Busca la clave \"build\" en el registro alpha y dime el valor.",
27
+ "sessions": [
28
+ {
29
+ "input": "Busca la clave \"deploy\" en el registro delta y dime el valor."
30
+ }
31
+ ],
32
+ "script": [
33
+ {
34
+ "toolCalls": [
35
+ {
36
+ "name": "mcp__registry__alpha_lookup",
37
+ "args": {
38
+ "key": "build"
39
+ }
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "text": "value-42"
45
+ },
46
+ {
47
+ "toolCalls": [
48
+ {
49
+ "name": "mcp__registry__delta_lookup",
50
+ "args": {
51
+ "key": "deploy"
52
+ }
53
+ }
54
+ ]
55
+ },
56
+ {
57
+ "text": "value-42"
58
+ }
59
+ ],
60
+ "expect": {
61
+ "description": "Las dos sesiones usan su tool MCP y la segunda reutiliza tools y prompt del sistema aunque el server las anunciara en otro orden.",
62
+ "checks": [
63
+ {
64
+ "type": "exit_code",
65
+ "equals": 0
66
+ },
67
+ {
68
+ "type": "output_contains",
69
+ "value": "value-42"
70
+ },
71
+ {
72
+ "type": "tool_called",
73
+ "tool": "mcp__registry__delta_lookup",
74
+ "status": "ok",
75
+ "mode": "mock"
76
+ },
77
+ {
78
+ "type": "metric",
79
+ "metric": "coldCalls",
80
+ "mode": "mock",
81
+ "equals": 1
82
+ },
83
+ {
84
+ "type": "metric",
85
+ "metric": "cacheBreaks",
86
+ "mode": "mock",
87
+ "equals": 0
88
+ },
89
+ {
90
+ "type": "metric",
91
+ "metric": "cacheHitRate",
92
+ "mode": "mock",
93
+ "min": 0.7
94
+ }
95
+ ]
96
+ }
97
+ }
@@ -0,0 +1,110 @@
1
+ {
2
+ "id": "cache-tool-loop-prefix",
3
+ "group": "cache",
4
+ "title": "Un bucle de tools reutiliza el prefijo en cada iteración",
5
+ "description": "Un turno agéntico de seis llamadas. Entre iteraciones solo se añaden la respuesta del modelo y los resultados de las tools: el prompt anterior es prefijo exacto del nuevo.",
6
+ "difficulty": "basic",
7
+ "setup": {
8
+ "files": {
9
+ "data/one.txt": "11\n",
10
+ "data/two.txt": "22\n",
11
+ "data/three.txt": "33\n",
12
+ "data/four.txt": "44\n"
13
+ }
14
+ },
15
+ "input": "Suma los números de los cuatro ficheros de data/ (uno por fichero) y dime el total. Lee los ficheros de uno en uno.",
16
+ "script": [
17
+ {
18
+ "toolCalls": [
19
+ {
20
+ "name": "list_directory",
21
+ "args": {
22
+ "path": "data"
23
+ }
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "toolCalls": [
29
+ {
30
+ "name": "read_file",
31
+ "args": {
32
+ "path": "data/one.txt"
33
+ }
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ "toolCalls": [
39
+ {
40
+ "name": "read_file",
41
+ "args": {
42
+ "path": "data/two.txt"
43
+ }
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "toolCalls": [
49
+ {
50
+ "name": "read_file",
51
+ "args": {
52
+ "path": "data/three.txt"
53
+ }
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "toolCalls": [
59
+ {
60
+ "name": "read_file",
61
+ "args": {
62
+ "path": "data/four.txt"
63
+ }
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "text": "El total es 110."
69
+ }
70
+ ],
71
+ "expect": {
72
+ "description": "Responde 110; de las seis llamadas solo la primera es fría y ninguna rompe la caché.",
73
+ "checks": [
74
+ {
75
+ "type": "output_contains",
76
+ "value": "110"
77
+ },
78
+ {
79
+ "type": "metric",
80
+ "metric": "coldCalls",
81
+ "mode": "mock",
82
+ "equals": 1
83
+ },
84
+ {
85
+ "type": "metric",
86
+ "metric": "warmCalls",
87
+ "mode": "mock",
88
+ "equals": 5
89
+ },
90
+ {
91
+ "type": "metric",
92
+ "metric": "cacheBreaks",
93
+ "mode": "mock",
94
+ "equals": 0
95
+ },
96
+ {
97
+ "type": "metric",
98
+ "metric": "prefixStability",
99
+ "mode": "mock",
100
+ "min": 0.98
101
+ },
102
+ {
103
+ "type": "metric",
104
+ "metric": "cacheHitRate",
105
+ "mode": "mock",
106
+ "min": 0.8
107
+ }
108
+ ]
109
+ }
110
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "id": "code-create-module",
3
+ "group": "code",
4
+ "title": "Crear un módulo que cumpla una comprobación dada",
5
+ "difficulty": "basic",
6
+ "setup": {
7
+ "files": {
8
+ "check.js": "const assert = require('assert');\nconst { slugify } = require('./slugify');\n\nassert.strictEqual(slugify('Hola Mundo'), 'hola-mundo');\nassert.strictEqual(slugify(' Árbol de Navidad! '), 'arbol-de-navidad');\nassert.strictEqual(slugify('a--b'), 'a-b');\nconsole.log('ok');\n"
9
+ }
10
+ },
11
+ "input": "Crea slugify.js (CommonJS) que exporte `slugify(texto)`: minúsculas, sin acentos, y cualquier secuencia de espacios o símbolos convertida en un único guion, sin guiones al principio ni al final. `node check.js` tiene que pasar.",
12
+ "script": [
13
+ {
14
+ "toolCalls": [
15
+ {
16
+ "name": "write_file",
17
+ "args": {
18
+ "path": "slugify.js",
19
+ "content": "function slugify(text) {\n return String(text)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nmodule.exports = { slugify };\n"
20
+ }
21
+ }
22
+ ]
23
+ },
24
+ {
25
+ "toolCalls": [
26
+ {
27
+ "name": "exec",
28
+ "args": {
29
+ "command": "node check.js"
30
+ }
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "text": "Creado slugify.js; `node check.js` pasa."
36
+ }
37
+ ],
38
+ "expect": {
39
+ "description": "Existe slugify.js y `node check.js` pasa sin tocar check.js.",
40
+ "checks": [
41
+ {
42
+ "type": "command",
43
+ "run": ["node", "check.js"]
44
+ },
45
+ {
46
+ "type": "file_contains",
47
+ "path": "check.js",
48
+ "value": "slugify('Hola Mundo')"
49
+ },
50
+ {
51
+ "type": "metric",
52
+ "metric": "toolCalls",
53
+ "max": 10
54
+ }
55
+ ]
56
+ }
57
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ "id": "code-fix-failing-test",
3
+ "group": "code",
4
+ "title": "Arreglar un bug a partir de un test que falla",
5
+ "description": "Un test rojo, un fallo de una línea. Mide el ciclo ejecutar → leer → editar → verificar.",
6
+ "difficulty": "intermediate",
7
+ "setup": {
8
+ "files": {
9
+ "sum.js": "function sum(a, b) {\n return a - b;\n}\n\nmodule.exports = { sum };\n",
10
+ "test.js": "const assert = require('assert');\nconst { sum } = require('./sum');\n\nassert.strictEqual(sum(2, 3), 5);\nassert.strictEqual(sum(-1, 1), 0);\nconsole.log('ok');\n"
11
+ }
12
+ },
13
+ "input": "`node test.js` falla. Encuentra el fallo y arréglalo en sum.js. No modifiques test.js. Al terminar, comprueba que `node test.js` pasa.",
14
+ "script": [
15
+ {
16
+ "toolCalls": [
17
+ {
18
+ "name": "exec",
19
+ "args": {
20
+ "command": "node test.js"
21
+ }
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "toolCalls": [
27
+ {
28
+ "name": "read_file",
29
+ "args": {
30
+ "path": "sum.js"
31
+ }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "toolCalls": [
37
+ {
38
+ "name": "edit_file",
39
+ "args": {
40
+ "path": "sum.js",
41
+ "old_string": "return a - b;",
42
+ "new_string": "return a + b;"
43
+ }
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "toolCalls": [
49
+ {
50
+ "name": "exec",
51
+ "args": {
52
+ "command": "node test.js"
53
+ }
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "text": "Arreglado: `sum` restaba en vez de sumar. `node test.js` pasa."
59
+ }
60
+ ],
61
+ "expect": {
62
+ "description": "sum.js corregido, test.js intacto y `node test.js` en verde.",
63
+ "checks": [
64
+ {
65
+ "type": "command",
66
+ "run": ["node", "test.js"]
67
+ },
68
+ {
69
+ "type": "file_contains",
70
+ "path": "test.js",
71
+ "value": "assert.strictEqual(sum(2, 3), 5);"
72
+ },
73
+ {
74
+ "type": "metric",
75
+ "metric": "toolCalls",
76
+ "max": 12
77
+ },
78
+ {
79
+ "type": "metric",
80
+ "metric": "toolErrors",
81
+ "equals": 1,
82
+ "mode": "mock"
83
+ }
84
+ ]
85
+ }
86
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "id": "code-json-config-edit",
3
+ "group": "code",
4
+ "title": "Editar un package.json sin romperlo",
5
+ "difficulty": "basic",
6
+ "setup": {
7
+ "files": {
8
+ "package.json": "{\n \"name\": \"demo\",\n \"version\": \"1.2.9\",\n \"scripts\": {\n \"test\": \"node test.js\"\n },\n \"dependencies\": {}\n}\n"
9
+ }
10
+ },
11
+ "input": "En package.json: sube la versión a 1.3.0 y añade un script `lint` con el comando `eslint .`. No cambies nada más.",
12
+ "script": [
13
+ {
14
+ "toolCalls": [
15
+ {
16
+ "name": "read_file",
17
+ "args": {
18
+ "path": "package.json"
19
+ }
20
+ }
21
+ ]
22
+ },
23
+ {
24
+ "toolCalls": [
25
+ {
26
+ "name": "edit_file",
27
+ "args": {
28
+ "path": "package.json",
29
+ "old_string": "\"version\": \"1.2.9\"",
30
+ "new_string": "\"version\": \"1.3.0\""
31
+ }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "toolCalls": [
37
+ {
38
+ "name": "edit_file",
39
+ "args": {
40
+ "path": "package.json",
41
+ "old_string": "\"test\": \"node test.js\"",
42
+ "new_string": "\"test\": \"node test.js\",\n \"lint\": \"eslint .\""
43
+ }
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "text": "Versión 1.3.0 y script `lint` añadido."
49
+ }
50
+ ],
51
+ "expect": {
52
+ "description": "package.json sigue siendo JSON válido, con la versión nueva, el script lint y el resto intacto.",
53
+ "checks": [
54
+ {
55
+ "type": "command",
56
+ "label": "package.json es válido y tiene los cambios pedidos",
57
+ "run": [
58
+ "node",
59
+ "-e",
60
+ "const p=require('./package.json');if(p.version!=='1.3.0'||p.scripts.lint!=='eslint .'||p.scripts.test!=='node test.js'||p.name!=='demo')process.exit(1)"
61
+ ]
62
+ },
63
+ {
64
+ "type": "metric",
65
+ "metric": "toolCalls",
66
+ "max": 8
67
+ }
68
+ ]
69
+ }
70
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "id": "code-read-and-answer",
3
+ "group": "code",
4
+ "title": "Responder una pregunta sobre el código sin modificarlo",
5
+ "difficulty": "basic",
6
+ "setup": {
7
+ "files": {
8
+ "README.md": "# demo-server\n\nServidor HTTP de ejemplo. Arranca con `node src/server.js`.\n",
9
+ "src/config.js": "const DEFAULT_PORT = 8472;\n\nmodule.exports = { port: Number(process.env.PORT) || DEFAULT_PORT };\n",
10
+ "src/server.js": "const http = require('http');\nconst { port } = require('./config');\n\nhttp.createServer((req, res) => res.end('ok')).listen(port);\n"
11
+ }
12
+ },
13
+ "input": "¿En qué puerto escucha el servidor si no se define PORT? Responde solo con el número. No modifiques nada.",
14
+ "script": [
15
+ {
16
+ "toolCalls": [
17
+ {
18
+ "name": "grep",
19
+ "args": {
20
+ "pattern": "PORT"
21
+ }
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "toolCalls": [
27
+ {
28
+ "name": "read_file",
29
+ "args": {
30
+ "path": "src/config.js"
31
+ }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "text": "8472"
37
+ }
38
+ ],
39
+ "expect": {
40
+ "description": "Responde 8472 sin escribir ni ejecutar nada.",
41
+ "checks": [
42
+ {
43
+ "type": "output_contains",
44
+ "value": "8472"
45
+ },
46
+ {
47
+ "type": "tool_called",
48
+ "tool": "write_file",
49
+ "min": 0,
50
+ "max": 0
51
+ },
52
+ {
53
+ "type": "tool_called",
54
+ "tool": "edit_file",
55
+ "min": 0,
56
+ "max": 0
57
+ },
58
+ {
59
+ "type": "metric",
60
+ "metric": "toolCalls",
61
+ "max": 8
62
+ }
63
+ ]
64
+ }
65
+ }