kodelyth-ecc 2.5.4 → 2.6.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,65 @@
2
2
 
3
3
  All notable changes to Kodelyth ECC are documented here.
4
4
 
5
+ ## v2.6.0 — GOD mode + EVIL mode v2 (Arena phases 0-2) (August 2026)
6
+
7
+ The foundation of the **adversarial arena**: two opposed crews that will eventually fight each other until the attacker gives up. Phases 0-2 of 6 — the contract, the adversary, and the builder. The arena loop itself lands in a later release, only once it is verified end-to-end.
8
+
9
+ ### Added — Phase 0: the contract (`scripts/arena/`)
10
+
11
+ - **`contract.js`** — the shared schema GOD and EVIL exchange. `Finding` (severity × confidence × exploitability → risk), `Artifact`, `RoundVerdict`, stable FNV-1a fingerprints so the same issue dedupes across rounds even when wording drifts, and `hasConverged()`.
12
+ - **Refuted findings carry zero effective risk** — verification can genuinely zero out a false positive.
13
+ - Certainty outweighs raw severity: a *confirmed/high/trivial* outranks a *speculative/critical/theoretical*, so noise sinks.
14
+ - **`state.js`** — resumable run state in `~/.kodelythecc/arena/`. Rounds are appended, so a crash or budget abort never loses rounds already paid for.
15
+ - **Hard stops, not advisory:** max rounds, token budget, wall-clock. `canAffordRound()` refuses *before* spending.
16
+
17
+ ### Added — Phase 1: EVIL mode v2 (`scripts/arena/evil.js`, `/evil-mode`)
18
+
19
+ The 8 adversarial agents already hunted well; what they lacked was judgment.
20
+
21
+ - **Scoring** — every finding gets a risk score instead of landing in a flat pile.
22
+ - **Adversarial verification** — each top finding is challenged by a fresh agent told to **REFUTE it**, defaulting to refuted when uncertain. False positives die before they can waste GOD mode's next round.
23
+ - **Loop-until-dry** — later rounds tell each agent what is already known and to hunt what the last pass *missed*.
24
+ - `selectForVerification()` is budget-bounded (default 12) and skips settled or near-zero-risk findings.
25
+ - `/devil-mode` still works — `/evil-mode` is the upgraded entry point.
26
+
27
+ ### Added — Phase 2: GOD mode (`scripts/arena/god.js`, `/god-mode`)
28
+
29
+ Not "fire nine agents at once" — that is `/project-launch`. A six-stage pipeline with three things the parallel commands lack:
30
+
31
+ | Stage | Agents |
32
+ |---|---|
33
+ | **Recall** | — (memory lookup; never invents a memory) |
34
+ | **Design** ∥ | `architect` + `code-architect` |
35
+ | **Build** → | `pair-programmer` + `tdd-guide` |
36
+ | **Self-critique** ∥ | `type-design-analyzer` + `api-guardian` + `ux-reviewer` |
37
+ | **Harden** ∥ | `performance-optimizer` + `refactor-cleaner` |
38
+ | **Prove** | — (runs the verification command) |
39
+
40
+ - **It recalls before it builds.**
41
+ - **Proof gate:** `verifyArtifacts()` marks work verified only when a command actually ran and exited clean — a truthy claim is explicitly not proof.
42
+ - **`roundComplete()` blocks** on any unverified artifact or unaddressed critical/high finding. A refuted finding never blocks.
43
+
44
+ ### Added — CLI
45
+
46
+ ```bash
47
+ kodelythecc god --task "add rate limiting" # plan + cost estimate
48
+ kodelythecc evil src/auth --all # crew + cost estimate
49
+ kodelythecc arena list # past runs
50
+ kodelythecc arena report <run-id> # rounds, trend, open risk
51
+ ```
52
+
53
+ ### Verified
54
+
55
+ - **444 tests, 0 failures** across 32 files (up from 388) — 56 new arena tests
56
+ - Simulated a 4-round run: new-findings trend **[2, 1, 0, 0] → converged**, stop reason *"attacker gave up"*
57
+ - Both CLI surfaces render real plans with real cost estimates
58
+ - Traversal-safe run ids, budget refusal, and convergence-streak reset all covered by tests
59
+
60
+ ### Assets
61
+
62
+ - `social/card-god.svg` + `social/card-evil.svg`; SVG badges → v2.6.0; 8K PNGs re-rendered.
63
+
5
64
  ## v2.5.4 — Memory: direct capture CLI + aggressive auto-capture (July 2026)
6
65
 
7
66
  Two memory improvements after an end-to-end verification pass confirmed the pipeline works but only captured when the user said "thanks".
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.5.4
1
+ 2.6.0
@@ -424,6 +424,103 @@ if (args[0] === 'memory') {
424
424
  } catch (e) { process.stderr.write(`[memory] ${e.message}\n`); process.exit(1); }
425
425
  }
426
426
 
427
+ // ── Subcommand: god / evil / arena (the adversarial arena) ──────────────────
428
+ // Usage:
429
+ // kodelythecc god --task "<task>" [--json] plan a GOD-mode build
430
+ // kodelythecc evil [scope] [--all|--license|...] plan an EVIL-mode sweep
431
+ // kodelythecc arena list list past arena runs
432
+ // kodelythecc arena report <run-id> show a run's report
433
+ if (args[0] === 'god' || args[0] === 'evil' || args[0] === 'arena') {
434
+ const mode = args[0];
435
+ const rest = args.slice(1);
436
+ const w = (m) => process.stdout.write(m + '\n');
437
+ const wantJson = rest.includes('--json');
438
+ function flag(name, dflt) {
439
+ const i = rest.indexOf('--' + name);
440
+ return i >= 0 && rest[i + 1] && !rest[i + 1].startsWith('--') ? rest[i + 1] : dflt;
441
+ }
442
+ try {
443
+ if (mode === 'god') {
444
+ const god = require(path.join(ROOT, 'scripts', 'arena', 'god.js'));
445
+ const task = flag('task') || rest.find(a => !a.startsWith('--'));
446
+ if (!task) { process.stderr.write('usage: kodelythecc god --task "<what to build>"\n'); process.exit(2); }
447
+ const plan = god.planBuild({ task });
448
+ if (wantJson) { w(JSON.stringify(plan, null, 2)); process.exit(0); }
449
+ w('');
450
+ w(`\x1b[1mGOD mode\x1b[0m — ${plan.task}`);
451
+ w('─'.repeat(60));
452
+ for (const s of plan.stages) {
453
+ const agents = s.agents.length ? s.agents.join(', ') : '(no agent — local operation)';
454
+ w(`\x1b[32m${String(s.id).padEnd(9)}\x1b[0m ${s.label.padEnd(14)} ${s.parallel ? '∥' : '→'} ${agents}`);
455
+ }
456
+ w('─'.repeat(60));
457
+ w(`${plan.stages.length} stages · est. ~${(plan.estimatedTokens / 1000).toFixed(0)}k tokens`);
458
+ w(`Run it in your AI tool with: \x1b[36m/god-mode ${plan.task}\x1b[0m`);
459
+ w('');
460
+ process.exit(0);
461
+ }
462
+
463
+ if (mode === 'evil') {
464
+ const evil = require(path.join(ROOT, 'scripts', 'arena', 'evil.js'));
465
+ const scope = rest.find(a => !a.startsWith('--')) || '.';
466
+ const plan = evil.planSweep({ scope, flags: rest.filter(a => a.startsWith('--')) });
467
+ if (wantJson) { w(JSON.stringify(plan, null, 2)); process.exit(0); }
468
+ w('');
469
+ w(`\x1b[1mEVIL mode\x1b[0m — adversarial sweep of \x1b[33m${scope}\x1b[0m`);
470
+ w('─'.repeat(60));
471
+ for (const agent of plan.crew) {
472
+ w(`\x1b[31m✗\x1b[0m ${agent.padEnd(26)} ${evil.CREW[agent].hunts}`);
473
+ }
474
+ w('─'.repeat(60));
475
+ w(`${plan.crew.length} hunters · est. ~${(plan.estimatedTokens / 1000).toFixed(0)}k tokens`);
476
+ w('Findings are scored (severity × confidence × exploitability) and');
477
+ w('adversarially verified — unreproducible findings get refuted.');
478
+ w(`Run it in your AI tool with: \x1b[36m/evil-mode ${scope}\x1b[0m`);
479
+ w('');
480
+ process.exit(0);
481
+ }
482
+
483
+ // arena
484
+ const state = require(path.join(ROOT, 'scripts', 'arena', 'state.js'));
485
+ const sub = rest[0] || 'list';
486
+ if (sub === 'list') {
487
+ const runs = state.listRuns();
488
+ if (!runs.length) { w('No arena runs yet.'); process.exit(0); }
489
+ w('');
490
+ w(`\x1b[1mArena runs\x1b[0m (${runs.length})`);
491
+ for (const r of runs.slice(0, 20)) {
492
+ const icon = r.status === 'converged' ? '\x1b[32m✓\x1b[0m' : r.status === 'running' ? '\x1b[33m•\x1b[0m' : '\x1b[31m✗\x1b[0m';
493
+ w(` ${icon} ${r.runId} ${String(r.rounds).padStart(2)} rounds risk ${String(r.openRisk).padStart(6)} ${r.task.slice(0, 40)}`);
494
+ }
495
+ w('');
496
+ process.exit(0);
497
+ }
498
+ if (sub === 'report') {
499
+ const runId = rest[1];
500
+ if (!runId) { process.stderr.write('usage: kodelythecc arena report <run-id>\n'); process.exit(2); }
501
+ const run = state.load(runId);
502
+ if (!run) { process.stderr.write(`no such run: ${runId}\n`); process.exit(1); }
503
+ const s = state.summarize(run);
504
+ if (wantJson) { w(JSON.stringify({ summary: s, rounds: run.rounds }, null, 2)); process.exit(0); }
505
+ w('');
506
+ w(`\x1b[1mArena report\x1b[0m — ${s.runId}`);
507
+ w(` task: ${s.task}`);
508
+ w(` status: ${s.status}${s.stopReason ? ` (${s.stopReason})` : ''}`);
509
+ w(` rounds: ${s.rounds}`);
510
+ w(` new/round: ${s.trend.join(' → ') || '—'} ${s.trend.length > 1 && s.trend[s.trend.length - 1] === 0 ? '\x1b[32m(attacker gave up)\x1b[0m' : ''}`);
511
+ w(` open issues: ${s.openFindings} (${s.confirmed} confirmed) · open risk ${s.openRisk}`);
512
+ w(` tokens: ${s.tokensSpent.toLocaleString()}`);
513
+ w('');
514
+ process.exit(0);
515
+ }
516
+ process.stderr.write('unknown arena subcommand. try: list | report <run-id>\n');
517
+ process.exit(2);
518
+ } catch (e) {
519
+ process.stderr.write(`[${mode}] ${e.message}\n`);
520
+ process.exit(1);
521
+ }
522
+ }
523
+
427
524
  if (args[0] === 'doctor') {
428
525
  const { run, PASS, WARN, FAIL } = require(path.join(ROOT, 'scripts', 'doctor-health.js'));
429
526
  const report = run();
@@ -0,0 +1,106 @@
1
+ ---
2
+ description: EVIL mode — adversarial sweep with scored findings, adversarial verification, and loop-until-dry. The upgrade of /devil-mode — unreproducible findings get refuted instead of padding the report.
3
+ argument-hint: "[scope] [--all|--license|--theft|--jailbreak|--chaos|--pre-public|--pre-launch]"
4
+ ---
5
+
6
+ # /evil-mode — Attack Your Own Code
7
+
8
+ Eight red-team specialists hunt your repository the way an attacker would. Unlike a plain sweep, EVIL mode **grades its own findings** and then **tries to disprove them** — so what reaches you is what survived scrutiny, not everything anyone thought of.
9
+
10
+ > Findings will be uncomfortable. That is the point. This attacks **your own** codebase so real attackers find nothing left.
11
+
12
+ ## What makes this different from a one-shot scan
13
+
14
+ | | Plain sweep | EVIL mode |
15
+ |---|---|---|
16
+ | Output | Flat list | Scored: severity × confidence × exploitability |
17
+ | False positives | You triage them | **Adversarially refuted** before you see them |
18
+ | Coverage | One pass | **Loops until two rounds find nothing new** |
19
+ | Handoff | Prose | Structured findings `/god-mode` can consume |
20
+
21
+ ## The crew
22
+
23
+ **Core (always):** `prompt-injection-hunter` · `supply-chain-auditor` · `secret-hunter` · `backdoor-hunter`
24
+
25
+ **Opt-in:** `--license` · `--theft` · `--jailbreak` · `--chaos` · `--all`
26
+
27
+ **Presets:** `--pre-public` (license + theft) · `--pre-launch` (jailbreak + chaos)
28
+
29
+ ## Usage
30
+
31
+ ```
32
+ /evil-mode # core 4 on the whole repo
33
+ /evil-mode src/auth # focus a module
34
+ /evil-mode --all # all 8 hunters
35
+ /evil-mode --pre-public # before open-sourcing
36
+ ```
37
+
38
+ Preview crew and cost first:
39
+ ```bash
40
+ kodelythecc evil src/auth --all
41
+ ```
42
+
43
+ ## Instructions to the assistant
44
+
45
+ ### Stage 1 — Hunt (parallel)
46
+ Launch the selected crew **simultaneously** via the Task tool. Give each agent its scope and tell it:
47
+
48
+ - `evidence` must **quote the actual offending code**, not describe it.
49
+ - `repro` must be concrete steps or a command.
50
+ - Only claim `confirmed` when you have actually reproduced it.
51
+ - **Do not pad.** Five confirmed findings beat forty guesses.
52
+
53
+ Each finding comes back as:
54
+ ```json
55
+ { "title", "severity": "critical|high|medium|low|info",
56
+ "confidence": "confirmed|likely|suspected|speculative",
57
+ "exploitability": "trivial|moderate|hard|theoretical",
58
+ "file", "line", "evidence", "repro", "fix" }
59
+ ```
60
+
61
+ ### Stage 2 — Verify (the part that matters)
62
+ Take the top findings by risk and, for each, launch a **fresh** agent whose job is to **REFUTE it**:
63
+
64
+ 1. Read the actual code — does the evidence match reality?
65
+ 2. Do guards, framework behaviour, or callers already neutralise it?
66
+ 3. Try to reproduce it.
67
+
68
+ Return `confirmed` / `refuted` / `needs_context`. **Default to `refuted` when uncertain.** A finding that cannot be demonstrated is not a finding.
69
+
70
+ Refuted findings stay in the record (so the same false positive isn't re-litigated) but count as **zero risk**.
71
+
72
+ ### Stage 3 — Loop until dry
73
+ If this is an arena run, sweep again — telling each agent which findings are already known and to hunt what the last pass **missed**. Stop when two consecutive rounds surface nothing new.
74
+
75
+ ### Stage 4 — Report
76
+
77
+ ```
78
+ EVIL MODE — <scope> — round <n>
79
+ swept: <agents>
80
+ found: <n> · confirmed <n> · refuted <n>
81
+ open risk: <sum of surviving risk>
82
+
83
+ CONFIRMED (fix these)
84
+ [risk] title — file:line
85
+ repro: <command or steps>
86
+ fix: <what to change>
87
+
88
+ REFUTED (checked, not real)
89
+ title — why it is not exploitable
90
+ ```
91
+
92
+ Order strictly by risk. Never lead with a `speculative` finding.
93
+
94
+ ## Rules
95
+
96
+ - **Never invent findings to look thorough.** An empty confirmed list is a valid, good result.
97
+ - **Never report a finding you could not reproduce** without marking it `speculative`.
98
+ - This is for auditing **code you own or are authorised to test**. It is a defensive tool.
99
+
100
+ ## Handoff
101
+
102
+ ```
103
+ /god-mode fix the confirmed findings from the EVIL sweep
104
+ ```
105
+
106
+ GOD mode treats confirmed findings as mandatory work items — each must be fixed or explicitly accepted as risk.
@@ -0,0 +1,85 @@
1
+ ---
2
+ description: GOD mode — the constructive crew. Recalls past solutions, designs, builds test-first, self-critiques, hardens, and proves the work with a command that actually runs.
3
+ argument-hint: "<what to build or harden>"
4
+ ---
5
+
6
+ # /god-mode — Build It Properly
7
+
8
+ Nine specialists in a six-stage pipeline. This is **not** `/project-launch` (which fires everyone at once) — GOD mode is sequential where sequence matters, parallel where it doesn't, and it refuses to call anything done that it hasn't proven.
9
+
10
+ ## The pipeline
11
+
12
+ | Stage | Agents | What happens |
13
+ |---|---|---|
14
+ | **Recall** | — | Search local memory for prior solutions to this problem |
15
+ | **Design** ∥ | `architect` + `code-architect` | Blueprint: files, interfaces, data flow, build order |
16
+ | **Build** → | `pair-programmer` + `tdd-guide` | Implement test-first; tests must actually run |
17
+ | **Self-critique** ∥ | `type-design-analyzer` + `api-guardian` + `ux-reviewer` | Attack our own work before anyone else can |
18
+ | **Harden** ∥ | `performance-optimizer` + `refactor-cleaner` | Hot spots and dead weight, no behaviour change |
19
+ | **Prove** | — | Run the verification commands |
20
+
21
+ ∥ = parallel · → = sequential
22
+
23
+ ## Usage
24
+
25
+ ```
26
+ /god-mode add rate limiting to the payments API
27
+ /god-mode harden the webhook signature verification
28
+ /god-mode # asks what to build
29
+ ```
30
+
31
+ Preview the plan and cost from the terminal first:
32
+ ```bash
33
+ kodelythecc god --task "add rate limiting to the payments API"
34
+ ```
35
+
36
+ ## Instructions to the assistant
37
+
38
+ Run the stages **in order**. Do not skip Recall and do not skip Prove.
39
+
40
+ ### Stage 1 — Recall (do this first, always)
41
+ Search local memory for prior work on this problem:
42
+ - Use the `recall_memory` MCP tool, or `kodelythecc memory recall "<task>"`.
43
+ - For each relevant hit, state: the past problem, the approach that worked, and whether it applies here.
44
+ - **If nothing relevant exists, say so plainly.** Never invent a memory.
45
+
46
+ ### Stage 2 — Design (parallel)
47
+ Launch `architect` and `code-architect` together via the Task tool. Produce one concrete blueprint: exact files, interfaces, data flow, build order. No hand-waving.
48
+
49
+ ### Stage 3 — Build (sequential)
50
+ Launch `pair-programmer` and `tdd-guide`. Write the test first, watch it fail, implement, watch it pass. Report which files you created or changed.
51
+
52
+ ### Stage 4 — Self-critique (parallel)
53
+ Launch `type-design-analyzer`, `api-guardian`, and `ux-reviewer` together. Be genuinely adversarial about your own output — it is far cheaper to find a problem here than to let `/evil-mode` find it later.
54
+
55
+ ### Stage 5 — Harden (parallel)
56
+ Launch `performance-optimizer` and `refactor-cleaner`. Improve without changing behaviour. Tests must stay green.
57
+
58
+ ### Stage 6 — Prove (mandatory)
59
+ **Run the verification command.** A test suite, a build, a benchmark — something that exits 0.
60
+
61
+ - If it passes: report the command and its output.
62
+ - If it fails: **the round is not complete.** Fix it and re-run. Do not report success.
63
+ - "Should work" / "looks correct" is a failure of this stage.
64
+
65
+ ## The output contract
66
+
67
+ End every GOD-mode run with:
68
+
69
+ ```
70
+ GOD MODE — <task>
71
+ files: <what you created or changed>
72
+ verified: <the command you ran> → <pass/fail + evidence>
73
+ memory: <what you recalled, or "no prior art">
74
+ open: <anything you deliberately did not do, and why>
75
+ ```
76
+
77
+ ## Handoff
78
+
79
+ When the work touches security, auth, payments, user input, or dependencies, follow with:
80
+
81
+ ```
82
+ /evil-mode <the files you changed>
83
+ ```
84
+
85
+ That is the adversarial half — it will try to break exactly what you just built.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodelyth-ecc",
3
- "version": "2.5.4",
3
+ "version": "2.6.0",
4
4
  "description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 194 skills, 97 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
5
5
  "author": "Kodelyth <github.com/sifxprime>",
6
6
  "license": "MIT",
@@ -0,0 +1,169 @@
1
+ // scripts/arena/contract.js
2
+ // The shared data contract between GOD mode (builds) and EVIL mode (attacks).
3
+ //
4
+ // Without one schema both crews agree on, they cannot exchange results:
5
+ // EVIL produces Findings, GOD consumes them and produces Artifacts, and the
6
+ // Arena scores both to decide whether to run another round.
7
+ //
8
+ // Pure data + validation. Zero dependencies, no I/O.
9
+
10
+ 'use strict';
11
+
12
+ // ── Severity / confidence scales ─────────────────────────────────────────────
13
+ // Severity is what it costs if real. Confidence is how sure we are it IS real.
14
+ // Exploitability is how hard it is to actually trigger. Risk multiplies all three
15
+ // so a "critical but almost certainly a false positive" ranks below a
16
+ // "high, confirmed, trivially exploitable".
17
+
18
+ const SEVERITY = { critical: 10, high: 7, medium: 4, low: 2, info: 1 };
19
+ const CONFIDENCE = { confirmed: 1.0, likely: 0.7, suspected: 0.4, speculative: 0.15 };
20
+ const EXPLOITABILITY = { trivial: 1.0, moderate: 0.7, hard: 0.4, theoretical: 0.2 };
21
+
22
+ const VERDICT = {
23
+ CONFIRMED: 'confirmed', // verification reproduced it
24
+ REFUTED: 'refuted', // verification proved it is a false positive
25
+ UNVERIFIED: 'unverified', // not yet challenged
26
+ NEEDS_CONTEXT: 'needs_context', // can't tell without runtime/secrets we don't have
27
+ };
28
+
29
+ // ── Finding ──────────────────────────────────────────────────────────────────
30
+ // One thing EVIL mode believes is wrong. `repro` is what makes a finding
31
+ // actionable rather than an opinion — GOD mode cannot fix what it cannot reproduce.
32
+
33
+ function makeFinding(input = {}) {
34
+ const f = {
35
+ id: input.id || null, // stable hash, assigned by dedupe
36
+ agent: input.agent || 'unknown',
37
+ title: String(input.title || '').slice(0, 200),
38
+ severity: SEVERITY[input.severity] ? input.severity : 'medium',
39
+ confidence: CONFIDENCE[input.confidence] ? input.confidence : 'suspected',
40
+ exploitability: EXPLOITABILITY[input.exploitability] ? input.exploitability : 'moderate',
41
+ file: input.file || null,
42
+ line: Number.isInteger(input.line) ? input.line : null,
43
+ evidence: String(input.evidence || '').slice(0, 2000),
44
+ repro: input.repro ? String(input.repro).slice(0, 2000) : null,
45
+ fix: input.fix ? String(input.fix).slice(0, 2000) : null,
46
+ verdict: VERDICT[String(input.verdict || '').toUpperCase()] || input.verdict || VERDICT.UNVERIFIED,
47
+ round: Number.isInteger(input.round) ? input.round : 0,
48
+ };
49
+ f.id = f.id || fingerprint(f);
50
+ f.risk = riskScore(f);
51
+ return f;
52
+ }
53
+
54
+ // Stable identity: same issue in the same place is the same finding across
55
+ // rounds, even if the wording of the title drifts between agents.
56
+ function fingerprint(f) {
57
+ const basis = [
58
+ (f.file || 'nofile').toLowerCase(),
59
+ f.line == null ? 'noline' : String(f.line),
60
+ normalizeTitle(f.title),
61
+ ].join('::');
62
+ // FNV-1a — deterministic, dependency-free, good enough for dedupe keys.
63
+ let h = 0x811c9dc5;
64
+ for (let i = 0; i < basis.length; i++) {
65
+ h ^= basis.charCodeAt(i);
66
+ h = Math.imul(h, 0x01000193) >>> 0;
67
+ }
68
+ return h.toString(16).padStart(8, '0');
69
+ }
70
+
71
+ function normalizeTitle(title) {
72
+ return String(title || '')
73
+ .toLowerCase()
74
+ .replace(/[^a-z0-9 ]+/g, ' ')
75
+ .replace(/\b(a|an|the|is|are|in|on|of|to|for|with|at|by)\b/g, ' ')
76
+ .replace(/\s+/g, ' ')
77
+ .trim()
78
+ .slice(0, 80);
79
+ }
80
+
81
+ // risk = severity × confidence × exploitability → 0..10
82
+ function riskScore(f) {
83
+ const s = SEVERITY[f.severity] || SEVERITY.medium;
84
+ const c = CONFIDENCE[f.confidence] || CONFIDENCE.suspected;
85
+ const e = EXPLOITABILITY[f.exploitability] || EXPLOITABILITY.moderate;
86
+ return Math.round(s * c * e * 100) / 100;
87
+ }
88
+
89
+ // Refuted findings carry no risk — that is the whole point of verification.
90
+ function effectiveRisk(f) {
91
+ return f.verdict === VERDICT.REFUTED ? 0 : riskScore(f);
92
+ }
93
+
94
+ // ── Dedupe / diff across rounds ──────────────────────────────────────────────
95
+
96
+ function dedupe(findings = []) {
97
+ const byId = new Map();
98
+ for (const raw of findings) {
99
+ const f = raw && raw.id && raw.risk != null ? raw : makeFinding(raw || {});
100
+ const prev = byId.get(f.id);
101
+ // Keep the strongest claim when two agents report the same thing.
102
+ if (!prev || effectiveRisk(f) > effectiveRisk(prev)) byId.set(f.id, f);
103
+ }
104
+ return [...byId.values()].sort((a, b) => effectiveRisk(b) - effectiveRisk(a));
105
+ }
106
+
107
+ // What EVIL found this round that it had never found before.
108
+ function newFindings(current = [], seenIds = []) {
109
+ const seen = new Set(seenIds);
110
+ return dedupe(current).filter(f => !seen.has(f.id));
111
+ }
112
+
113
+ // ── Artifact (what GOD produces) ─────────────────────────────────────────────
114
+ // GOD must emit something checkable. `verified` is only true when a command
115
+ // actually ran and passed — claims alone never count.
116
+
117
+ function makeArtifact(input = {}) {
118
+ return {
119
+ kind: ['code', 'test', 'doc', 'config', 'benchmark'].includes(input.kind) ? input.kind : 'code',
120
+ files: Array.isArray(input.files) ? input.files.slice(0, 50) : [],
121
+ summary: String(input.summary || '').slice(0, 1000),
122
+ verifyCommand: input.verifyCommand ? String(input.verifyCommand).slice(0, 300) : null,
123
+ verified: input.verified === true,
124
+ round: Number.isInteger(input.round) ? input.round : 0,
125
+ };
126
+ }
127
+
128
+ // ── Round verdict ────────────────────────────────────────────────────────────
129
+
130
+ function makeRoundVerdict(input = {}) {
131
+ const findings = dedupe(input.findings || []);
132
+ const fresh = Array.isArray(input.newFindingIds) ? input.newFindingIds : [];
133
+ const open = findings.filter(f => f.verdict !== VERDICT.REFUTED);
134
+ const confirmed = findings.filter(f => f.verdict === VERDICT.CONFIRMED);
135
+ return {
136
+ round: Number.isInteger(input.round) ? input.round : 0,
137
+ findings,
138
+ newFindingIds: fresh,
139
+ counts: {
140
+ total: findings.length,
141
+ open: open.length,
142
+ confirmed: confirmed.length,
143
+ refuted: findings.length - open.length,
144
+ new: fresh.length,
145
+ },
146
+ // Total unmitigated risk still standing after verification.
147
+ openRisk: Math.round(open.reduce((sum, f) => sum + effectiveRisk(f), 0) * 100) / 100,
148
+ artifacts: (input.artifacts || []).map(makeArtifact),
149
+ };
150
+ }
151
+
152
+ // ── Convergence ──────────────────────────────────────────────────────────────
153
+ // The arena stops when the attacker gives up: N consecutive rounds where EVIL
154
+ // found nothing genuinely new. Not "zero findings" — findings may remain open
155
+ // and accepted; what matters is that attacking harder stops yielding anything.
156
+
157
+ function hasConverged(roundVerdicts = [], quietRoundsRequired = 2) {
158
+ if (roundVerdicts.length < quietRoundsRequired) return false;
159
+ return roundVerdicts
160
+ .slice(-quietRoundsRequired)
161
+ .every(r => (r.counts?.new || 0) === 0);
162
+ }
163
+
164
+ module.exports = {
165
+ SEVERITY, CONFIDENCE, EXPLOITABILITY, VERDICT,
166
+ makeFinding, fingerprint, riskScore, effectiveRisk,
167
+ dedupe, newFindings,
168
+ makeArtifact, makeRoundVerdict, hasConverged,
169
+ };
@@ -0,0 +1,175 @@
1
+ // scripts/arena/evil.js
2
+ // EVIL mode — the adversarial half of the arena.
3
+ //
4
+ // The 8 devil-mode agents already hunt well (real ripgrep/jq detection commands).
5
+ // What they lacked was judgment: everything came back as an undifferentiated pile
6
+ // with no way to tell a confirmed exploit from a hunch.
7
+ //
8
+ // This module adds the three missing pieces:
9
+ // 1. SCORING — severity x confidence x exploitability, so noise sinks
10
+ // 2. VERIFICATION — each finding gets challenged: prove it, or it is refuted
11
+ // 3. LOOP-UNTIL-DRY — keep sweeping until two rounds turn up nothing new
12
+ //
13
+ // It does not call an LLM itself. It builds the agent briefs the orchestrator
14
+ // dispatches, and it grades what comes back. That keeps this file testable.
15
+
16
+ 'use strict';
17
+
18
+ const { makeFinding, dedupe, VERDICT, effectiveRisk } = require('./contract');
19
+
20
+ // ── The crew ─────────────────────────────────────────────────────────────────
21
+ // `core` fires on every sweep. The rest are opt-in via flags, because each one
22
+ // costs a full agent invocation and not every task needs a license audit.
23
+
24
+ const CREW = {
25
+ 'prompt-injection-hunter': { core: true, hunts: 'jailbreaks, indirect injection, system-prompt leaks, tool-call hijacking' },
26
+ 'supply-chain-auditor': { core: true, hunts: 'typosquats, malicious install scripts, lockfile drift, unsigned packages' },
27
+ 'secret-hunter': { core: true, hunts: 'live credentials, git-history leaks, encoded keys, client-bundled env vars' },
28
+ 'backdoor-hunter': { core: true, hunts: 'obfuscated payloads, network beacons, time bombs, eval/exec abuse' },
29
+ 'license-violation-finder':{ core: false, flag: '--license', hunts: 'GPL contamination, missing attribution, copyleft risk' },
30
+ 'code-stealer-detector': { core: false, flag: '--theft', hunts: 'copy-paste provenance, leaked private code, AI-gen origin' },
31
+ 'jailbreak-tester': { core: false, flag: '--jailbreak', hunts: 'live AI-feature red-team, refusal bypass, overrefusal' },
32
+ 'chaos-engineer': { core: false, flag: '--chaos', hunts: 'fault injection, resource exhaustion, hidden assumptions' },
33
+ };
34
+
35
+ const PRESETS = {
36
+ default: ['--core'],
37
+ '--all': Object.keys(CREW),
38
+ '--pre-public':['--core', '--license', '--theft'],
39
+ '--pre-launch':['--core', '--jailbreak', '--chaos'],
40
+ };
41
+
42
+ function selectCrew(flags = []) {
43
+ const set = new Set(Object.entries(CREW).filter(([, m]) => m.core).map(([n]) => n));
44
+ if (flags.includes('--all')) return Object.keys(CREW);
45
+ for (const [name, meta] of Object.entries(CREW)) {
46
+ if (meta.flag && flags.includes(meta.flag)) set.add(name);
47
+ }
48
+ for (const preset of ['--pre-public', '--pre-launch']) {
49
+ if (flags.includes(preset)) {
50
+ for (const f of PRESETS[preset]) {
51
+ if (f === '--core') continue;
52
+ const hit = Object.entries(CREW).find(([, m]) => m.flag === f);
53
+ if (hit) set.add(hit[0]);
54
+ }
55
+ }
56
+ }
57
+ return [...set];
58
+ }
59
+
60
+ // ── Stage 1: hunt briefs ─────────────────────────────────────────────────────
61
+ // Each agent is told to report in the Finding contract, and — critically — that
62
+ // a finding without a reproduction is worth less than no finding at all.
63
+
64
+ function huntBrief({ agent, scope, round, knownFindingIds = [] }) {
65
+ const meta = CREW[agent] || { hunts: 'issues' };
66
+ return [
67
+ `You are ${agent}. Hunt: ${meta.hunts}.`,
68
+ `Scope: ${scope || 'the whole repository'}.`,
69
+ `Round ${round}.`,
70
+ knownFindingIds.length
71
+ ? `Already-reported findings (${knownFindingIds.length}) are known — do NOT re-report them. Hunt for what a previous sweep MISSED. Go deeper: different files, different attack classes, indirect paths.`
72
+ : `First sweep — cover your full detection surface.`,
73
+ '',
74
+ 'Report every finding as JSON matching this shape:',
75
+ '{ "title", "severity": critical|high|medium|low|info, "confidence": confirmed|likely|suspected|speculative,',
76
+ ' "exploitability": trivial|moderate|hard|theoretical, "file", "line", "evidence", "repro", "fix" }',
77
+ '',
78
+ 'Rules that decide whether your finding survives review:',
79
+ '- `evidence` must quote the actual offending code, not describe it.',
80
+ '- `repro` must be concrete steps or a command. A finding you cannot reproduce is `speculative` at best.',
81
+ '- Do not pad. Five confirmed findings beat forty guesses — unverifiable findings get refuted and count against you.',
82
+ '- Only claim `confirmed` when you have actually reproduced it.',
83
+ ].join('\n');
84
+ }
85
+
86
+ // ── Stage 2: verification briefs ─────────────────────────────────────────────
87
+ // The adversarial-verification trick: the verifier is told to REFUTE, not to
88
+ // confirm. Default-to-refuted kills the false positives that otherwise waste
89
+ // GOD mode's entire next round.
90
+
91
+ function verifyBrief(finding) {
92
+ return [
93
+ `Adversarially verify this finding. Your job is to REFUTE it.`,
94
+ '',
95
+ `Title: ${finding.title}`,
96
+ `Claimed severity: ${finding.severity} (${finding.confidence}, ${finding.exploitability})`,
97
+ `Location: ${finding.file || 'unknown'}:${finding.line ?? '?'}`,
98
+ `Evidence: ${finding.evidence || '(none given)'}`,
99
+ `Claimed repro: ${finding.repro || '(none given)'}`,
100
+ '',
101
+ 'Do this:',
102
+ '1. Read the actual code at that location. Does the quoted evidence match reality?',
103
+ '2. Check whether guards, framework behaviour, or callers already neutralise it.',
104
+ '3. Try to reproduce it. If you cannot, say so plainly.',
105
+ '',
106
+ 'Return JSON: { "verdict": "confirmed"|"refuted"|"needs_context", "why": "...", "repro": "..."|null }',
107
+ '',
108
+ 'Default to "refuted" when uncertain. A finding that cannot be demonstrated is not a finding.',
109
+ 'Use "needs_context" only when verification genuinely requires runtime access or secrets you do not have.',
110
+ ].join('\n');
111
+ }
112
+
113
+ // ── Stage 3: grade the returns ───────────────────────────────────────────────
114
+
115
+ function normalizeFindings(rawList = [], { agent, round }) {
116
+ const out = [];
117
+ for (const raw of rawList) {
118
+ if (!raw || !raw.title) continue;
119
+ out.push(makeFinding({ ...raw, agent, round }));
120
+ }
121
+ return out;
122
+ }
123
+
124
+ // Apply verification verdicts back onto the findings. A refuted finding stays in
125
+ // the record (so the same false positive is not re-litigated next round) but its
126
+ // effective risk drops to zero.
127
+ function applyVerdicts(findings = [], verdicts = {}) {
128
+ return findings.map(f => {
129
+ const v = verdicts[f.id];
130
+ if (!v) return f;
131
+ const next = { ...f, verdict: v.verdict || f.verdict };
132
+ if (v.repro && !next.repro) next.repro = String(v.repro).slice(0, 2000);
133
+ if (v.why) next.evidence = `${next.evidence}\n[verification] ${v.why}`.slice(0, 2000);
134
+ // Confirming a finding raises certainty; refuting zeroes it out via effectiveRisk.
135
+ if (next.verdict === VERDICT.CONFIRMED) next.confidence = 'confirmed';
136
+ return makeFinding(next);
137
+ });
138
+ }
139
+
140
+ // Which findings are worth spending a verification pass on. Verifying an
141
+ // already-confirmed or clearly-trivial item is wasted tokens.
142
+ function selectForVerification(findings = [], { max = 12, minRisk = 1.0 } = {}) {
143
+ return dedupe(findings)
144
+ .filter(f => f.verdict === VERDICT.UNVERIFIED && f.risk >= minRisk)
145
+ .slice(0, max);
146
+ }
147
+
148
+ // What GOD mode actually has to fix: open, verified-or-plausible, worst first.
149
+ function actionable(findings = [], { minRisk = 2.0 } = {}) {
150
+ return dedupe(findings)
151
+ .filter(f => f.verdict !== VERDICT.REFUTED)
152
+ .filter(f => effectiveRisk(f) >= minRisk)
153
+ .sort((a, b) => effectiveRisk(b) - effectiveRisk(a));
154
+ }
155
+
156
+ // ── Sweep plan ───────────────────────────────────────────────────────────────
157
+ // The orchestrator asks for a plan, dispatches the agents, and feeds results back.
158
+
159
+ function planSweep({ scope, flags = [], round = 1, knownFindingIds = [] } = {}) {
160
+ const crew = selectCrew(flags);
161
+ return {
162
+ round,
163
+ crew,
164
+ briefs: crew.map(agent => ({ agent, brief: huntBrief({ agent, scope, round, knownFindingIds }) })),
165
+ estimatedTokens: crew.length * 12000, // rough: one agent sweep ≈ 12k
166
+ };
167
+ }
168
+
169
+ module.exports = {
170
+ CREW, PRESETS,
171
+ selectCrew, planSweep,
172
+ huntBrief, verifyBrief,
173
+ normalizeFindings, applyVerdicts,
174
+ selectForVerification, actionable,
175
+ };
@@ -0,0 +1,208 @@
1
+ // scripts/arena/god.js
2
+ // GOD mode — the constructive half of the arena.
3
+ //
4
+ // This is deliberately NOT "fire nine agents at once" — /project-launch already
5
+ // does that. GOD mode is a pipeline with three properties the parallel commands
6
+ // do not have:
7
+ //
8
+ // 1. It RECALLS before it builds — past solutions inform the design
9
+ // 2. It must emit VERIFIABLE artifacts — a claim is not a result; a passing
10
+ // command is
11
+ // 3. It SELF-CRITIQUES before shipping — it attacks its own work before EVIL
12
+ // gets the chance
13
+ //
14
+ // In arena mode it additionally consumes EVIL's verified findings and must
15
+ // address them before the round can close.
16
+
17
+ 'use strict';
18
+
19
+ const { makeArtifact } = require('./contract');
20
+
21
+ // ── The pipeline ─────────────────────────────────────────────────────────────
22
+ // Stages run in order. Each names the specialists it draws on; the orchestrator
23
+ // dispatches them (in parallel where the stage allows it).
24
+
25
+ const STAGES = [
26
+ {
27
+ id: 'recall',
28
+ label: 'Recall',
29
+ parallel: false,
30
+ agents: [], // no agent — this is a memory lookup
31
+ goal: 'Find what we already know about this problem before designing anything new.',
32
+ },
33
+ {
34
+ id: 'design',
35
+ label: 'Design',
36
+ parallel: true,
37
+ agents: ['architect', 'code-architect'],
38
+ goal: 'Produce a concrete blueprint: files, interfaces, data flow, build order.',
39
+ },
40
+ {
41
+ id: 'build',
42
+ label: 'Build',
43
+ parallel: false,
44
+ agents: ['pair-programmer', 'tdd-guide'],
45
+ goal: 'Implement the blueprint test-first. Tests must actually run and pass.',
46
+ },
47
+ {
48
+ id: 'critique',
49
+ label: 'Self-critique',
50
+ parallel: true,
51
+ agents: ['type-design-analyzer', 'api-guardian', 'ux-reviewer'],
52
+ goal: 'Attack our own work: weak types, breaking contracts, unusable flows.',
53
+ },
54
+ {
55
+ id: 'harden',
56
+ label: 'Harden',
57
+ parallel: true,
58
+ agents: ['performance-optimizer', 'refactor-cleaner'],
59
+ goal: 'Remove hot spots and dead weight without changing behaviour.',
60
+ },
61
+ {
62
+ id: 'prove',
63
+ label: 'Prove',
64
+ parallel: false,
65
+ agents: [], // no agent — this runs commands
66
+ goal: 'Run the verification commands. Nothing ships unproven.',
67
+ },
68
+ ];
69
+
70
+ function stage(id) {
71
+ return STAGES.find(s => s.id === id) || null;
72
+ }
73
+
74
+ // ── Briefs ───────────────────────────────────────────────────────────────────
75
+
76
+ function recallBrief({ task }) {
77
+ return [
78
+ `Before building anything, search local memory for prior work related to:`,
79
+ `"${task}"`,
80
+ '',
81
+ 'Use the memory store (BM25 recall). For each relevant hit, report:',
82
+ '- what the past problem was',
83
+ '- what approach actually worked',
84
+ '- whether it applies here, or why this case differs',
85
+ '',
86
+ 'If nothing relevant exists, say so plainly and move on. Do not invent a memory.',
87
+ ].join('\n');
88
+ }
89
+
90
+ function stageBrief({ stageId, task, blueprint = null, findings = [], round = 1 }) {
91
+ const s = stage(stageId);
92
+ if (!s) throw new Error(`god: unknown stage "${stageId}"`);
93
+
94
+ const lines = [
95
+ `GOD mode — stage: ${s.label} (round ${round}).`,
96
+ `Task: ${task}`,
97
+ `Goal of this stage: ${s.goal}`,
98
+ ];
99
+
100
+ if (blueprint) {
101
+ lines.push('', 'Blueprint from the design stage:', blueprint);
102
+ }
103
+
104
+ // Arena mode: EVIL's verified findings are non-negotiable work items.
105
+ if (findings.length) {
106
+ lines.push(
107
+ '',
108
+ `EVIL mode found ${findings.length} verified issue(s) in the last round. These are NOT suggestions — each must be fixed or explicitly justified as accepted risk:`,
109
+ ...findings.map((f, i) =>
110
+ ` ${i + 1}. [${f.severity}/${f.confidence}] ${f.title}` +
111
+ `${f.file ? ` (${f.file}:${f.line ?? '?'})` : ''}` +
112
+ `${f.repro ? `\n repro: ${String(f.repro).split('\n')[0].slice(0, 160)}` : ''}`),
113
+ );
114
+ }
115
+
116
+ lines.push(
117
+ '',
118
+ 'Output contract:',
119
+ '- State exactly which files you changed or created.',
120
+ '- Give a `verifyCommand` that PROVES the work (a test run, a build, a benchmark).',
121
+ '- Do not claim success you have not observed. "Should work" is a failure of this stage.',
122
+ );
123
+
124
+ if (stageId === 'critique') {
125
+ lines.push(
126
+ '',
127
+ 'Be genuinely adversarial about our own output. It is cheaper to find it here than to let EVIL find it next round.',
128
+ );
129
+ }
130
+
131
+ return lines.join('\n');
132
+ }
133
+
134
+ // ── Verification ─────────────────────────────────────────────────────────────
135
+ // The heart of GOD mode: an artifact only counts as verified when a command
136
+ // actually ran and exited clean. `runner` is injected so this stays testable
137
+ // and so callers control what is allowed to execute.
138
+
139
+ function verifyArtifacts(artifacts = [], runner) {
140
+ if (typeof runner !== 'function') {
141
+ throw new Error('god: verifyArtifacts requires a runner(command) -> {ok, output}');
142
+ }
143
+ return artifacts.map(a => {
144
+ const art = makeArtifact(a);
145
+ if (!art.verifyCommand) {
146
+ return { ...art, verified: false, verifyResult: 'no verify command supplied' };
147
+ }
148
+ let result;
149
+ try {
150
+ result = runner(art.verifyCommand);
151
+ } catch (err) {
152
+ return { ...art, verified: false, verifyResult: `runner threw: ${err.message}` };
153
+ }
154
+ const ok = !!(result && result.ok);
155
+ return {
156
+ ...art,
157
+ verified: ok,
158
+ verifyResult: ok
159
+ ? 'passed'
160
+ : `failed: ${String((result && result.output) || 'no output').slice(0, 300)}`,
161
+ };
162
+ });
163
+ }
164
+
165
+ // A round only closes when every artifact proved itself AND every high-risk
166
+ // finding was addressed. This is what stops "I fixed it" from being enough.
167
+ function roundComplete({ artifacts = [], findings = [], addressedIds = [] } = {}) {
168
+ const unverified = artifacts.filter(a => !a.verified);
169
+ const addressed = new Set(addressedIds);
170
+ const mustFix = findings.filter(f =>
171
+ f.verdict !== 'refuted' && (f.severity === 'critical' || f.severity === 'high'));
172
+ const outstanding = mustFix.filter(f => !addressed.has(f.id));
173
+
174
+ return {
175
+ complete: unverified.length === 0 && outstanding.length === 0,
176
+ unverifiedArtifacts: unverified.map(a => a.summary || a.kind),
177
+ outstandingFindings: outstanding.map(f => f.title),
178
+ };
179
+ }
180
+
181
+ // ── Plan ─────────────────────────────────────────────────────────────────────
182
+
183
+ function planBuild({ task, findings = [], round = 1, skipStages = [] } = {}) {
184
+ if (!task || !String(task).trim()) throw new Error('god: task is required');
185
+ const active = STAGES.filter(s => !skipStages.includes(s.id));
186
+ return {
187
+ round,
188
+ task,
189
+ stages: active.map(s => ({
190
+ id: s.id,
191
+ label: s.label,
192
+ parallel: s.parallel,
193
+ agents: s.agents,
194
+ brief: s.id === 'recall'
195
+ ? recallBrief({ task })
196
+ : stageBrief({ stageId: s.id, task, findings, round }),
197
+ })),
198
+ // Only agent stages cost tokens; recall and prove are local operations.
199
+ estimatedTokens: active.reduce((n, s) => n + s.agents.length * 10000, 0),
200
+ };
201
+ }
202
+
203
+ module.exports = {
204
+ STAGES, stage,
205
+ recallBrief, stageBrief,
206
+ verifyArtifacts, roundComplete,
207
+ planBuild,
208
+ };
@@ -0,0 +1,195 @@
1
+ // scripts/arena/state.js
2
+ // Persistent, resumable state for an arena run.
3
+ //
4
+ // An arena run is expensive (multiple agent crews × multiple rounds), so it must
5
+ // survive a crash, a Ctrl-C, or a budget abort without losing the rounds already
6
+ // paid for. Every round is appended to a run file under ~/.kodelythecc/arena/.
7
+ //
8
+ // Also owns the hard stops: max rounds, token budget, wall-clock. These are not
9
+ // advisory — the arena aborts cleanly and reports partial results.
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+
17
+ const { makeRoundVerdict, hasConverged, dedupe } = require('./contract');
18
+
19
+ const DIR = process.env.KODELYTH_ARENA_DIR
20
+ || path.join(os.homedir(), '.kodelythecc', 'arena');
21
+
22
+ const DEFAULTS = {
23
+ maxRounds: 3, // deliberately low — see docs/arena.md on cost
24
+ tokenBudget: 400000,
25
+ wallClockMs: 45 * 60 * 1000,
26
+ quietRoundsRequired: 2,
27
+ };
28
+
29
+ function ensureDir() {
30
+ fs.mkdirSync(DIR, { recursive: true });
31
+ }
32
+
33
+ function runPath(runId) {
34
+ // runId is generated internally; still refuse traversal in case a caller
35
+ // passes one in from a CLI flag.
36
+ const safe = String(runId).replace(/[^A-Za-z0-9._-]/g, '');
37
+ return path.join(DIR, `${safe}.json`);
38
+ }
39
+
40
+ function newRunId(now = Date.now(), rand = Math.random) {
41
+ const stamp = new Date(now).toISOString().replace(/[:.]/g, '-').slice(0, 19);
42
+ const suffix = Math.floor(rand() * 0xffff).toString(16).padStart(4, '0');
43
+ return `arena-${stamp}-${suffix}`;
44
+ }
45
+
46
+ // ── Lifecycle ────────────────────────────────────────────────────────────────
47
+
48
+ function createRun({ task, limits = {}, runId = null, now = Date.now() } = {}) {
49
+ if (!task || !String(task).trim()) throw new Error('arena: task is required');
50
+ const run = {
51
+ runId: runId || newRunId(now),
52
+ task: String(task).slice(0, 1000),
53
+ startedAt: new Date(now).toISOString(),
54
+ limits: { ...DEFAULTS, ...limits },
55
+ spent: { tokens: 0, rounds: 0, ms: 0 },
56
+ rounds: [],
57
+ seenFindingIds: [],
58
+ status: 'running', // running | converged | exhausted | aborted
59
+ stopReason: null,
60
+ };
61
+ save(run);
62
+ return run;
63
+ }
64
+
65
+ function save(run) {
66
+ ensureDir();
67
+ fs.writeFileSync(runPath(run.runId), JSON.stringify(run, null, 2));
68
+ return run;
69
+ }
70
+
71
+ function load(runId) {
72
+ const p = runPath(runId);
73
+ if (!fs.existsSync(p)) return null;
74
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
75
+ catch { return null; }
76
+ }
77
+
78
+ function listRuns() {
79
+ if (!fs.existsSync(DIR)) return [];
80
+ return fs.readdirSync(DIR)
81
+ .filter(f => f.endsWith('.json'))
82
+ .map(f => {
83
+ try {
84
+ const r = JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8'));
85
+ return {
86
+ runId: r.runId,
87
+ task: r.task,
88
+ status: r.status,
89
+ rounds: r.rounds?.length || 0,
90
+ openRisk: r.rounds?.length ? r.rounds[r.rounds.length - 1].openRisk : 0,
91
+ startedAt: r.startedAt,
92
+ };
93
+ } catch { return null; }
94
+ })
95
+ .filter(Boolean)
96
+ .sort((a, b) => String(b.startedAt).localeCompare(String(a.startedAt)));
97
+ }
98
+
99
+ // ── Recording a round ────────────────────────────────────────────────────────
100
+
101
+ function recordRound(run, { findings = [], artifacts = [], tokensSpent = 0, elapsedMs = 0 } = {}) {
102
+ const roundNo = run.rounds.length + 1;
103
+ const seen = new Set(run.seenFindingIds);
104
+ const deduped = dedupe(findings);
105
+ const newIds = deduped.filter(f => !seen.has(f.id)).map(f => f.id);
106
+
107
+ const verdict = makeRoundVerdict({
108
+ round: roundNo,
109
+ findings: deduped,
110
+ newFindingIds: newIds,
111
+ artifacts,
112
+ });
113
+
114
+ run.rounds.push(verdict);
115
+ run.seenFindingIds = [...new Set([...run.seenFindingIds, ...deduped.map(f => f.id)])];
116
+ run.spent.rounds = run.rounds.length;
117
+ run.spent.tokens += Math.max(0, Number(tokensSpent) || 0);
118
+ run.spent.ms += Math.max(0, Number(elapsedMs) || 0);
119
+
120
+ const stop = shouldStop(run);
121
+ run.status = stop.stop ? stop.status : 'running';
122
+ run.stopReason = stop.stop ? stop.reason : null;
123
+
124
+ save(run);
125
+ return verdict;
126
+ }
127
+
128
+ // ── Hard stops + convergence ─────────────────────────────────────────────────
129
+
130
+ function shouldStop(run) {
131
+ const { limits, spent } = run;
132
+
133
+ if (hasConverged(run.rounds, limits.quietRoundsRequired)) {
134
+ return {
135
+ stop: true,
136
+ status: 'converged',
137
+ reason: `no new findings in ${limits.quietRoundsRequired} consecutive rounds — attacker gave up`,
138
+ };
139
+ }
140
+ if (spent.rounds >= limits.maxRounds) {
141
+ return { stop: true, status: 'exhausted', reason: `max rounds reached (${limits.maxRounds})` };
142
+ }
143
+ if (spent.tokens >= limits.tokenBudget) {
144
+ return {
145
+ stop: true,
146
+ status: 'aborted',
147
+ reason: `token budget exhausted (${spent.tokens.toLocaleString()} / ${limits.tokenBudget.toLocaleString()})`,
148
+ };
149
+ }
150
+ if (spent.ms >= limits.wallClockMs) {
151
+ return {
152
+ stop: true,
153
+ status: 'aborted',
154
+ reason: `wall-clock limit reached (${Math.round(spent.ms / 60000)} min)`,
155
+ };
156
+ }
157
+ return { stop: false };
158
+ }
159
+
160
+ // Budget check BEFORE spending on another round, so we never blow past the cap.
161
+ function canAffordRound(run, estimatedTokens) {
162
+ const remaining = run.limits.tokenBudget - run.spent.tokens;
163
+ return {
164
+ ok: remaining >= estimatedTokens,
165
+ remaining,
166
+ estimated: estimatedTokens,
167
+ };
168
+ }
169
+
170
+ // ── Summary for reports / dashboard ──────────────────────────────────────────
171
+
172
+ function summarize(run) {
173
+ const last = run.rounds[run.rounds.length - 1] || null;
174
+ const open = last ? last.findings.filter(f => f.verdict !== 'refuted') : [];
175
+ return {
176
+ runId: run.runId,
177
+ task: run.task,
178
+ status: run.status,
179
+ stopReason: run.stopReason,
180
+ rounds: run.rounds.length,
181
+ tokensSpent: run.spent.tokens,
182
+ openFindings: open.length,
183
+ openRisk: last ? last.openRisk : 0,
184
+ confirmed: open.filter(f => f.verdict === 'confirmed').length,
185
+ // Round-over-round new-finding counts — the curve that should trend to zero.
186
+ trend: run.rounds.map(r => r.counts.new),
187
+ artifacts: run.rounds.reduce((n, r) => n + (r.artifacts?.length || 0), 0),
188
+ };
189
+ }
190
+
191
+ module.exports = {
192
+ DIR, DEFAULTS,
193
+ newRunId, createRun, save, load, listRuns,
194
+ recordRound, shouldStop, canAffordRound, summarize,
195
+ };