@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 +12 -12
- package/package.json +7 -7
- package/src/arena-toml.test.ts +19 -19
- package/src/cli.test.ts +118 -0
- package/src/cli.ts +121 -98
- package/src/comparative-judge.test.ts +31 -31
- package/src/player.test.ts +11 -11
- package/src/preflight.test.ts +56 -56
- package/src/runner.test.ts +269 -10
- package/src/runner.ts +88 -35
- package/src/stats.test.ts +10 -10
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,22 @@ import { parseArenaToml, buildExecutionPlan } from './arena-toml'
|
|
|
8
8
|
import { buildArchiveSidePlan, buildCopyPlan, buildPreparePlan, parseDeckSkills } from './preflight'
|
|
9
9
|
import { checkSkillExistence, formatSkillWarnings, resolveColdPoolDir } from './preflight'
|
|
10
10
|
|
|
11
|
+
// ─── ArenaCliIO interface (injected for testability) ───────────────────────
|
|
12
|
+
|
|
13
|
+
export interface ArenaCliIO {
|
|
14
|
+
log?: (msg: string) => void
|
|
15
|
+
error?: (msg: string) => void
|
|
16
|
+
warn?: (msg: string) => void
|
|
17
|
+
exit?: (code: number) => never
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const defaultArenaCliIO: Required<ArenaCliIO> = {
|
|
21
|
+
log: console.log,
|
|
22
|
+
error: console.error,
|
|
23
|
+
warn: console.warn,
|
|
24
|
+
exit: (code) => { process.exit(code) },
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
// ─── fetchWithProxy (infra dependency, no package boundary) ─────────────────
|
|
12
28
|
|
|
13
29
|
async function fetchWithProxy(url: string, init?: RequestInit): Promise<Response> {
|
|
@@ -82,9 +98,10 @@ function validateLinkResult(exitCode: number | null, stderr: string): { ok: bool
|
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
85
|
-
export async function main(args: string[] = process.argv.slice(2)) {
|
|
101
|
+
export async function main(args: string[] = process.argv.slice(2), io: ArenaCliIO = defaultArenaCliIO) {
|
|
102
|
+
const { log, exit } = { ...defaultArenaCliIO, ...io }
|
|
86
103
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
87
|
-
|
|
104
|
+
log(`lythoskill-arena — skill evaluation CLI
|
|
88
105
|
|
|
89
106
|
Usage:
|
|
90
107
|
lythoskill-arena single|vs|viz <options>
|
|
@@ -103,28 +120,30 @@ Examples:
|
|
|
103
120
|
lythoskill-arena prepare-workdir --deck ./decks/scout.toml --out /tmp/arena-20260517-side-a
|
|
104
121
|
lythoskill-arena archive --from /tmp/arena-20260517 --to playground/arena-20260517 --sides side-a,side-b
|
|
105
122
|
`)
|
|
106
|
-
|
|
123
|
+
exit(0)
|
|
107
124
|
}
|
|
108
|
-
return cli(args)
|
|
125
|
+
return cli(args, io)
|
|
109
126
|
}
|
|
110
127
|
|
|
111
|
-
function cli(args: string[]) {
|
|
128
|
+
function cli(args: string[], io: ArenaCliIO) {
|
|
112
129
|
const cmd = args[0]
|
|
113
130
|
const rest = args.slice(1)
|
|
114
131
|
|
|
115
|
-
if (cmd === 'vs' || cmd === 'compare') return vsRun(rest)
|
|
116
|
-
if (cmd === 'single' || cmd === 'run') return singleRun(rest)
|
|
117
|
-
if (cmd === 'viz') return vizRun(rest)
|
|
118
|
-
if (cmd === 'prepare-workdir') return prepareWorkdir(rest)
|
|
119
|
-
if (cmd === 'archive') return archiveRun(rest)
|
|
132
|
+
if (cmd === 'vs' || cmd === 'compare') return vsRun(rest, io)
|
|
133
|
+
if (cmd === 'single' || cmd === 'run') return singleRun(rest, io)
|
|
134
|
+
if (cmd === 'viz') return vizRun(rest, io)
|
|
135
|
+
if (cmd === 'prepare-workdir') return prepareWorkdir(rest, io)
|
|
136
|
+
if (cmd === 'archive') return archiveRun(rest, io)
|
|
120
137
|
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
const { error, exit } = { ...defaultArenaCliIO, ...io }
|
|
139
|
+
error(`Unknown command: ${cmd}`)
|
|
140
|
+
exit(1)
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
// ── single: single-player deck test (exec shortcut) ──────────────────────
|
|
126
144
|
|
|
127
|
-
async function singleRun(args: string[]) {
|
|
145
|
+
async function singleRun(args: string[], io: ArenaCliIO) {
|
|
146
|
+
const { log, error, warn, exit } = { ...defaultArenaCliIO, ...io }
|
|
128
147
|
const opts: Record<string, string | undefined> = {}
|
|
129
148
|
for (let i = 0; i < args.length; i++) {
|
|
130
149
|
if (args[i] === '--task' || args[i] === '-t') opts.task = args[++i]
|
|
@@ -136,7 +155,7 @@ async function singleRun(args: string[]) {
|
|
|
136
155
|
}
|
|
137
156
|
|
|
138
157
|
if (!opts.deck) {
|
|
139
|
-
|
|
158
|
+
error(`❌ --deck <path|url> is required.
|
|
140
159
|
--deck accepts local paths and http/https URLs (auto-fetched).
|
|
141
160
|
|
|
142
161
|
Example (no local file needed — URL is auto-fetched):
|
|
@@ -146,10 +165,10 @@ async function singleRun(args: string[]) {
|
|
|
146
165
|
|
|
147
166
|
Or with a local deck file you already have:
|
|
148
167
|
lythoskill-arena single --deck ./examples/decks/scout.toml --brief "your task"`)
|
|
149
|
-
|
|
168
|
+
exit(1)
|
|
150
169
|
}
|
|
151
170
|
if (!opts.task && (!opts.brief || !opts.brief.trim())) {
|
|
152
|
-
|
|
171
|
+
error(`❌ --task <path> or --brief "<text>" is required.
|
|
153
172
|
--task reads a .agent.md scenario file; --brief takes inline text.
|
|
154
173
|
|
|
155
174
|
Example (no local file needed — URL is auto-fetched):
|
|
@@ -159,24 +178,24 @@ async function singleRun(args: string[]) {
|
|
|
159
178
|
|
|
160
179
|
Or with a local deck file:
|
|
161
180
|
lythoskill-arena single --deck ./examples/decks/scout.toml --brief "your task"`)
|
|
162
|
-
|
|
181
|
+
exit(1)
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
let resolvedTaskPath: string | undefined
|
|
166
185
|
if (opts.task) {
|
|
167
186
|
resolvedTaskPath = resolve(opts.task)
|
|
168
187
|
if (!existsSync(resolvedTaskPath)) {
|
|
169
|
-
|
|
188
|
+
error(`❌ Task file not found: ${resolvedTaskPath}
|
|
170
189
|
Use --brief for inline tasks, or point --task to an existing .agent.md file.
|
|
171
190
|
Format: name + description + Given/When/Then sections.
|
|
172
191
|
|
|
173
192
|
Example (URL): lythoskill-arena single --brief "your task" --deck https://raw.githubusercontent.com/lythos-labs/lythoskill/main/examples/decks/scout.toml
|
|
174
193
|
Or (local): lythoskill-arena single --brief "your task" --deck ./examples/decks/scout.toml`)
|
|
175
|
-
|
|
194
|
+
exit(1)
|
|
176
195
|
}
|
|
177
196
|
const raw = readFileSync(resolvedTaskPath, 'utf-8')
|
|
178
197
|
if (!raw.startsWith('---')) {
|
|
179
|
-
|
|
198
|
+
error(`❌ Invalid .agent.md: missing frontmatter (must start with "---")
|
|
180
199
|
Correct format:
|
|
181
200
|
---
|
|
182
201
|
name: my-scenario
|
|
@@ -189,13 +208,13 @@ async function singleRun(args: string[]) {
|
|
|
189
208
|
...
|
|
190
209
|
## Then
|
|
191
210
|
...`)
|
|
192
|
-
|
|
211
|
+
exit(1)
|
|
193
212
|
}
|
|
194
213
|
if (!raw.includes('## When')) {
|
|
195
|
-
|
|
214
|
+
error(`❌ Invalid .agent.md: missing "## When" section.
|
|
196
215
|
The ## When section defines what the agent should do.
|
|
197
216
|
See template: playground/arena-one-shot/TASK-arena.agent.md`)
|
|
198
|
-
|
|
217
|
+
exit(1)
|
|
199
218
|
}
|
|
200
219
|
}
|
|
201
220
|
|
|
@@ -213,7 +232,7 @@ async function singleRun(args: string[]) {
|
|
|
213
232
|
}
|
|
214
233
|
const { mirrorUrls, isLikelyGitHubBlock } = await import('../../lythoskill-cold-pool/src/mirror.js')
|
|
215
234
|
const dest = resolve(process.cwd(), 'arena-deck.toml')
|
|
216
|
-
|
|
235
|
+
log(`📥 Fetching arena deck: ${url}`)
|
|
217
236
|
let res: Response | undefined
|
|
218
237
|
let allFailed = true
|
|
219
238
|
|
|
@@ -222,7 +241,7 @@ async function singleRun(args: string[]) {
|
|
|
222
241
|
if (!res?.ok) {
|
|
223
242
|
for (const mirrorUrl of mirrorUrls(url)) {
|
|
224
243
|
try {
|
|
225
|
-
|
|
244
|
+
log(` ↳ trying mirror: ${mirrorUrl}`)
|
|
226
245
|
const r = await fetchWithProxy(mirrorUrl, { signal: AbortSignal.timeout(30_000) })
|
|
227
246
|
if (r.ok) { res = r; allFailed = false; break }
|
|
228
247
|
} catch {}
|
|
@@ -231,21 +250,21 @@ async function singleRun(args: string[]) {
|
|
|
231
250
|
|
|
232
251
|
if (!res?.ok) {
|
|
233
252
|
const errorDetail = res ? `HTTP ${res.status}` : 'unreachable'
|
|
234
|
-
|
|
235
|
-
if (allFailed)
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
error(`❌ Cannot reach ${url} (${errorDetail})`)
|
|
254
|
+
if (allFailed) error(' Set LYTHOS_GH_MIRROR to use a custom mirror.')
|
|
255
|
+
error(' Or download manually and reference the local file.')
|
|
256
|
+
exit(1)
|
|
238
257
|
}
|
|
239
258
|
|
|
240
259
|
deckWrite(dest, await res.text())
|
|
241
|
-
|
|
260
|
+
log(` → saved to ${dest}`)
|
|
242
261
|
deckPath = dest
|
|
243
262
|
} else {
|
|
244
263
|
deckPath = resolve(opts.deck)
|
|
245
|
-
if (!deckExists(deckPath)) {
|
|
264
|
+
if (!deckExists(deckPath)) { error(`❌ Deck file not found: ${deckPath}
|
|
246
265
|
Make sure the path is correct, or use a URL:
|
|
247
266
|
--deck https://raw.githubusercontent.com/lythos-labs/lythoskill/main/examples/decks/scout.toml
|
|
248
|
-
(URLs are auto-fetched — no local file needed)`);
|
|
267
|
+
(URLs are auto-fetched — no local file needed)`); exit(1) }
|
|
249
268
|
}
|
|
250
269
|
|
|
251
270
|
const { useAgent } = await import('@lythos/test-utils/agents')
|
|
@@ -263,9 +282,9 @@ async function singleRun(args: string[]) {
|
|
|
263
282
|
// Markdown is for LLM agents; task text is read/stored as a raw string.
|
|
264
283
|
const taskText = resolvedTaskPath ? readFileSync(resolvedTaskPath, 'utf-8') : opts.brief!
|
|
265
284
|
|
|
266
|
-
|
|
267
|
-
if (opts.task)
|
|
268
|
-
else
|
|
285
|
+
log(`🤖 agent-run: ${player} × ${deckPath}`)
|
|
286
|
+
if (opts.task) log(`📋 task: ${resolve(opts.task!)}`)
|
|
287
|
+
else log(`📋 brief: ${opts.brief!.slice(0, 60)}...`)
|
|
269
288
|
|
|
270
289
|
// Setup workdir
|
|
271
290
|
const agentWorkdir = join(tmpdir(), `arena-single-${Date.now()}`)
|
|
@@ -298,11 +317,11 @@ async function singleRun(args: string[]) {
|
|
|
298
317
|
const linkStderr = await new Response(linkProc.stderr).text()
|
|
299
318
|
const linkResult = validateLinkResult(linkProc.exitCode, linkStderr)
|
|
300
319
|
if (!linkResult.ok) {
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
error(`❌ ${linkResult.error}`)
|
|
321
|
+
exit(1)
|
|
303
322
|
}
|
|
304
323
|
} else {
|
|
305
|
-
|
|
324
|
+
log('ℹ️ No skills declared in deck — skipping link')
|
|
306
325
|
}
|
|
307
326
|
|
|
308
327
|
const { existsSync: es } = await import('node:fs')
|
|
@@ -313,10 +332,10 @@ async function singleRun(args: string[]) {
|
|
|
313
332
|
const skills = parseDeckSkills(deckParsed)
|
|
314
333
|
const checks = checkSkillExistence(skills, coldPoolDir, es)
|
|
315
334
|
for (const warning of formatSkillWarnings(checks)) {
|
|
316
|
-
|
|
335
|
+
warn(`⚠️ ${warning}`)
|
|
317
336
|
}
|
|
318
337
|
} catch (e) {
|
|
319
|
-
|
|
338
|
+
warn('⚠️ Could not check skill existence:', e instanceof Error ? e.message : e)
|
|
320
339
|
}
|
|
321
340
|
|
|
322
341
|
// Template injection: brief is the {task} variable, template carries fixed contract
|
|
@@ -345,23 +364,24 @@ async function singleRun(args: string[]) {
|
|
|
345
364
|
const plan = buildCopyPlan(agentWorkdir, outDir, entries, skipSet)
|
|
346
365
|
for (const { src, dest, name } of plan) {
|
|
347
366
|
try { cpSync(src, dest, { recursive: true }) } catch (e) {
|
|
348
|
-
|
|
367
|
+
warn(`⚠️ Failed to copy agent output: ${name} — ${e instanceof Error ? e.message : e}`)
|
|
349
368
|
}
|
|
350
369
|
}
|
|
351
370
|
} catch (e) {
|
|
352
|
-
|
|
371
|
+
warn(`⚠️ Failed to copy agent output: ${e instanceof Error ? e.message : e}`)
|
|
353
372
|
}
|
|
354
373
|
}
|
|
355
374
|
|
|
356
375
|
// Summary (no judge — single mode is execution-only)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
376
|
+
log(`\n✅ Agent run complete → ${outDir}`)
|
|
377
|
+
log(` deck: ${deckPath}`)
|
|
378
|
+
log(` player: ${player}`)
|
|
360
379
|
}
|
|
361
380
|
|
|
362
381
|
// ── vs: arena.toml-driven comparison ──────────────────────────────────────
|
|
363
382
|
|
|
364
|
-
async function vsRun(args: string[]) {
|
|
383
|
+
async function vsRun(args: string[], io: ArenaCliIO) {
|
|
384
|
+
const { log, error, exit } = { ...defaultArenaCliIO, ...io }
|
|
365
385
|
// Native TOML parser is simpler than adding smol-toml dependency
|
|
366
386
|
const opts: Record<string, string | undefined> = {}
|
|
367
387
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -372,14 +392,14 @@ async function vsRun(args: string[]) {
|
|
|
372
392
|
}
|
|
373
393
|
|
|
374
394
|
if (!opts.config) {
|
|
375
|
-
|
|
376
|
-
|
|
395
|
+
error('❌ arena.toml path required: lythoskill-arena vs --config arena.toml')
|
|
396
|
+
exit(1)
|
|
377
397
|
}
|
|
378
398
|
|
|
379
399
|
const configPath = resolve(opts.config)
|
|
380
400
|
if (!existsSync(configPath)) {
|
|
381
|
-
|
|
382
|
-
|
|
401
|
+
error(`❌ Config file not found: ${configPath}`)
|
|
402
|
+
exit(1)
|
|
383
403
|
}
|
|
384
404
|
|
|
385
405
|
const toml = parseArenaToml(readFileSync(configPath, 'utf-8'))
|
|
@@ -395,11 +415,11 @@ async function vsRun(args: string[]) {
|
|
|
395
415
|
const isDryRun = opts.dryRun === 'true'
|
|
396
416
|
|
|
397
417
|
if (isDryRun) {
|
|
398
|
-
|
|
418
|
+
log(`🔍 Scanning arena.toml: ${configPath}`)
|
|
399
419
|
} else {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
420
|
+
log(`🏟 Arena VS: ${configPath}`)
|
|
421
|
+
log(` sides: ${toml.side.length}`)
|
|
422
|
+
log(` runs per side: ${toml.arena.runs_per_side}`)
|
|
403
423
|
}
|
|
404
424
|
|
|
405
425
|
const { runArenaFromToml } = await import('./runner')
|
|
@@ -408,38 +428,40 @@ async function vsRun(args: string[]) {
|
|
|
408
428
|
taskPath,
|
|
409
429
|
outDir: opts.out ? resolve(opts.out) : undefined,
|
|
410
430
|
dryRun: isDryRun,
|
|
411
|
-
log:
|
|
431
|
+
log: log,
|
|
412
432
|
configDir: resolve(configPath, '..'),
|
|
413
433
|
})
|
|
414
434
|
|
|
415
435
|
if ('plan' in result) {
|
|
416
|
-
if (!isDryRun)
|
|
417
|
-
for (const line of formatPlanOutput(result.plan))
|
|
436
|
+
if (!isDryRun) log('📋 Execution plan (dry-run):')
|
|
437
|
+
for (const line of formatPlanOutput(result.plan)) log(line)
|
|
418
438
|
} else if ('manifest' in result) {
|
|
419
439
|
const r = result
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
440
|
+
log(`\n📊 Arena complete: ${r.manifest.id}`)
|
|
441
|
+
log(` report: ${r.artifactsDir}/report.md`)
|
|
442
|
+
log(` participants: ${r.manifest.participants.map(p => p.name).join(', ')}`)
|
|
423
443
|
}
|
|
424
444
|
}
|
|
425
445
|
|
|
426
446
|
// ── viz: generate HTML report from arena.json ─────────────────────────────
|
|
427
447
|
|
|
428
|
-
async function vizRun(args: string[]) {
|
|
448
|
+
async function vizRun(args: string[], io: ArenaCliIO) {
|
|
449
|
+
const { error, exit, log } = { ...defaultArenaCliIO, ...io }
|
|
429
450
|
const runsDir = args.find(a => !a.startsWith('-'))
|
|
430
|
-
if (!runsDir) {
|
|
451
|
+
if (!runsDir) { error('❌ runs/<arena-id> path required: lythoskill-arena viz runs/arena-20260504'); exit(1) }
|
|
431
452
|
|
|
432
453
|
const arenaJsonPath = resolve(runsDir, 'arena.json')
|
|
433
|
-
if (!existsSync(arenaJsonPath)) {
|
|
454
|
+
if (!existsSync(arenaJsonPath)) { error(`❌ arena.json not found in: ${runsDir}`); exit(1) }
|
|
434
455
|
|
|
435
|
-
|
|
456
|
+
log(`📈 Arena HTML report not yet implemented. See report.md in ${runsDir}/`)
|
|
436
457
|
}
|
|
437
458
|
|
|
438
459
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
439
460
|
// ── prepare-workdir: reusable workdir setup (used by both CLI and agent) ──
|
|
440
461
|
// Intent: create an isolated arena workdir with deck linked and ready to run
|
|
441
462
|
|
|
442
|
-
async function prepareWorkdir(args: string[]) {
|
|
463
|
+
async function prepareWorkdir(args: string[], io: ArenaCliIO) {
|
|
464
|
+
const { log, error, warn, exit } = { ...defaultArenaCliIO, ...io }
|
|
443
465
|
const opts: Record<string, string | undefined> = {}
|
|
444
466
|
let dryRun = false
|
|
445
467
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -450,15 +472,15 @@ async function prepareWorkdir(args: string[]) {
|
|
|
450
472
|
}
|
|
451
473
|
|
|
452
474
|
if (!opts.deck) {
|
|
453
|
-
|
|
475
|
+
error(`❌ --deck <path> is required.
|
|
454
476
|
lythoskill-arena prepare-workdir --deck ./skill-deck.toml --out /tmp/arena-side-a`)
|
|
455
|
-
|
|
477
|
+
exit(1)
|
|
456
478
|
}
|
|
457
479
|
|
|
458
480
|
const deckPath = resolve(opts.deck)
|
|
459
481
|
if (!existsSync(deckPath)) {
|
|
460
|
-
|
|
461
|
-
|
|
482
|
+
error(`❌ Deck file not found: ${deckPath}`)
|
|
483
|
+
exit(1)
|
|
462
484
|
}
|
|
463
485
|
|
|
464
486
|
const workDir = opts.out
|
|
@@ -475,16 +497,16 @@ async function prepareWorkdir(args: string[]) {
|
|
|
475
497
|
brief: opts.brief,
|
|
476
498
|
})
|
|
477
499
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (opts.brief)
|
|
500
|
+
log('📋 Prepare plan:')
|
|
501
|
+
log(` deck: ${plan.deckPath}`)
|
|
502
|
+
log(` workdir: ${plan.workDir}`)
|
|
503
|
+
log(` skills: ${plan.skills.length} declared (${plan.skills.map(s => s.name).join(', ') || 'none'})`)
|
|
504
|
+
log(` link: ${plan.hasSkills ? 'Bun.spawn deck link' : 'skip (no skills)'}`)
|
|
505
|
+
log(` AGENTS.md: write (${plan.agentsMd.split('\n').length} lines)`)
|
|
506
|
+
if (opts.brief) log(` brief: ${opts.brief!.slice(0, 60)}...`)
|
|
485
507
|
|
|
486
508
|
if (dryRun) {
|
|
487
|
-
|
|
509
|
+
log(`\n🏁 Dry-run complete (no files created). Remove --dry-run to execute.`)
|
|
488
510
|
return
|
|
489
511
|
}
|
|
490
512
|
|
|
@@ -506,11 +528,11 @@ async function prepareWorkdir(args: string[]) {
|
|
|
506
528
|
const linkStderr = await new Response(linkProc.stderr).text()
|
|
507
529
|
const linkResult = validateLinkResult(linkProc.exitCode, linkStderr)
|
|
508
530
|
if (!linkResult.ok) {
|
|
509
|
-
|
|
510
|
-
|
|
531
|
+
error(`❌ ${linkResult.error}`)
|
|
532
|
+
exit(1)
|
|
511
533
|
}
|
|
512
534
|
} else {
|
|
513
|
-
|
|
535
|
+
log('ℹ️ No skills declared in deck — skipping link')
|
|
514
536
|
}
|
|
515
537
|
|
|
516
538
|
// Skill existence check
|
|
@@ -519,22 +541,23 @@ async function prepareWorkdir(args: string[]) {
|
|
|
519
541
|
const coldPoolDir = resolveColdPoolDir(Bun.TOML.parse(deckContent)?.deck?.cold_pool, homedir(), coldPoolDefault)
|
|
520
542
|
const checks = checkSkillExistence(plan.skills, coldPoolDir, existsSync)
|
|
521
543
|
for (const warning of formatSkillWarnings(checks)) {
|
|
522
|
-
|
|
544
|
+
warn(`⚠️ ${warning}`)
|
|
523
545
|
}
|
|
524
546
|
} catch (e) {
|
|
525
|
-
|
|
547
|
+
warn('⚠️ Could not check skill existence:', e instanceof Error ? e.message : e)
|
|
526
548
|
}
|
|
527
549
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (opts.brief)
|
|
550
|
+
log(`✅ Workdir ready → ${workDir}`)
|
|
551
|
+
log(` deck: ${deckPath}`)
|
|
552
|
+
if (opts.brief) log(` brief: ${opts.brief!.slice(0, 60)}...`)
|
|
531
553
|
}
|
|
532
554
|
|
|
533
555
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
534
556
|
// ── archive: copy agent outputs from workdir(s) to outDir ─────────────────
|
|
535
557
|
// Intent: same copy behavior as CLI singleRun, reusable for agent-orchestrated
|
|
536
558
|
|
|
537
|
-
async function archiveRun(args: string[]) {
|
|
559
|
+
async function archiveRun(args: string[], io: ArenaCliIO) {
|
|
560
|
+
const { log, error, warn, exit } = { ...defaultArenaCliIO, ...io }
|
|
538
561
|
const opts: Record<string, string | undefined> = {}
|
|
539
562
|
let dryRun = false
|
|
540
563
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -546,9 +569,9 @@ async function archiveRun(args: string[]) {
|
|
|
546
569
|
}
|
|
547
570
|
|
|
548
571
|
if (!opts.from || !opts.to) {
|
|
549
|
-
|
|
572
|
+
error(`❌ --from <workdir> and --to <outdir> are required.
|
|
550
573
|
lythoskill-arena archive --from /tmp/arena-20260517 --to playground/arena-20260517 --sides side-a,side-b --report ./report.md`)
|
|
551
|
-
|
|
574
|
+
exit(1)
|
|
552
575
|
}
|
|
553
576
|
|
|
554
577
|
const fromDir = resolve(opts.from)
|
|
@@ -558,18 +581,18 @@ async function archiveRun(args: string[]) {
|
|
|
558
581
|
const plan = buildArchiveSidePlan(fromDir, sides, existsSync)
|
|
559
582
|
|
|
560
583
|
// ── Plan output (always shown, also serves as dry-run) ──────────────────
|
|
561
|
-
|
|
584
|
+
log('📋 Archive plan:')
|
|
562
585
|
for (const pe of plan) {
|
|
563
586
|
if (!pe.found) {
|
|
564
|
-
|
|
587
|
+
log(` ⚠️ ${pe.side}: not found (${pe.sourceDir}) — will skip`)
|
|
565
588
|
} else if (pe.sourceDir === fromDir && pe.side !== '.') {
|
|
566
|
-
|
|
589
|
+
log(` ${pe.side}: ${pe.sourceDir} (fallback → root) → ${join(outDir, pe.side)}`)
|
|
567
590
|
} else {
|
|
568
|
-
|
|
591
|
+
log(` ${pe.side}: ${pe.sourceDir} → ${join(outDir, pe.side)}`)
|
|
569
592
|
}
|
|
570
593
|
}
|
|
571
594
|
if (dryRun) {
|
|
572
|
-
|
|
595
|
+
log(`\n🏁 Dry-run complete (no files copied). Remove --dry-run to execute.`)
|
|
573
596
|
return
|
|
574
597
|
}
|
|
575
598
|
|
|
@@ -579,7 +602,7 @@ async function archiveRun(args: string[]) {
|
|
|
579
602
|
if (opts.report && existsSync(resolve(opts.report))) {
|
|
580
603
|
const { cpSync: cpR } = await import('node:fs')
|
|
581
604
|
cpR(resolve(opts.report), join(outDir, 'report.md'))
|
|
582
|
-
|
|
605
|
+
log(`📄 report.md → ${outDir}/report.md`)
|
|
583
606
|
}
|
|
584
607
|
|
|
585
608
|
const { cpSync, readdirSync } = await import('node:fs')
|
|
@@ -587,7 +610,7 @@ async function archiveRun(args: string[]) {
|
|
|
587
610
|
|
|
588
611
|
for (const planEntry of plan) {
|
|
589
612
|
if (!planEntry.found) {
|
|
590
|
-
|
|
613
|
+
warn(`⚠️ Side workdir not found: ${planEntry.sourceDir}`)
|
|
591
614
|
continue
|
|
592
615
|
}
|
|
593
616
|
|
|
@@ -601,14 +624,14 @@ async function archiveRun(args: string[]) {
|
|
|
601
624
|
const dest = join(sideOutDir, entry.name)
|
|
602
625
|
try {
|
|
603
626
|
cpSync(src, dest, { recursive: entry.isDirectory() })
|
|
604
|
-
|
|
627
|
+
log(` ${planEntry.side}/${entry.name} → ${dest}`)
|
|
605
628
|
} catch (e) {
|
|
606
|
-
|
|
629
|
+
warn(`⚠️ Failed to copy ${planEntry.side}/${entry.name}: ${e instanceof Error ? e.message : e}`)
|
|
607
630
|
}
|
|
608
631
|
}
|
|
609
632
|
}
|
|
610
633
|
|
|
611
|
-
|
|
634
|
+
log(`✅ Archive complete → ${outDir}`)
|
|
612
635
|
}
|
|
613
636
|
|
|
614
637
|
// ── Entry point ────────────────────────────────────────────────────────────
|