@lythos/skill-arena 0.15.7 → 0.16.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.
package/README.md CHANGED
@@ -16,20 +16,20 @@
16
16
  ```bash
17
17
  bun add -d @lythos/skill-arena
18
18
  # or use directly
19
- bunx @lythos/skill-arena@0.15.7 <command>
19
+ bunx @lythos/skill-arena@0.16.1 <command>
20
20
  ```
21
21
 
22
22
  ## Quick Start
23
23
 
24
24
  ```bash
25
25
  # single — test one deck (most common)
26
- bunx @lythos/skill-arena@0.15.7 single \
26
+ bunx @lythos/skill-arena@0.16.1 single \
27
27
  --deck ./examples/decks/scout.toml \
28
28
  --brief "Generate auth flow diagram" \
29
29
  --out ./output
30
30
 
31
31
  # single with explicit player
32
- bunx @lythos/skill-arena@0.15.7 single \
32
+ bunx @lythos/skill-arena@0.16.1 single \
33
33
  --deck ./examples/decks/scout.toml \
34
34
  --brief "Generate auth flow diagram" \
35
35
  --player kimi \
@@ -37,10 +37,10 @@ bunx @lythos/skill-arena@0.15.7 single \
37
37
 
38
38
  # cross-deck vs — compare two decks (agent-orchestrated)
39
39
  # Create arena.toml declaring sides with different decks, then:
40
- bunx @lythos/skill-arena@0.15.7 vs --config ./arena.toml
40
+ bunx @lythos/skill-arena@0.16.1 vs --config ./arena.toml
41
41
 
42
42
  # cross-player vs — compare kimi vs codex (CLI only)
43
- bunx @lythos/skill-arena@0.15.7 vs --config ./arena.toml --player kimi
43
+ bunx @lythos/skill-arena@0.16.1 vs --config ./arena.toml --player kimi
44
44
  ```
45
45
 
46
46
  **What happens**: Agent creates isolated `/tmp` workdir per side, `deck link` skills, spawns parallel subagents, collects artifacts, judge scores outputs. Parent deck restored after.
@@ -50,14 +50,14 @@ bunx @lythos/skill-arena@0.15.7 vs --config ./arena.toml --player kimi
50
50
  ### `single` — one deck, one task
51
51
 
52
52
  ```bash
53
- bunx @lythos/skill-arena@0.15.7 single \
53
+ bunx @lythos/skill-arena@0.16.1 single \
54
54
  --deck ./deck.toml \
55
55
  --brief "Produce a .docx report with radar chart" \
56
56
  --timeout 600000 \
57
57
  --out ./output
58
58
 
59
59
  # with explicit player
60
- bunx @lythos/skill-arena@0.15.7 single \
60
+ bunx @lythos/skill-arena@0.16.1 single \
61
61
  --deck ./deck.toml \
62
62
  --brief "Produce a .docx report with radar chart" \
63
63
  --player kimi \
@@ -67,14 +67,14 @@ bunx @lythos/skill-arena@0.15.7 single \
67
67
  ### `vs` — multi-deck comparison
68
68
 
69
69
  ```bash
70
- bunx @lythos/skill-arena@0.15.7 vs --config ./arena.toml
71
- bunx @lythos/skill-arena@0.15.7 vs --config ./arena.toml --dry-run
70
+ bunx @lythos/skill-arena@0.16.1 vs --config ./arena.toml
71
+ bunx @lythos/skill-arena@0.16.1 vs --config ./arena.toml --dry-run
72
72
  ```
73
73
 
74
74
  ### `prepare-workdir` — isolate + link skills (agent-orchestrated)
75
75
 
76
76
  ```bash
77
- bunx @lythos/skill-arena@0.15.7 prepare-workdir \
77
+ bunx @lythos/skill-arena@0.16.1 prepare-workdir \
78
78
  --deck ./skill-deck.toml \
79
79
  --out /tmp/arena-side-a \
80
80
  --brief "task description"
@@ -85,7 +85,7 @@ Creates `/tmp`-isolated workdir with deck copied, AGENTS.md written, and `deck l
85
85
  ### `archive` — collect agent outputs (agent-orchestrated)
86
86
 
87
87
  ```bash
88
- bunx @lythos/skill-arena@0.15.7 archive \
88
+ bunx @lythos/skill-arena@0.16.1 archive \
89
89
  --from /tmp/arena-side-a \
90
90
  --to ./playground/output \
91
91
  --sides side-a
@@ -96,7 +96,7 @@ Copies agent artifacts from workdir(s) to output, skipping internal files (`.cla
96
96
  ### `viz` — render results (WIP — HTML report generation pending)
97
97
 
98
98
  ```bash
99
- bunx @lythos/skill-arena@0.15.7 viz runs/arena-<id>/
99
+ bunx @lythos/skill-arena@0.16.1 viz runs/arena-<id>/
100
100
  ```
101
101
 
102
102
  ## Parameters
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lythos/skill-arena",
3
- "version": "0.15.7",
3
+ "version": "0.16.1",
4
4
  "description": "Skill Arena — benchmark skill effectiveness with controlled-variable comparison",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -42,15 +42,15 @@
42
42
  "bun": ">=1.0.0"
43
43
  },
44
44
  "dependencies": {
45
- "@lythos/cold-pool": "^0.15.7",
46
- "@lythos/infra": "^0.15.7",
47
- "@lythos/test-utils": "^0.15.7",
45
+ "@lythos/cold-pool": "^0.16.1",
46
+ "@lythos/infra": "^0.16.1",
47
+ "@lythos/test-utils": "^0.16.1",
48
48
  "zod": "^3.24.0",
49
49
  "zod-to-json-schema": "^3.25.2"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@lythos/agent-adapter-claude-sdk": "^0.15.7",
53
- "@lythos/agent-adapter-deepseek-serve": "^0.15.7",
54
- "@lythos/agent-adapter-codex": "^0.15.7"
52
+ "@lythos/agent-adapter-claude-sdk": "^0.16.1",
53
+ "@lythos/agent-adapter-deepseek-serve": "^0.16.1",
54
+ "@lythos/agent-adapter-codex": "^0.16.1"
55
55
  }
56
56
  }
@@ -1,4 +1,4 @@
1
- import { describe, test, expect } from 'bun:test'
1
+ import { describe, it, expect } from 'bun:test'
2
2
  import { parseArenaToml, buildExecutionPlan, ArenaToml } from './arena-toml'
3
3
  import { formatPlanOutput } from './runner'
4
4
 
@@ -63,7 +63,7 @@ working_dir = "/workspace"
63
63
  `
64
64
 
65
65
  describe('parseArenaToml', () => {
66
- test('parses minimal two-side arena with criteria', () => {
66
+ it('parses minimal two-side arena with criteria', () => {
67
67
  const result = parseArenaToml(minimalToml)
68
68
  expect(result.arena.task).toBe('Test task')
69
69
  expect(result.arena.criteria).toEqual(['a', 'b'])
@@ -75,14 +75,14 @@ describe('parseArenaToml', () => {
75
75
  expect(result.side[0].control).toBe(false)
76
76
  })
77
77
 
78
- test('parses arena with judge field (preferred over criteria)', () => {
78
+ it('parses arena with judge field (preferred over criteria)', () => {
79
79
  const result = parseArenaToml(judgeToml)
80
80
  expect(result.arena.judge).toContain('Evaluate completeness')
81
81
  expect(result.arena.criteria).toBeUndefined()
82
82
  expect(result.side).toHaveLength(2)
83
83
  })
84
84
 
85
- test('parses full arena with runs_per_side and control', () => {
85
+ it('parses full arena with runs_per_side and control', () => {
86
86
  const result = parseArenaToml(fullToml)
87
87
  expect(result.arena.runs_per_side).toBe(3)
88
88
  expect(result.side).toHaveLength(3)
@@ -90,7 +90,7 @@ describe('parseArenaToml', () => {
90
90
  expect(result.side[2].control).toBe(true)
91
91
  })
92
92
 
93
- test('parses side env block', () => {
93
+ it('parses side env block', () => {
94
94
  const result = parseArenaToml(fullToml)
95
95
  const env = result.side[2].env
96
96
  expect(env.container).toBe('node:20-alpine')
@@ -99,47 +99,47 @@ describe('parseArenaToml', () => {
99
99
  expect(env.env_vars).toEqual({})
100
100
  })
101
101
 
102
- test('rejects fewer than 2 sides', () => {
102
+ it('rejects fewer than 2 sides', () => {
103
103
  const bad = `[arena]\ntask = "x"\ncriteria = ["a"]\n\n[[side]]\nname = "only"\nplayer = "c"\ndeck = "x.toml"`
104
104
  expect(() => parseArenaToml(bad)).toThrow()
105
105
  })
106
106
 
107
- test('rejects neither judge nor criteria provided', () => {
107
+ it('rejects neither judge nor criteria provided', () => {
108
108
  const bad = `[arena]\ntask = "x"\n\n[[side]]\nname = "a"\nplayer = "c"\ndeck = "a.toml"\n\n[[side]]\nname = "b"\nplayer = "c"\ndeck = "b.toml"`
109
109
  expect(() => parseArenaToml(bad)).toThrow()
110
110
  })
111
111
 
112
- test('accepts judge without criteria (either is sufficient)', () => {
112
+ it('accepts judge without criteria (either is sufficient)', () => {
113
113
  const toml = `[arena]\ntask = "x"\njudge = "Evaluate this."\n\n[[side]]\nname = "a"\nplayer = "c"\ndeck = "a.toml"\n\n[[side]]\nname = "b"\nplayer = "c"\ndeck = "b.toml"`
114
114
  expect(() => parseArenaToml(toml)).not.toThrow()
115
115
  })
116
116
 
117
- test('rejects empty criteria and no judge', () => {
117
+ it('rejects empty criteria and no judge', () => {
118
118
  const bad = `[arena]\ntask = "x"\ncriteria = []\n\n[[side]]\nname = "a"\nplayer = "c"\ndeck = "a.toml"\n\n[[side]]\nname = "b"\nplayer = "c"\ndeck = "b.toml"`
119
119
  expect(() => parseArenaToml(bad)).toThrow()
120
120
  })
121
121
 
122
- test('rejects non-object input', () => {
122
+ it('rejects non-object input', () => {
123
123
  expect(() => ArenaToml.parse('not valid')).toThrow()
124
124
  })
125
125
 
126
- test('rejects missing arena section', () => {
126
+ it('rejects missing arena section', () => {
127
127
  expect(() => parseArenaToml('[[side]]\nname = "a"')).toThrow()
128
128
  })
129
129
 
130
- test('rejects runs_per_side = 0', () => {
130
+ it('rejects runs_per_side = 0', () => {
131
131
  const bad = `[arena]\ntask = "x"\ncriteria = ["a"]\nruns_per_side = 0\n\n[[side]]\nname = "a"\nplayer = "c"\ndeck = "a.toml"\n\n[[side]]\nname = "b"\nplayer = "c"\ndeck = "b.toml"`
132
132
  expect(() => parseArenaToml(bad)).toThrow()
133
133
  })
134
134
 
135
- test('parses integer and boolean values correctly', () => {
135
+ it('parses integer and boolean values correctly', () => {
136
136
  const toml = `[arena]\ntask = "x"\ncriteria = ["a"]\nruns_per_side = 2\nmax_participants = 5\n\n[[side]]\nname = "a"\nplayer = "c"\ndeck = "a.toml"\n\n[[side]]\nname = "b"\nplayer = "c"\ndeck = "b.toml"`
137
137
  const result = parseArenaToml(toml)
138
138
  expect(result.arena.runs_per_side).toBe(2)
139
139
  expect(result.arena.max_participants).toBe(5)
140
140
  })
141
141
 
142
- test('comments are stripped', () => {
142
+ it('comments are stripped', () => {
143
143
  const toml = `[arena]\n# this is a comment\ntask = "x"\ncriteria = ["a"]\n\n[[side]]\nname = "a"\nplayer = "c"\ndeck = "a.toml"\n\n[[side]]\nname = "b"\nplayer = "c"\ndeck = "b.toml"`
144
144
  const result = parseArenaToml(toml)
145
145
  expect(result.arena.task).toBe('x')
@@ -147,7 +147,7 @@ describe('parseArenaToml', () => {
147
147
  })
148
148
 
149
149
  describe('buildExecutionPlan', () => {
150
- test('generates plan: 2 sides × 1 run = 2 cells', () => {
150
+ it('generates plan: 2 sides × 1 run = 2 cells', () => {
151
151
  const toml = parseArenaToml(minimalToml)
152
152
  const plan = buildExecutionPlan(toml)
153
153
  expect(plan.task).toBe('Test task')
@@ -156,20 +156,20 @@ describe('buildExecutionPlan', () => {
156
156
  expect(plan.total_runs).toBe(2)
157
157
  })
158
158
 
159
- test('generates plan with judge field populated', () => {
159
+ it('generates plan with judge field populated', () => {
160
160
  const toml = parseArenaToml(judgeToml)
161
161
  const plan = buildExecutionPlan(toml)
162
162
  expect(plan.judge).toContain('Evaluate completeness')
163
163
  })
164
164
 
165
- test('generates plan: 3 sides × 3 runs = 9 cells', () => {
165
+ it('generates plan: 3 sides × 3 runs = 9 cells', () => {
166
166
  const toml = parseArenaToml(fullToml)
167
167
  const plan = buildExecutionPlan(toml)
168
168
  expect(plan.cells).toHaveLength(9)
169
169
  expect(plan.total_runs).toBe(9)
170
170
  })
171
171
 
172
- test('control flag preserved in plan cells', () => {
172
+ it('control flag preserved in plan cells', () => {
173
173
  const toml = parseArenaToml(fullToml)
174
174
  const plan = buildExecutionPlan(toml)
175
175
  const baselineCells = plan.cells.filter(c => c.side === 'baseline')
@@ -177,7 +177,7 @@ describe('buildExecutionPlan', () => {
177
177
  expect(baselineCells.every(c => c.control)).toBe(true)
178
178
  })
179
179
 
180
- test('dry-run: plan is pure data, no side effects', () => {
180
+ it('dry-run: plan is pure data, no side effects', () => {
181
181
  const toml = parseArenaToml(fullToml)
182
182
  const plan = buildExecutionPlan(toml)
183
183
  expect(plan.total_runs).toBeGreaterThan(0)
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { main, defaultArenaCliIO, type ArenaCliIO } from './cli'
3
+
4
+ // ─── Helpers ───────────────────────────────────────────────────────────────
5
+
6
+ async function catchExitAsync(fn: () => Promise<void>): Promise<number | undefined> {
7
+ let exitCode: number | undefined
8
+ try { await fn() } catch (e: any) {
9
+ if (!String(e).includes('EXIT:')) throw e
10
+ const m = String(e).match(/EXIT:(\d+)/)
11
+ if (m) exitCode = parseInt(m[1], 10)
12
+ }
13
+ return exitCode
14
+ }
15
+
16
+ function mockIO(): { io: Required<ArenaCliIO>; logs: string[]; errors: string[]; warns: string[] } {
17
+ const logs: string[] = []
18
+ const errors: string[] = []
19
+ const warns: string[] = []
20
+ const io: Required<ArenaCliIO> = {
21
+ log: (msg: string) => logs.push(msg),
22
+ error: (msg: string) => errors.push(msg),
23
+ warn: (msg: string) => warns.push(msg),
24
+ exit: (code: number) => { throw new Error(`EXIT:${code}`) },
25
+ }
26
+ return { io, logs, errors, warns }
27
+ }
28
+
29
+ // ─── Tests ─────────────────────────────────────────────────────────────────
30
+
31
+ describe('main', () => {
32
+ it('shows help on --help', async () => {
33
+ const { io, logs, errors } = mockIO()
34
+ const exitCode = await catchExitAsync(async () => {
35
+ await main(['--help'], io)
36
+ })
37
+ expect(exitCode).toBe(0)
38
+ expect(logs.length).toBeGreaterThan(0)
39
+ expect(logs[0]).toContain('lythoskill-arena')
40
+ expect(errors.length).toBe(0)
41
+ })
42
+
43
+ it('shows help on -h', async () => {
44
+ const { io, logs, errors } = mockIO()
45
+ const exitCode = await catchExitAsync(async () => {
46
+ await main(['-h'], io)
47
+ })
48
+ expect(exitCode).toBe(0)
49
+ expect(logs.length).toBeGreaterThan(0)
50
+ expect(logs[0]).toContain('lythoskill-arena')
51
+ expect(errors.length).toBe(0)
52
+ })
53
+
54
+ it('shows help on no args', async () => {
55
+ const { io, logs, errors } = mockIO()
56
+ const exitCode = await catchExitAsync(async () => {
57
+ await main([], io)
58
+ })
59
+ expect(exitCode).toBe(0)
60
+ expect(logs.length).toBeGreaterThan(0)
61
+ expect(errors.length).toBe(0)
62
+ })
63
+
64
+ it('errors on unknown command', async () => {
65
+ const { io, logs, errors } = mockIO()
66
+ const exitCode = await catchExitAsync(async () => {
67
+ await main(['bogus'], io)
68
+ })
69
+ expect(exitCode).toBe(1)
70
+ expect(errors.length).toBeGreaterThan(0)
71
+ expect(errors[0]).toContain('Unknown command')
72
+ expect(errors[0]).toContain('bogus')
73
+ })
74
+ })
75
+
76
+ describe('singleRun', () => {
77
+ it('errors when --deck is missing', async () => {
78
+ const { io, logs, errors } = mockIO()
79
+ const exitCode = await catchExitAsync(async () => {
80
+ await main(['single', '--brief', 'test task'], io)
81
+ })
82
+ expect(exitCode).toBe(1)
83
+ expect(errors.length).toBeGreaterThan(0)
84
+ expect(errors[0]).toContain('--deck')
85
+ expect(errors[0]).toContain('required')
86
+ })
87
+
88
+ it('errors when both --task and --brief are missing', async () => {
89
+ const { io, logs, errors } = mockIO()
90
+ const exitCode = await catchExitAsync(async () => {
91
+ await main(['single', '--deck', './some-deck.toml'], io)
92
+ })
93
+ expect(exitCode).toBe(1)
94
+ expect(errors.length).toBeGreaterThan(0)
95
+ expect(errors[0]).toContain('--task')
96
+ expect(errors[0]).toContain('--brief')
97
+ })
98
+
99
+ it('errors when --brief is empty string', async () => {
100
+ const { io, logs, errors } = mockIO()
101
+ const exitCode = await catchExitAsync(async () => {
102
+ await main(['single', '--deck', './some-deck.toml', '--brief', ''], io)
103
+ })
104
+ expect(exitCode).toBe(1)
105
+ expect(errors.length).toBeGreaterThan(0)
106
+ expect(errors[0]).toContain('--task')
107
+ expect(errors[0]).toContain('--brief')
108
+ })
109
+ })
110
+
111
+ describe('defaultArenaCliIO', () => {
112
+ it('has all required functions', () => {
113
+ expect(typeof defaultArenaCliIO.log).toBe('function')
114
+ expect(typeof defaultArenaCliIO.error).toBe('function')
115
+ expect(typeof defaultArenaCliIO.warn).toBe('function')
116
+ expect(typeof defaultArenaCliIO.exit).toBe('function')
117
+ })
118
+ })