kodelyth-ecc 2.5.4 → 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 +115 -0
- package/VERSION +1 -1
- package/bin/kodelyth-ecc.js +137 -0
- package/commands/arena.md +101 -0
- package/commands/evil-mode.md +106 -0
- package/commands/god-mode.md +85 -0
- package/package.json +1 -1
- package/scripts/arena/arena.js +280 -0
- package/scripts/arena/contract.js +169 -0
- package/scripts/arena/evil.js +175 -0
- package/scripts/arena/god.js +208 -0
- package/scripts/arena/state.js +195 -0
|
@@ -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
|
+
};
|