aiki-cli 0.2.2 → 0.3.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 +98 -7
- package/README.md +109 -30
- package/dist/bench/arms.js +10 -9
- package/dist/bench/harness.js +13 -10
- package/dist/bench/idea-lane-rotation.js +237 -0
- package/dist/bench/idea-v3-bench.js +506 -0
- package/dist/bench/idea-v3-rating.js +582 -0
- package/dist/bench/results.js +10 -3
- package/dist/bench/scoring/decision-insights.js +112 -0
- package/dist/bench/scoring/seeded-bugs.js +4 -0
- package/dist/cli/bench.js +180 -3
- package/dist/cli/doctor.js +56 -24
- package/dist/cli/index.js +31 -6
- package/dist/cli/resolve.js +7 -6
- package/dist/cli/resume.js +18 -0
- package/dist/cli/run.js +63 -8
- package/dist/council/view.js +378 -109
- package/dist/orchestration/calculations.js +97 -0
- package/dist/orchestration/context.js +37 -9
- package/dist/orchestration/decision-dossier.js +262 -0
- package/dist/orchestration/decision-graph.js +256 -0
- package/dist/orchestration/engine.js +5 -2
- package/dist/orchestration/evidence-pack.js +46 -0
- package/dist/orchestration/idea-lanes.js +20 -0
- package/dist/orchestration/jsonStage.js +72 -0
- package/dist/orchestration/legacy-idea-adapter.js +102 -0
- package/dist/orchestration/modes.js +33 -0
- package/dist/orchestration/preflight.js +183 -0
- package/dist/orchestration/quick-analysis.js +81 -0
- package/dist/orchestration/stages/cr-ladder.js +80 -0
- package/dist/orchestration/stages/s10-render.js +562 -150
- package/dist/orchestration/stages/s4-analyze.js +12 -7
- package/dist/orchestration/stages/s5-drift.js +9 -9
- package/dist/orchestration/stages/s6-positions.js +10 -0
- package/dist/orchestration/stages/s7-decision-graph.js +76 -0
- package/dist/orchestration/stages/s8-verify.js +153 -46
- package/dist/orchestration/stages/s8b-rebuttal.js +208 -0
- package/dist/orchestration/stages/s9-judge.js +329 -108
- package/dist/orchestration/stages/s9b-plan.js +85 -75
- package/dist/providers/codex.js +2 -1
- package/dist/providers/spawn.js +5 -0
- package/dist/schemas/index.js +572 -13
- package/dist/skills/idea-refinement/analyst.md +18 -14
- package/dist/skills/idea-refinement/economics-delivery.md +7 -0
- package/dist/skills/idea-refinement/market-adoption.md +7 -0
- package/dist/storage/runs.js +11 -4
- package/dist/tui/app.js +37 -13
- package/dist/tui/format.js +4 -5
- package/dist/tui/smart-entry.js +17 -1
- package/dist/tui/timeline.js +2 -4
- package/dist/workflows/code-review.js +4 -2
- package/dist/workflows/idea-refinement.js +110 -46
- package/package.json +12 -4
- package/dist/orchestration/stages/s0-grill.js +0 -79
- package/dist/orchestration/stages/s1-intent.js +0 -25
- package/dist/orchestration/stages/s2-misread.js +0 -76
- package/dist/orchestration/stages/s3-prompts.js +0 -55
- package/dist/orchestration/stages/s6-claims.js +0 -56
- package/dist/orchestration/stages/s7-disagreement.js +0 -134
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { makeRunId, resolveRoles, RunCtx, setupProviders } from '../orchestration/context.js';
|
|
5
|
+
import { executeRun } from '../orchestration/engine.js';
|
|
6
|
+
import { DecisionGraph } from '../schemas/index.js';
|
|
7
|
+
import { RunWriter } from '../storage/runs.js';
|
|
8
|
+
import { runIdeaRefinement } from '../workflows/idea-refinement.js';
|
|
9
|
+
import { IDEA_MODE_PLANS } from '../orchestration/modes.js';
|
|
10
|
+
import { DecisionInsightAdjudication, IdeaV3CaseManifest, scoreDecisionInsights } from './scoring/decision-insights.js';
|
|
11
|
+
const ROTATIONS = [
|
|
12
|
+
{ rotation: 'agy-market', s4: ['agy', 'codex'] },
|
|
13
|
+
{ rotation: 'codex-market', s4: ['codex', 'agy'] },
|
|
14
|
+
];
|
|
15
|
+
export const LaneRotationObservation = z.object({
|
|
16
|
+
case_id: z.string().min(1),
|
|
17
|
+
rotation: z.enum(['agy-market', 'codex-market']),
|
|
18
|
+
run_id: z.string().min(1),
|
|
19
|
+
decision_critical_recall: z.number().min(0).max(1).nullable(),
|
|
20
|
+
evidence_precision: z.number().min(0).max(1).nullable(),
|
|
21
|
+
json_repair_rate: z.number().min(0).max(1),
|
|
22
|
+
latency_ms: z.number().nonnegative(),
|
|
23
|
+
unique_supported_contributions: z.object({
|
|
24
|
+
agy: z.number().int().nonnegative(),
|
|
25
|
+
codex: z.number().int().nonnegative(),
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
const LaneRotationResult = z.object({
|
|
29
|
+
at: z.string().min(1),
|
|
30
|
+
observations: z.array(LaneRotationObservation),
|
|
31
|
+
});
|
|
32
|
+
async function loadBuildCases(root) {
|
|
33
|
+
const dir = join(root, 'bench', 'sets', 'idea-refinement', 'build');
|
|
34
|
+
const names = (await readdir(dir, { withFileTypes: true }))
|
|
35
|
+
.filter((entry) => entry.isDirectory())
|
|
36
|
+
.map((entry) => entry.name)
|
|
37
|
+
.sort();
|
|
38
|
+
return Promise.all(names.map(async (name) => {
|
|
39
|
+
const manifest = IdeaV3CaseManifest.parse(JSON.parse(await readFile(join(dir, name, 'case.json'), 'utf8')));
|
|
40
|
+
return { id: manifest.id, input: await readFile(join(dir, name, manifest.input_file), 'utf8') };
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
// Only real `idea-lanes-YYYY-MM-DD.json` campaign files — an archived/renamed run is never resumed by accident.
|
|
44
|
+
const LANE_CAMPAIGN = /^idea-lanes-\d{4}-\d{2}-\d{2}\.json$/;
|
|
45
|
+
/** Latest dated campaign file under <root>/bench/results, or null when none exists. */
|
|
46
|
+
export async function findLatestLaneResults(root) {
|
|
47
|
+
const dir = join(root, 'bench', 'results');
|
|
48
|
+
try {
|
|
49
|
+
const latest = (await readdir(dir)).filter((name) => LANE_CAMPAIGN.test(name)).sort().pop();
|
|
50
|
+
return latest ? join(dir, latest) : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function loadPriorObservations(path) {
|
|
57
|
+
try {
|
|
58
|
+
return LaneRotationResult.parse(JSON.parse(await readFile(path, 'utf8'))).observations;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === 'ENOENT')
|
|
62
|
+
return [];
|
|
63
|
+
throw error; // a corrupt checkpoint fails loud — silently starting fresh would re-pay the whole matrix
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function resolveResume(root, opts) {
|
|
67
|
+
const path = opts.resultsPath ?? (opts.resume ? await findLatestLaneResults(root) : null);
|
|
68
|
+
return { path, prior: opts.resume && path ? await loadPriorObservations(path) : [] };
|
|
69
|
+
}
|
|
70
|
+
/** Narrow to one named build case (metered single-case runs); an unknown id fails loud, never runs 0 pairs. */
|
|
71
|
+
function filterCases(cases, caseId) {
|
|
72
|
+
if (!caseId)
|
|
73
|
+
return cases;
|
|
74
|
+
const picked = cases.filter((item) => item.id === caseId);
|
|
75
|
+
if (picked.length === 0)
|
|
76
|
+
throw new Error(`unknown case "${caseId}" — build cases: ${cases.map((item) => item.id).join(', ')}`);
|
|
77
|
+
return picked;
|
|
78
|
+
}
|
|
79
|
+
/** A rotation sample is valid only when both scout seats actually produced output. A `low_diversity` run
|
|
80
|
+
* (a seat died; the survivor was resampled) would silently poison the lane comparison if recorded. */
|
|
81
|
+
export function assertLaneRunDiversity(flags, label) {
|
|
82
|
+
if (flags?.includes('low_diversity')) {
|
|
83
|
+
throw new Error(`${label}: run completed low_diversity (a scout seat died) — not a valid rotation sample; re-run when both providers are healthy`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function planIdeaLaneBench(opts = {}) {
|
|
87
|
+
const root = opts.root ?? process.cwd();
|
|
88
|
+
const handles = opts.handles ?? await setupProviders();
|
|
89
|
+
const available = new Set(handles.map((handle) => handle.id));
|
|
90
|
+
const cases = filterCases(await loadBuildCases(root), opts.caseId);
|
|
91
|
+
const { path, prior } = await resolveResume(root, opts);
|
|
92
|
+
const done = new Set(prior.map((item) => `${item.case_id}:${item.rotation}`));
|
|
93
|
+
const runnable = ROTATIONS.filter(({ s4 }) => available.has('claude') && s4.every((provider) => available.has(provider)));
|
|
94
|
+
const runs = cases.flatMap((item) => runnable.map(({ rotation, s4 }) => ({ case: item.id, rotation, s4 })))
|
|
95
|
+
.filter((run) => !done.has(`${run.case}:${run.rotation}`));
|
|
96
|
+
return {
|
|
97
|
+
cases: cases.map((item) => item.id),
|
|
98
|
+
runs,
|
|
99
|
+
estimatedCalls: runs.length * IDEA_MODE_PLANS.council.maxCalls,
|
|
100
|
+
resumedFrom: prior.length ? path ?? undefined : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function executeLaneRun(target, handles, root) {
|
|
104
|
+
const ids = handles.map((handle) => handle.id);
|
|
105
|
+
const runId = makeRunId('idea-refinement');
|
|
106
|
+
const roles = resolveRoles('idea-refinement', ids, { judge: 'claude', s4: target.s4 });
|
|
107
|
+
const writer = new RunWriter(runId, join(root, '.aiki'));
|
|
108
|
+
// Bench runs are the deep path: hard cases legitimately run ~18 min and the 20-min default leaves no
|
|
109
|
+
// headroom (per-call timeout now bounds any single hang). 45 min so a healthy deep run is never
|
|
110
|
+
// killed by headroom; the per-call timeout is still the real safety bound.
|
|
111
|
+
const ctx = new RunCtx({ runId, workflow: 'idea-refinement', handles, roles, writer, cwd: writer.dir, deadlineMs: 45 * 60 * 1000 });
|
|
112
|
+
const started = Date.now();
|
|
113
|
+
const outcome = await executeRun(ctx, target.input, runIdeaRefinement);
|
|
114
|
+
if (!outcome.ok)
|
|
115
|
+
throw new Error(`${target.case_id}/${target.rotation} failed: ${outcome.error?.code}: ${outcome.error?.message}`);
|
|
116
|
+
const meta = JSON.parse(await readFile(join(outcome.dir, 'meta.json'), 'utf8'));
|
|
117
|
+
assertLaneRunDiversity(meta.flags, `${target.case_id}/${target.rotation}`);
|
|
118
|
+
const graph = DecisionGraph.parse(JSON.parse(await readFile(join(outcome.dir, '07-decision-graph.json'), 'utf8')));
|
|
119
|
+
const positions = new Map(graph.positions.map((position) => [position.id, position]));
|
|
120
|
+
const unique = { agy: 0, codex: 0 };
|
|
121
|
+
for (const claim of graph.claims.filter((item) => item.state === 'UNIQUE' && item.evidence_state === 'SUPPORTED')) {
|
|
122
|
+
const provider = positions.get(claim.position_ids[0])?.provider;
|
|
123
|
+
if (provider === 'agy' || provider === 'codex')
|
|
124
|
+
unique[provider]++;
|
|
125
|
+
}
|
|
126
|
+
return LaneRotationObservation.parse({
|
|
127
|
+
case_id: target.case_id,
|
|
128
|
+
rotation: target.rotation,
|
|
129
|
+
run_id: outcome.runId,
|
|
130
|
+
decision_critical_recall: null,
|
|
131
|
+
evidence_precision: null,
|
|
132
|
+
json_repair_rate: ctx.calls.length ? ctx.calls.filter((call) => call.stage.endsWith('-repair')).length / ctx.calls.length : 0,
|
|
133
|
+
latency_ms: Date.now() - started,
|
|
134
|
+
unique_supported_contributions: unique,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export async function runIdeaLaneBench(opts = {}) {
|
|
138
|
+
const root = opts.root ?? process.cwd();
|
|
139
|
+
const handles = opts.handles ?? await setupProviders();
|
|
140
|
+
const available = new Set(handles.map((handle) => handle.id));
|
|
141
|
+
const cases = filterCases(await loadBuildCases(root), opts.caseId);
|
|
142
|
+
const targets = cases.flatMap((item) => ROTATIONS
|
|
143
|
+
.filter(({ s4 }) => available.has('claude') && s4.every((provider) => available.has(provider)))
|
|
144
|
+
.map(({ rotation, s4 }) => ({ case_id: item.id, input: item.input, rotation, s4 })));
|
|
145
|
+
const resumed = await resolveResume(root, opts);
|
|
146
|
+
const path = resumed.path ?? join(root, 'bench', 'results', `idea-lanes-${new Date().toISOString().slice(0, 10)}.json`);
|
|
147
|
+
const done = new Set(resumed.prior.map((item) => `${item.case_id}:${item.rotation}`));
|
|
148
|
+
const execute = opts.execute ?? ((target) => executeLaneRun(target, handles, root));
|
|
149
|
+
const observations = [...resumed.prior];
|
|
150
|
+
await mkdir(dirname(path), { recursive: true });
|
|
151
|
+
for (const target of targets) {
|
|
152
|
+
if (done.has(`${target.case_id}:${target.rotation}`))
|
|
153
|
+
continue; // already paid for — keep, don't re-run
|
|
154
|
+
observations.push(LaneRotationObservation.parse(await execute(target)));
|
|
155
|
+
const result = LaneRotationResult.parse({ at: new Date().toISOString(), observations });
|
|
156
|
+
const tmp = `${path}.tmp`;
|
|
157
|
+
await writeFile(tmp, JSON.stringify(result, null, 2), 'utf8');
|
|
158
|
+
await rename(tmp, path);
|
|
159
|
+
}
|
|
160
|
+
return { path, observations };
|
|
161
|
+
}
|
|
162
|
+
/** One human-authored adjudication entry: which pair it scores + the frozen R0 scorer's exact input. */
|
|
163
|
+
const LaneAdjudicationEntry = z
|
|
164
|
+
.object({
|
|
165
|
+
case_id: z.string().min(1),
|
|
166
|
+
rotation: z.enum(['agy-market', 'codex-market']),
|
|
167
|
+
adjudication: DecisionInsightAdjudication,
|
|
168
|
+
})
|
|
169
|
+
.strict();
|
|
170
|
+
const LaneAdjudicationFile = z.array(LaneAdjudicationEntry).min(1);
|
|
171
|
+
/** Import blind adjudications into a campaign file: each entry flows through the frozen R0 scorer and
|
|
172
|
+
* fills that pair's null recall/precision. One pass by design — an already-scored pair is refused, and
|
|
173
|
+
* an unknown pair fails loud rather than silently dropping a rater's work. */
|
|
174
|
+
export async function importLaneAdjudications(opts) {
|
|
175
|
+
const root = opts.root ?? process.cwd();
|
|
176
|
+
const path = opts.resultsPath ?? await findLatestLaneResults(root);
|
|
177
|
+
if (!path)
|
|
178
|
+
throw new Error('no idea-lanes campaign file under bench/results/ — run the bench first');
|
|
179
|
+
const prior = LaneRotationResult.parse(JSON.parse(await readFile(path, 'utf8')));
|
|
180
|
+
const entries = LaneAdjudicationFile.parse(JSON.parse(await readFile(opts.importPath, 'utf8')));
|
|
181
|
+
const byPair = new Map(prior.observations.map((item) => [`${item.case_id}:${item.rotation}`, item]));
|
|
182
|
+
const scored = [];
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
const key = `${entry.case_id}:${entry.rotation}`;
|
|
185
|
+
const observation = byPair.get(key);
|
|
186
|
+
if (!observation)
|
|
187
|
+
throw new Error(`no observation for ${key} in ${path} — run that pair first`);
|
|
188
|
+
if (observation.decision_critical_recall !== null || observation.evidence_precision !== null) {
|
|
189
|
+
throw new Error(`${key} is already scored — blind adjudication is one pass (BENCHMARK-IDEA-V3.md)`);
|
|
190
|
+
}
|
|
191
|
+
const updated = scoreLaneObservation(observation, entry.adjudication);
|
|
192
|
+
byPair.set(key, updated);
|
|
193
|
+
scored.push(updated);
|
|
194
|
+
}
|
|
195
|
+
const observations = prior.observations.map((item) => byPair.get(`${item.case_id}:${item.rotation}`));
|
|
196
|
+
const result = LaneRotationResult.parse({ at: new Date().toISOString(), observations });
|
|
197
|
+
const tmp = `${path}.tmp`;
|
|
198
|
+
await writeFile(tmp, JSON.stringify(result, null, 2), 'utf8');
|
|
199
|
+
await rename(tmp, path);
|
|
200
|
+
return { path, scored, observations };
|
|
201
|
+
}
|
|
202
|
+
/** Pick only from fully adjudicated build observations; exact ties leave the default unfrozen. */
|
|
203
|
+
export function chooseLaneDefault(input) {
|
|
204
|
+
const observations = z.array(LaneRotationObservation).parse(input);
|
|
205
|
+
if (observations.length === 0 || observations.some((item) => item.decision_critical_recall === null || item.evidence_precision === null))
|
|
206
|
+
return null;
|
|
207
|
+
const cases = new Set(observations.map((item) => item.case_id));
|
|
208
|
+
const pairs = new Set(observations.map((item) => `${item.case_id}:${item.rotation}`));
|
|
209
|
+
if (pairs.size !== cases.size * ROTATIONS.length || observations.length !== pairs.size)
|
|
210
|
+
return null;
|
|
211
|
+
const average = (items, key) => items.reduce((sum, item) => sum + (item[key] ?? 0), 0) / items.length;
|
|
212
|
+
const scores = ROTATIONS.map(({ rotation }) => {
|
|
213
|
+
const items = observations.filter((item) => item.rotation === rotation);
|
|
214
|
+
return {
|
|
215
|
+
rotation,
|
|
216
|
+
recall: average(items, 'decision_critical_recall'),
|
|
217
|
+
evidence: average(items, 'evidence_precision'),
|
|
218
|
+
unique: items.reduce((sum, item) => sum + item.unique_supported_contributions.agy + item.unique_supported_contributions.codex, 0) / items.length,
|
|
219
|
+
repairs: average(items, 'json_repair_rate'),
|
|
220
|
+
latency: average(items, 'latency_ms'),
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
scores.sort((a, b) => b.recall - a.recall || b.evidence - a.evidence || b.unique - a.unique || a.repairs - b.repairs || a.latency - b.latency);
|
|
224
|
+
const [best, other] = scores;
|
|
225
|
+
if (!best || !other)
|
|
226
|
+
return null;
|
|
227
|
+
return best.recall === other.recall && best.evidence === other.evidence && best.unique === other.unique
|
|
228
|
+
&& best.repairs === other.repairs && best.latency === other.latency ? null : best.rotation;
|
|
229
|
+
}
|
|
230
|
+
export function scoreLaneObservation(observation, adjudication) {
|
|
231
|
+
const score = scoreDecisionInsights(adjudication);
|
|
232
|
+
return LaneRotationObservation.parse({
|
|
233
|
+
...observation,
|
|
234
|
+
decision_critical_recall: score.recall,
|
|
235
|
+
evidence_precision: score.precision,
|
|
236
|
+
});
|
|
237
|
+
}
|