kodelyth-ecc 2.6.0 → 2.7.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,62 @@
2
2
 
3
3
  All notable changes to Kodelyth ECC are documented here.
4
4
 
5
+ ## v2.7.0 — The Arena: GOD vs EVIL loop (phase 3) (August 2026)
6
+
7
+ The two crews now fight. GOD builds, EVIL attacks, verified findings return to GOD as mandatory work, and the loop repeats **until the attacker gives up**.
8
+
9
+ ### Added — `scripts/arena/arena.js` + `/arena`
10
+
11
+ A **state machine**, not an agent dispatcher — it decides what happens next and grades what comes back, so the entire loop is testable without spending a token. Agent invocation is driven by the AI through `/arena`.
12
+
13
+ ```
14
+ round N: god_build → evil_hunt → evil_verify → round_close
15
+
16
+ converged / out of budget / out of rounds? ──→ report
17
+ ```
18
+
19
+ - **`nextAction(run)`** — the loop's brain. Returns the next step, its briefs, and a token estimate the budget can veto.
20
+ - **Findings carry forward.** Round 1 builds; every later round *fixes what EVIL proved*, with the findings injected into GOD's brief as mandatory items. **Refuted findings are never carried** — a false positive can no longer consume an entire fix round.
21
+ - **Later EVIL rounds know what's already known** and are told to hunt what the last pass missed.
22
+ - **Phase guards** — submitting out of order throws instead of silently corrupting a run.
23
+ - **`affordOrAbort()`** — aborts cleanly *before* an unaffordable step. A readable partial result beats a surprise bill.
24
+ - **Resumable** — a crash mid-loop reloads from disk with phase and spend intact.
25
+ - **`buildReport()`** — markdown report with the trend histogram, per-round detail, still-open findings ranked by real risk, and a refuted section so the same false positive is never re-litigated.
26
+
27
+ ### Added — CLI
28
+
29
+ ```bash
30
+ kodelythecc arena start --task "harden the webhook" --scope src/ --max-rounds 3
31
+ kodelythecc arena next <run-id> # what the loop wants next (JSON)
32
+ kodelythecc arena report <run-id> --md # full markdown report
33
+ ```
34
+
35
+ ### Verified end-to-end
36
+
37
+ Simulated a realistic run at true cost (~191k tokens/round):
38
+
39
+ ```
40
+ round 1 3 new ████████████████████████
41
+ round 2 1 new ████████
42
+ round 3 0 new ·
43
+ round 4 0 new ·
44
+ **Converged** — two consecutive rounds surfaced nothing new.
45
+ ```
46
+
47
+ - Convergence fires on 2 quiet rounds; a fresh finding **restarts** the streak
48
+ - Rounds where GOD left an artifact unproven or a critical unaddressed are flagged **incomplete** in the report
49
+ - A refuting verdict zeroed the finding and kept `openRisk` honest
50
+ - **The budget guard fired for real** — an earlier run at the 400k default aborted at round 3 with a clean partial report, exactly as designed
51
+ - **463 tests, 0 failures** across 33 files (up from 444) — 19 new arena-loop tests
52
+
53
+ ### Honest cost note
54
+
55
+ ~200k tokens per round (GOD ≈ 90k + EVIL ≈ 96k + verification). Defaults are deliberately conservative: **3 rounds, 400k tokens, 45 min**. Use `/god-mode` or `/evil-mode` alone when you don't need the full loop — the arena is for work that must not break.
56
+
57
+ ### Assets
58
+
59
+ `social/card-arena.svg`; SVG badges → v2.7.0; 8K PNGs re-rendered.
60
+
5
61
  ## v2.6.0 — GOD mode + EVIL mode v2 (Arena phases 0-2) (August 2026)
6
62
 
7
63
  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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 2.6.0
1
+ 2.7.0
@@ -482,7 +482,46 @@ if (args[0] === 'god' || args[0] === 'evil' || args[0] === 'arena') {
482
482
 
483
483
  // arena
484
484
  const state = require(path.join(ROOT, 'scripts', 'arena', 'state.js'));
485
+ const arena = require(path.join(ROOT, 'scripts', 'arena', 'arena.js'));
485
486
  const sub = rest[0] || 'list';
487
+
488
+ if (sub === 'start') {
489
+ const task = flag('task') || rest.slice(1).find(a => !a.startsWith('--'));
490
+ if (!task) { process.stderr.write('usage: kodelythecc arena start --task "<goal>" [--scope src/] [--max-rounds 3] [--budget 400000]\n'); process.exit(2); }
491
+ const limits = {};
492
+ if (flag('max-rounds')) limits.maxRounds = Number(flag('max-rounds'));
493
+ if (flag('budget')) limits.tokenBudget = Number(flag('budget'));
494
+ const run = arena.startArena({
495
+ task,
496
+ scope: flag('scope', '.'),
497
+ flags: rest.filter(a => ['--all', '--license', '--theft', '--jailbreak', '--chaos', '--pre-public', '--pre-launch'].includes(a)),
498
+ limits,
499
+ });
500
+ const first = arena.nextAction(run);
501
+ if (wantJson) { w(JSON.stringify({ run: state.summarize(run), next: first }, null, 2)); process.exit(0); }
502
+ w('');
503
+ w(`\x1b[1mArena started\x1b[0m — ${run.task}`);
504
+ w(` run id: \x1b[36m${run.runId}\x1b[0m`);
505
+ w(` scope: ${run.scope}`);
506
+ w(` limits: ${run.limits.maxRounds} rounds · ${run.limits.tokenBudget.toLocaleString()} tokens · ${Math.round(run.limits.wallClockMs / 60000)} min`);
507
+ w(` next: \x1b[32m${first.action}\x1b[0m (round ${first.round}, ~${((first.estimatedTokens || 0) / 1000).toFixed(0)}k tokens)`);
508
+ w('');
509
+ w(`Drive the loop in your AI tool: \x1b[36m/arena ${run.task}\x1b[0m`);
510
+ w(`Inspect anytime: kodelythecc arena report ${run.runId}`);
511
+ w('');
512
+ process.exit(0);
513
+ }
514
+
515
+ if (sub === 'next') {
516
+ const runId = rest[1];
517
+ if (!runId) { process.stderr.write('usage: kodelythecc arena next <run-id>\n'); process.exit(2); }
518
+ const run = state.load(runId);
519
+ if (!run) { process.stderr.write(`no such run: ${runId}\n`); process.exit(1); }
520
+ const action = arena.nextAction(run);
521
+ w(JSON.stringify(action, null, 2));
522
+ process.exit(0);
523
+ }
524
+
486
525
  if (sub === 'list') {
487
526
  const runs = state.listRuns();
488
527
  if (!runs.length) { w('No arena runs yet.'); process.exit(0); }
@@ -502,6 +541,7 @@ if (args[0] === 'god' || args[0] === 'evil' || args[0] === 'arena') {
502
541
  if (!run) { process.stderr.write(`no such run: ${runId}\n`); process.exit(1); }
503
542
  const s = state.summarize(run);
504
543
  if (wantJson) { w(JSON.stringify({ summary: s, rounds: run.rounds }, null, 2)); process.exit(0); }
544
+ if (rest.includes('--md')) { w(arena.buildReport(run)); process.exit(0); }
505
545
  w('');
506
546
  w(`\x1b[1mArena report\x1b[0m — ${s.runId}`);
507
547
  w(` task: ${s.task}`);
@@ -0,0 +1,101 @@
1
+ ---
2
+ description: The Arena — GOD builds, EVIL attacks, repeat until the attacker gives up. The adversarial loop with scored findings, verification, convergence detection, and hard budget stops.
3
+ argument-hint: "<goal> [--scope src/] [--max-rounds 3] [--all]"
4
+ ---
5
+
6
+ # /arena — GOD vs EVIL, until the attacker gives up
7
+
8
+ The two crews fight over your code. GOD builds and hardens. EVIL attacks and tries to break it. Verified findings go back to GOD. Repeat until **two consecutive rounds surface nothing new** — then you ship, knowing an adversary already tried and failed.
9
+
10
+ > **This is the expensive one.** ~200k tokens per round (GOD ≈ 90k + EVIL ≈ 96k + verification). Defaults are 3 rounds / 400k tokens, and the budget guard aborts *before* overspending. Use `/god-mode` or `/evil-mode` alone when you don't need the full loop.
11
+
12
+ ## The loop
13
+
14
+ ```
15
+ round N: GOD builds/fixes → EVIL hunts → EVIL verifies → close round
16
+
17
+ converged? out of budget? out of rounds? ─┤
18
+ no → round N+1 │
19
+ yes → report
20
+ ```
21
+
22
+ **Convergence = the win condition.** Not "zero findings" — findings may remain open and accepted. It means attacking harder stopped yielding anything new.
23
+
24
+ ## Usage
25
+
26
+ ```
27
+ /arena harden the payment webhook
28
+ /arena add rate limiting --scope src/api --max-rounds 2
29
+ /arena prepare this repo for open-source --all
30
+ ```
31
+
32
+ Start and inspect from the terminal:
33
+ ```bash
34
+ kodelythecc arena start --task "harden the webhook" --scope src/ --max-rounds 3
35
+ kodelythecc arena next <run-id> # what the loop wants next (JSON)
36
+ kodelythecc arena report <run-id> --md # full markdown report
37
+ kodelythecc arena list
38
+ ```
39
+
40
+ ## Instructions to the assistant
41
+
42
+ ### 0. Start the run
43
+ ```bash
44
+ kodelythecc arena start --task "<goal>" --scope "<path>" [--max-rounds N]
45
+ ```
46
+ Capture the **run id**. Every step below is driven by:
47
+ ```bash
48
+ kodelythecc arena next <run-id>
49
+ ```
50
+ which returns the next action, its briefs, and a token estimate. **Follow it — do not improvise the order.**
51
+
52
+ ### 1. `god_build`
53
+ Run the GOD-mode stages from the action's `stages` array (see `/god-mode`). Round 1 builds; later rounds **fix what EVIL proved** — those findings arrive in the brief as mandatory work items.
54
+
55
+ Finish by reporting artifacts, each with a `verifyCommand` you **actually ran**.
56
+
57
+ ### 2. `evil_hunt`
58
+ Launch the crew from `briefs` **in parallel** via the Task tool. On round 2+, agents are told what's already known and to hunt what the last pass missed.
59
+
60
+ ### 3. `evil_verify`
61
+ For each target, launch a **fresh** agent with the supplied refute-brief. Its job is to **disprove** the finding. Default to `refuted` when uncertain.
62
+
63
+ ### 4. `round_close`
64
+ Record the round. The state machine decides whether to loop again or stop.
65
+
66
+ ### 5. `report`
67
+ When the action is `report`, print the full markdown:
68
+ ```bash
69
+ kodelythecc arena report <run-id> --md
70
+ ```
71
+
72
+ ## Rules that keep this honest
73
+
74
+ - **Never skip verification.** Unverified findings waste GOD's next entire round — that's the expensive failure mode this design exists to prevent.
75
+ - **Never claim a fix you didn't prove.** An artifact counts only when its command exited clean. A round with an unverified artifact is flagged incomplete in the report.
76
+ - **Never invent findings to look thorough.** An empty confirmed list is a good result.
77
+ - **Respect the budget guard.** If it aborts, report the partial result — a readable partial beats a surprise bill.
78
+ - Convergence is the goal, not zero findings. Accepted risk, stated plainly, is a legitimate outcome.
79
+
80
+ ## Reading the report
81
+
82
+ The trend line is the whole story:
83
+
84
+ ```
85
+ round 1 12 new ████████████████████████
86
+ round 2 4 new ████████
87
+ round 3 0 new ·
88
+ round 4 0 new ·
89
+ **Converged** — two consecutive rounds surfaced nothing new.
90
+ ```
91
+
92
+ Falling to zero means the attacker ran out of ideas. A flat or rising line means **stop and think** — either the code has deep problems, or EVIL is finding new surface each pass because the scope is too broad.
93
+
94
+ ## When to use which
95
+
96
+ | Situation | Command |
97
+ |---|---|
98
+ | Build something properly | `/god-mode` |
99
+ | Audit what already exists | `/evil-mode` |
100
+ | Ship something that must not break | `/arena` |
101
+ | Before open-sourcing | `/arena --all` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodelyth-ecc",
3
- "version": "2.6.0",
3
+ "version": "2.7.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,280 @@
1
+ // scripts/arena/arena.js
2
+ // The Arena — GOD builds, EVIL attacks, repeat until the attacker gives up.
3
+ //
4
+ // This is a STATE MACHINE, not an agent dispatcher. It decides what should
5
+ // happen next and grades what comes back; the actual agent invocations are made
6
+ // by the AI driving `/arena`. Keeping dispatch out of here is what makes the
7
+ // loop testable without spending a single token.
8
+ //
9
+ // round N: god_build -> evil_hunt -> evil_verify -> round_close
10
+ // |
11
+ // converged / out of budget? --------+
12
+ // | no -> round N+1
13
+ // yes
14
+ // v
15
+ // report
16
+
17
+ 'use strict';
18
+
19
+ const god = require('./god');
20
+ const evil = require('./evil');
21
+ const state = require('./state');
22
+ const { dedupe, effectiveRisk, VERDICT } = require('./contract');
23
+
24
+ // ── Phases within a round ────────────────────────────────────────────────────
25
+
26
+ const PHASE = {
27
+ GOD_BUILD: 'god_build',
28
+ EVIL_HUNT: 'evil_hunt',
29
+ EVIL_VERIFY: 'evil_verify',
30
+ ROUND_CLOSE: 'round_close',
31
+ REPORT: 'report',
32
+ };
33
+
34
+ // ── Starting a run ───────────────────────────────────────────────────────────
35
+
36
+ function startArena({ task, scope = '.', flags = [], limits = {} } = {}) {
37
+ const run = state.createRun({ task, limits });
38
+ run.scope = scope;
39
+ run.flags = flags;
40
+ run.phase = PHASE.GOD_BUILD;
41
+ run.pending = { artifacts: [], findings: [], addressedIds: [] };
42
+ state.save(run);
43
+ return run;
44
+ }
45
+
46
+ // ── The state machine ────────────────────────────────────────────────────────
47
+ // Given a run, what happens next? Returns an actionable step with the brief the
48
+ // AI should execute, plus a cost estimate the budget can veto.
49
+
50
+ function nextAction(run) {
51
+ if (!run) throw new Error('arena: no run');
52
+
53
+ // A finished run has nothing left but its report.
54
+ if (run.status !== 'running') {
55
+ return { action: PHASE.REPORT, reason: run.stopReason, run: state.summarize(run) };
56
+ }
57
+
58
+ const round = run.rounds.length + 1;
59
+ const carried = carriedFindings(run);
60
+
61
+ switch (run.phase) {
62
+ case PHASE.GOD_BUILD: {
63
+ const plan = god.planBuild({ task: run.task, findings: carried, round });
64
+ return {
65
+ action: PHASE.GOD_BUILD,
66
+ round,
67
+ stages: plan.stages,
68
+ // Round 1 builds; later rounds are fixing what EVIL proved.
69
+ intent: round === 1 ? 'build' : 'fix',
70
+ carriedFindings: carried.length,
71
+ estimatedTokens: plan.estimatedTokens,
72
+ };
73
+ }
74
+
75
+ case PHASE.EVIL_HUNT: {
76
+ const plan = evil.planSweep({
77
+ scope: run.scope,
78
+ flags: run.flags,
79
+ round,
80
+ knownFindingIds: run.seenFindingIds,
81
+ });
82
+ return {
83
+ action: PHASE.EVIL_HUNT,
84
+ round,
85
+ crew: plan.crew,
86
+ briefs: plan.briefs,
87
+ estimatedTokens: plan.estimatedTokens,
88
+ };
89
+ }
90
+
91
+ case PHASE.EVIL_VERIFY: {
92
+ const targets = evil.selectForVerification(run.pending.findings);
93
+ return {
94
+ action: PHASE.EVIL_VERIFY,
95
+ round,
96
+ targets: targets.map(f => ({ id: f.id, title: f.title, brief: evil.verifyBrief(f) })),
97
+ // Verification is cheap per finding but must stay bounded.
98
+ estimatedTokens: targets.length * 3000,
99
+ };
100
+ }
101
+
102
+ case PHASE.ROUND_CLOSE:
103
+ default:
104
+ return { action: PHASE.ROUND_CLOSE, round };
105
+ }
106
+ }
107
+
108
+ // Findings GOD still has to answer for: open, real, high-signal, worst first.
109
+ function carriedFindings(run) {
110
+ const last = run.rounds[run.rounds.length - 1];
111
+ if (!last) return [];
112
+ return evil.actionable(last.findings).slice(0, 10);
113
+ }
114
+
115
+ // ── Recording each phase ─────────────────────────────────────────────────────
116
+
117
+ function submitGodWork(run, { artifacts = [], addressedIds = [], tokensSpent = 0, elapsedMs = 0 } = {}) {
118
+ requirePhase(run, PHASE.GOD_BUILD);
119
+ run.pending.artifacts = artifacts;
120
+ run.pending.addressedIds = addressedIds;
121
+ run.spent.tokens += Math.max(0, Number(tokensSpent) || 0);
122
+ run.spent.ms += Math.max(0, Number(elapsedMs) || 0);
123
+ run.phase = PHASE.EVIL_HUNT;
124
+ state.save(run);
125
+ return run;
126
+ }
127
+
128
+ function submitEvilHunt(run, { findings = [], tokensSpent = 0, elapsedMs = 0 } = {}) {
129
+ requirePhase(run, PHASE.EVIL_HUNT);
130
+ run.pending.findings = dedupe(findings);
131
+ run.spent.tokens += Math.max(0, Number(tokensSpent) || 0);
132
+ run.spent.ms += Math.max(0, Number(elapsedMs) || 0);
133
+ run.phase = PHASE.EVIL_VERIFY;
134
+ state.save(run);
135
+ return run;
136
+ }
137
+
138
+ function submitVerdicts(run, { verdicts = {}, tokensSpent = 0, elapsedMs = 0 } = {}) {
139
+ requirePhase(run, PHASE.EVIL_VERIFY);
140
+ run.pending.findings = evil.applyVerdicts(run.pending.findings, verdicts);
141
+ run.spent.tokens += Math.max(0, Number(tokensSpent) || 0);
142
+ run.spent.ms += Math.max(0, Number(elapsedMs) || 0);
143
+ run.phase = PHASE.ROUND_CLOSE;
144
+ state.save(run);
145
+ return run;
146
+ }
147
+
148
+ // Close the round: grade GOD's work, record the verdict, decide whether to
149
+ // run another round. state.recordRound() owns convergence + hard stops.
150
+ function closeRound(run) {
151
+ requirePhase(run, PHASE.ROUND_CLOSE);
152
+
153
+ const completion = god.roundComplete({
154
+ artifacts: run.pending.artifacts,
155
+ findings: run.pending.findings,
156
+ addressedIds: run.pending.addressedIds,
157
+ });
158
+
159
+ const verdict = state.recordRound(run, {
160
+ findings: run.pending.findings,
161
+ artifacts: run.pending.artifacts,
162
+ tokensSpent: 0, // already accrued per-phase; do not double count
163
+ elapsedMs: 0,
164
+ });
165
+
166
+ // Attach whether GOD actually finished its side of the round.
167
+ verdict.godComplete = completion.complete;
168
+ verdict.unverifiedArtifacts = completion.unverifiedArtifacts;
169
+ verdict.outstandingFindings = completion.outstandingFindings;
170
+ run.rounds[run.rounds.length - 1] = verdict;
171
+
172
+ run.pending = { artifacts: [], findings: [], addressedIds: [] };
173
+ run.phase = run.status === 'running' ? PHASE.GOD_BUILD : PHASE.REPORT;
174
+ state.save(run);
175
+ return verdict;
176
+ }
177
+
178
+ function requirePhase(run, expected) {
179
+ if (run.phase !== expected) {
180
+ throw new Error(`arena: expected phase "${expected}" but run is in "${run.phase}"`);
181
+ }
182
+ }
183
+
184
+ // ── Budget guard ─────────────────────────────────────────────────────────────
185
+ // Called before dispatching an action. The arena aborts cleanly rather than
186
+ // overspending — a partial result you can read beats a surprise bill.
187
+
188
+ function affordOrAbort(run, action) {
189
+ const est = action.estimatedTokens || 0;
190
+ const check = state.canAffordRound(run, est);
191
+ if (check.ok) return { ok: true, ...check };
192
+ run.status = 'aborted';
193
+ run.stopReason = `budget guard: next step needs ~${est.toLocaleString()} tokens, only ${check.remaining.toLocaleString()} left`;
194
+ run.phase = PHASE.REPORT;
195
+ state.save(run);
196
+ return { ok: false, ...check };
197
+ }
198
+
199
+ // ── Report ───────────────────────────────────────────────────────────────────
200
+
201
+ function buildReport(run) {
202
+ const s = state.summarize(run);
203
+ const lines = [];
204
+
205
+ lines.push(`# Arena report — ${s.task}`, '');
206
+ lines.push(`- **Run:** \`${s.runId}\``);
207
+ lines.push(`- **Status:** ${s.status}${s.stopReason ? ` — ${s.stopReason}` : ''}`);
208
+ lines.push(`- **Rounds:** ${s.rounds}`);
209
+ lines.push(`- **Tokens:** ${s.tokensSpent.toLocaleString()}`);
210
+ lines.push('');
211
+
212
+ // The curve that matters: new findings per round should fall to zero.
213
+ lines.push('## Did the attacker give up?', '');
214
+ if (s.trend.length) {
215
+ const max = Math.max(...s.trend, 1);
216
+ for (let i = 0; i < s.trend.length; i++) {
217
+ const n = s.trend[i];
218
+ const bar = '█'.repeat(Math.round((n / max) * 24)) || '·';
219
+ lines.push(`round ${i + 1} ${String(n).padStart(3)} new ${bar}`);
220
+ }
221
+ lines.push('');
222
+ lines.push(s.status === 'converged'
223
+ ? '**Converged** — two consecutive rounds surfaced nothing new.'
224
+ : `**Not converged** (${s.stopReason || 'still running'}). Remaining risk is unproven, not absent.`);
225
+ } else {
226
+ lines.push('_No rounds completed._');
227
+ }
228
+ lines.push('');
229
+
230
+ // Per-round detail
231
+ lines.push('## Rounds', '');
232
+ for (const r of run.rounds) {
233
+ const verified = (r.artifacts || []).filter(a => a.verified).length;
234
+ lines.push(`### Round ${r.round}`);
235
+ lines.push(`- GOD: ${(r.artifacts || []).length} artifact(s), ${verified} verified` +
236
+ (r.godComplete === false ? ' — **round incomplete**' : ''));
237
+ if (r.unverifiedArtifacts?.length) {
238
+ lines.push(` - unverified: ${r.unverifiedArtifacts.join(', ')}`);
239
+ }
240
+ if (r.outstandingFindings?.length) {
241
+ lines.push(` - not addressed: ${r.outstandingFindings.join(', ')}`);
242
+ }
243
+ lines.push(`- EVIL: ${r.counts.total} finding(s) — ${r.counts.new} new, ${r.counts.confirmed} confirmed, ${r.counts.refuted} refuted`);
244
+ lines.push(`- Open risk after round: ${r.openRisk}`);
245
+ lines.push('');
246
+ }
247
+
248
+ // What is still standing
249
+ const last = run.rounds[run.rounds.length - 1];
250
+ const open = last ? evil.actionable(last.findings, { minRisk: 0 }) : [];
251
+ lines.push('## Still open', '');
252
+ if (!open.length) {
253
+ lines.push('_Nothing open. Every finding was either fixed or refuted._');
254
+ } else {
255
+ for (const f of open) {
256
+ lines.push(`- **[${effectiveRisk(f)}] ${f.title}** — ${f.severity}/${f.confidence}` +
257
+ (f.file ? ` · \`${f.file}:${f.line ?? '?'}\`` : ''));
258
+ if (f.repro) lines.push(` - repro: ${String(f.repro).split('\n')[0].slice(0, 160)}`);
259
+ if (f.fix) lines.push(` - fix: ${String(f.fix).split('\n')[0].slice(0, 160)}`);
260
+ }
261
+ }
262
+ lines.push('');
263
+
264
+ // Refuted — kept visible so the same false positive isn't re-litigated.
265
+ const refuted = last ? last.findings.filter(f => f.verdict === VERDICT.REFUTED) : [];
266
+ if (refuted.length) {
267
+ lines.push('## Refuted (checked, not real)', '');
268
+ for (const f of refuted) lines.push(`- ~~${f.title}~~`);
269
+ lines.push('');
270
+ }
271
+
272
+ return lines.join('\n');
273
+ }
274
+
275
+ module.exports = {
276
+ PHASE,
277
+ startArena, nextAction, carriedFindings,
278
+ submitGodWork, submitEvilHunt, submitVerdicts, closeRound,
279
+ affordOrAbort, buildReport,
280
+ };