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,112 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const DecisionStance = z.enum(['SUPPORT', 'OPPOSE', 'QUALIFY', 'UNRESOLVED']);
|
|
3
|
+
export const FactKind = z.enum(['CURRENT_FACT', 'DURABLE_FACT', 'INFERENCE']);
|
|
4
|
+
export const EvidenceStatus = z.enum(['SUPPORTED', 'UNSUPPORTED', 'NOT_REQUIRED']);
|
|
5
|
+
export const ExpectedDecisionClaim = z.object({
|
|
6
|
+
id: z.string().min(1),
|
|
7
|
+
proposition: z.string().min(1),
|
|
8
|
+
acceptable_stances: z.array(DecisionStance).min(1),
|
|
9
|
+
evidence_required: z.boolean(),
|
|
10
|
+
});
|
|
11
|
+
export const DecisionInsightAdjudication = z.object({
|
|
12
|
+
expected_claims: z.array(ExpectedDecisionClaim),
|
|
13
|
+
report_claims: z.array(z.object({
|
|
14
|
+
id: z.string().min(1),
|
|
15
|
+
stance: DecisionStance,
|
|
16
|
+
fact_kind: FactKind,
|
|
17
|
+
correct: z.boolean(),
|
|
18
|
+
relevant: z.boolean(),
|
|
19
|
+
evidence_status: EvidenceStatus,
|
|
20
|
+
})),
|
|
21
|
+
matches: z.array(z.object({
|
|
22
|
+
expected_claim_id: z.string().min(1),
|
|
23
|
+
report_claim_id: z.string().min(1),
|
|
24
|
+
})),
|
|
25
|
+
}).superRefine((input, ctx) => {
|
|
26
|
+
const expectedIds = new Set(input.expected_claims.map((claim) => claim.id));
|
|
27
|
+
const reportIds = new Set(input.report_claims.map((claim) => claim.id));
|
|
28
|
+
if (expectedIds.size !== input.expected_claims.length || reportIds.size !== input.report_claims.length) {
|
|
29
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'expert and report claim ids must be unique' });
|
|
30
|
+
}
|
|
31
|
+
const expected = new Set();
|
|
32
|
+
const reported = new Set();
|
|
33
|
+
for (const match of input.matches) {
|
|
34
|
+
if (!expectedIds.has(match.expected_claim_id) || !reportIds.has(match.report_claim_id)) {
|
|
35
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'claim matches must reference declared claim ids' });
|
|
36
|
+
}
|
|
37
|
+
if (expected.has(match.expected_claim_id) || reported.has(match.report_claim_id)) {
|
|
38
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'claim matches must be one-to-one' });
|
|
39
|
+
}
|
|
40
|
+
expected.add(match.expected_claim_id);
|
|
41
|
+
reported.add(match.report_claim_id);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
export const IdeaV3CaseManifest = z.object({
|
|
45
|
+
id: z.string().min(1),
|
|
46
|
+
title: z.string().min(1),
|
|
47
|
+
set: z.enum(['build', 'holdout']),
|
|
48
|
+
provenance: z.enum(['INSPECTED_BUILD', 'AUTHORED_BUILD', 'SEALED_HOLDOUT']),
|
|
49
|
+
tags: z.array(z.string().min(1)).min(1),
|
|
50
|
+
input_file: z.string().min(1),
|
|
51
|
+
critical_claims: z.array(ExpectedDecisionClaim).min(1),
|
|
52
|
+
common_false_claims: z.array(z.object({
|
|
53
|
+
id: z.string().min(1),
|
|
54
|
+
claim: z.string().min(1),
|
|
55
|
+
reason: z.string().min(1),
|
|
56
|
+
})),
|
|
57
|
+
required_dimensions: z.array(z.string().min(1)).length(12),
|
|
58
|
+
source_pack: z.array(z.object({
|
|
59
|
+
id: z.string().min(1),
|
|
60
|
+
title: z.string().min(1),
|
|
61
|
+
as_of: z.string().min(1),
|
|
62
|
+
url: z.string().url().optional(),
|
|
63
|
+
local_file: z.string().min(1).optional(),
|
|
64
|
+
})),
|
|
65
|
+
acceptable_unresolved_outcomes: z.array(z.object({
|
|
66
|
+
claim_id: z.string().min(1),
|
|
67
|
+
reason: z.string().min(1),
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
/** Score one blinded, human-adjudicated idea report. */
|
|
71
|
+
export function scoreDecisionInsights(input) {
|
|
72
|
+
input = DecisionInsightAdjudication.parse(input);
|
|
73
|
+
const reportById = new Map(input.report_claims.map((claim) => [claim.id, claim]));
|
|
74
|
+
const isTruePositive = (claim) => claim.correct && claim.relevant && (claim.fact_kind !== 'CURRENT_FACT' || claim.evidence_status === 'SUPPORTED');
|
|
75
|
+
const truePositiveReports = input.report_claims.filter(isTruePositive).length;
|
|
76
|
+
const matched = input.matches.filter((match) => {
|
|
77
|
+
const expected = input.expected_claims.find((claim) => claim.id === match.expected_claim_id);
|
|
78
|
+
const report = reportById.get(match.report_claim_id);
|
|
79
|
+
return expected && report && isTruePositive(report) && expected.acceptable_stances.includes(report.stance)
|
|
80
|
+
&& (!expected.evidence_required || report.evidence_status === 'SUPPORTED');
|
|
81
|
+
}).length;
|
|
82
|
+
const recall = input.expected_claims.length ? matched / input.expected_claims.length : 0;
|
|
83
|
+
const precision = input.report_claims.length ? truePositiveReports / input.report_claims.length : 0;
|
|
84
|
+
return {
|
|
85
|
+
expected: input.expected_claims.length,
|
|
86
|
+
matched,
|
|
87
|
+
reported: input.report_claims.length,
|
|
88
|
+
true_positive_reports: truePositiveReports,
|
|
89
|
+
recall,
|
|
90
|
+
precision,
|
|
91
|
+
f1: recall + precision ? (2 * recall * precision) / (recall + precision) : 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Micro-average case scores by summing claim counts before calculating rates. */
|
|
95
|
+
export function summarizeDecisionInsights(scores) {
|
|
96
|
+
const sum = (key) => scores.reduce((total, score) => total + score[key], 0);
|
|
97
|
+
const expected = sum('expected');
|
|
98
|
+
const matched = sum('matched');
|
|
99
|
+
const reported = sum('reported');
|
|
100
|
+
const truePositiveReports = sum('true_positive_reports');
|
|
101
|
+
const recall = expected ? matched / expected : 0;
|
|
102
|
+
const precision = reported ? truePositiveReports / reported : 0;
|
|
103
|
+
return {
|
|
104
|
+
expected,
|
|
105
|
+
matched,
|
|
106
|
+
reported,
|
|
107
|
+
true_positive_reports: truePositiveReports,
|
|
108
|
+
recall,
|
|
109
|
+
precision,
|
|
110
|
+
f1: recall + precision ? (2 * recall * precision) / (recall + precision) : 0,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -19,11 +19,15 @@ export const BugManifest = z.object({ bugs: z.array(SeededBug) });
|
|
|
19
19
|
/** Score one arm's findings against a case's seeded bugs (BENCHMARK.md §3). Pure. */
|
|
20
20
|
export function scoreRun(findings, bugs) {
|
|
21
21
|
const matchedBugIds = bugs.filter((bug) => findings.some((f) => sameFinding(f, bug))).map((b) => b.id);
|
|
22
|
+
const sameLocation = (finding, bug) => finding.file === bug.file && finding.line_start <= bug.line_end && bug.line_start <= finding.line_end;
|
|
23
|
+
const matchedRelaxed = bugs.filter((bug) => findings.some((finding) => sameLocation(finding, bug))).length;
|
|
22
24
|
const unmatched = findings.filter((f) => !bugs.some((bug) => sameFinding(f, bug))).length;
|
|
23
25
|
return {
|
|
24
26
|
seeded: bugs.length,
|
|
25
27
|
matched: matchedBugIds.length,
|
|
26
28
|
recall: bugs.length === 0 ? 0 : matchedBugIds.length / bugs.length,
|
|
29
|
+
matchedRelaxed,
|
|
30
|
+
recallRelaxed: bugs.length === 0 ? 0 : matchedRelaxed / bugs.length,
|
|
27
31
|
reported: findings.length,
|
|
28
32
|
unmatched,
|
|
29
33
|
matchedBugIds,
|
package/dist/cli/bench.js
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
// quota). --resume continues the latest results file, keeping already-scored case×arm pairs.
|
|
5
5
|
import { planBench, renderTable, runBench } from '../bench/harness.js';
|
|
6
6
|
import { setupProviders } from '../orchestration/context.js';
|
|
7
|
-
|
|
7
|
+
import { chooseLaneDefault, importLaneAdjudications, planIdeaLaneBench, runIdeaLaneBench } from '../bench/idea-lane-rotation.js';
|
|
8
|
+
import { IDEA_V3_ARM_IDS, planIdeaV3Bench, runIdeaV3Bench } from '../bench/idea-v3-bench.js';
|
|
9
|
+
import { exportIdeaV3BlindBundle, importIdeaV3Ratings, writeFrozenIdeaV3Protocol, writeIdeaV3Results } from '../bench/idea-v3-rating.js';
|
|
10
|
+
import { DISPLAY_NAME } from '../providers/types.js';
|
|
11
|
+
const VALID_ARMS = ['A', 'B', 'C', 'D', 'E', 'L'];
|
|
8
12
|
/** One-block pre-run summary: what will run + the ≈Opus cost, so the user commits knowingly (§19). */
|
|
9
13
|
function renderPlan(plan) {
|
|
10
14
|
const L = [`bench ${plan.suite} — set ${plan.set} — ${plan.cases.length} case(s) × arms ${plan.arms.join(',')}`];
|
|
@@ -15,8 +19,181 @@ function renderPlan(plan) {
|
|
|
15
19
|
return L.join('\n');
|
|
16
20
|
}
|
|
17
21
|
export async function benchCommand(workflow, opts = {}) {
|
|
22
|
+
if (workflow === 'idea-v3') {
|
|
23
|
+
if (opts.freezeProtocol) {
|
|
24
|
+
try {
|
|
25
|
+
const result = await writeFrozenIdeaV3Protocol({ draftPath: opts.freezeProtocol });
|
|
26
|
+
process.stdout.write(`\nprotocol frozen: ${result.path}\n`);
|
|
27
|
+
process.stdout.write('Commit this file before opening holdout. No provider calls were made.\n\n');
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (opts.publishResults) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await writeIdeaV3Results({ scoredPath: opts.publishResults });
|
|
38
|
+
process.stdout.write(`\nresults: ${result.path}\n`);
|
|
39
|
+
process.stdout.write(`frozen ship gate: ${result.gates.ship ? 'PASS' : 'FAIL'}\n\n`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (opts.importRatings) {
|
|
48
|
+
try {
|
|
49
|
+
const result = await importIdeaV3Ratings({ campaignPath: opts.campaign, resolutionPath: opts.importRatings });
|
|
50
|
+
process.stdout.write(`\nscored campaign: ${result.path}\n`);
|
|
51
|
+
for (const item of result.scored.summary) {
|
|
52
|
+
process.stdout.write(`${item.arm}: recall ${item.score.recall.toFixed(3)} · precision ${item.score.precision.toFixed(3)} · F1 ${item.score.f1.toFixed(3)}\n`);
|
|
53
|
+
}
|
|
54
|
+
process.stdout.write('Raw locked ratings and hashes were retained. No provider calls were made.\n\n');
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (opts.exportBlind) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await exportIdeaV3BlindBundle({ campaignPath: opts.campaign, outDir: opts.exportBlind });
|
|
65
|
+
process.stdout.write(`\nblinded rating packets: ${result.outDir}\n`);
|
|
66
|
+
process.stdout.write(`private mapping (do not give to raters): ${result.mappingPath}\n`);
|
|
67
|
+
process.stdout.write('Give each rater only their own rater-* directory. No provider calls were made.\n\n');
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (opts.set && opts.set !== 'build' && opts.set !== 'holdout') {
|
|
76
|
+
process.stderr.write(`idea-v3 set must be build or holdout (got "${opts.set}")\n`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const set = (opts.set ?? 'build');
|
|
80
|
+
const requested = opts.arms
|
|
81
|
+
? opts.arms.split(',').map((value) => value.trim().toUpperCase()).filter(Boolean)
|
|
82
|
+
: set === 'build' ? ['B', 'C', 'D2', 'R'] : ['B', 'C', 'R'];
|
|
83
|
+
const invalid = requested.filter((arm) => !IDEA_V3_ARM_IDS.includes(arm));
|
|
84
|
+
if (invalid.length) {
|
|
85
|
+
process.stderr.write(`invalid idea-v3 arm(s): ${invalid.join(', ')}. Valid: B,C,D2,R\n`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
const arms = requested;
|
|
89
|
+
const provider = opts.baselineProvider ?? 'claude';
|
|
90
|
+
if (!['claude', 'codex', 'agy'].includes(provider)) {
|
|
91
|
+
process.stderr.write(`invalid baseline provider "${provider}". Valid: claude,codex,agy\n`);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
let plan;
|
|
95
|
+
try {
|
|
96
|
+
plan = await planIdeaV3Bench({ set, arms, resume: opts.resume, baselineProvider: provider });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write(`\nidea-v3 protocol comparison — ${set} — ${plan.cases.length} case(s) × arms ${arms.join(',')}\n`);
|
|
103
|
+
process.stdout.write(`B/C baseline provider: ${DISPLAY_NAME[provider]}\n`);
|
|
104
|
+
if (plan.resumedFrom)
|
|
105
|
+
process.stdout.write(`resume: continuing ${plan.resumedFrom} — ${plan.skipCompleted} recorded pair(s) kept\n`);
|
|
106
|
+
process.stdout.write(`to run: ${plan.toRun.length} case×arm pair(s) → ≤${plan.estimatedProviderCalls} nominal provider call(s)\n`);
|
|
107
|
+
if (arms.includes('D2'))
|
|
108
|
+
process.stdout.write('D2 source: archived R0 runner at commit 680fba3 (supply --d2-import when executing)\n');
|
|
109
|
+
if (!plan.toRun.length) {
|
|
110
|
+
process.stdout.write('\nnothing to run — every requested pair is already recorded.\n\n');
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
if (!opts.yes) {
|
|
114
|
+
process.stdout.write('\nRe-run with --yes to execute. Paid calls are never started by this dry-run.\n\n');
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const result = await runIdeaV3Bench({
|
|
119
|
+
set,
|
|
120
|
+
arms,
|
|
121
|
+
resume: opts.resume,
|
|
122
|
+
baselineProvider: provider,
|
|
123
|
+
d2ImportPath: opts.d2Import,
|
|
124
|
+
});
|
|
125
|
+
process.stdout.write(`\nresults: ${result.path}\n`);
|
|
126
|
+
process.stdout.write('Reports remain unscored until the frozen blinded-rating import step.\n\n');
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (workflow === 'idea-refinement') {
|
|
135
|
+
if (opts.set && opts.set !== 'build') {
|
|
136
|
+
process.stderr.write('idea lane rotation is build-set-only; holdout remains sealed\n');
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
// --import is offline: blind adjudications → frozen R0 scorer → filled campaign metrics. No provider calls.
|
|
140
|
+
if (opts.import) {
|
|
141
|
+
try {
|
|
142
|
+
const { path, scored, observations } = await importLaneAdjudications({ importPath: opts.import });
|
|
143
|
+
for (const o of scored) {
|
|
144
|
+
process.stdout.write(`scored ${o.case_id}/${o.rotation} — recall ${o.decision_critical_recall} · evidence precision ${o.evidence_precision}\n`);
|
|
145
|
+
}
|
|
146
|
+
const pending = observations.filter((o) => o.decision_critical_recall === null || o.evidence_precision === null).length;
|
|
147
|
+
const selected = pending === 0 ? chooseLaneDefault(observations) : null;
|
|
148
|
+
process.stdout.write(`\nresults: ${path}\n`);
|
|
149
|
+
process.stdout.write(selected
|
|
150
|
+
? `default lane assignment: ${selected}\n\n`
|
|
151
|
+
: pending > 0
|
|
152
|
+
? `${pending} pair(s) still unscored — the lane default stays provisional until every pair is adjudicated.\n\n`
|
|
153
|
+
: 'all pairs scored but no winner (incomplete matrix or exact tie) — default stays provisional.\n\n');
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const handles = await setupProviders();
|
|
162
|
+
let plan;
|
|
163
|
+
try {
|
|
164
|
+
plan = await planIdeaLaneBench({ handles, resume: opts.resume, caseId: opts.case });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
if (plan.cases.length === 0) {
|
|
171
|
+
process.stderr.write('no cases found in bench/sets/idea-refinement/build/\n');
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
process.stdout.write(`\nidea lane rotation — ${plan.cases.length} case(s) × 2 assignments\n`);
|
|
175
|
+
if (plan.resumedFrom)
|
|
176
|
+
process.stdout.write(`resume: continuing ${plan.resumedFrom} — completed pairs are kept, not re-run\n`);
|
|
177
|
+
process.stdout.write(`to run: ${plan.runs.length} council run(s) → ≈${plan.estimatedCalls} provider call(s)\n`);
|
|
178
|
+
if (plan.runs.length === 0) {
|
|
179
|
+
process.stdout.write('\nnothing to run — every case×rotation pair is already recorded.\n\n');
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
if (!opts.yes) {
|
|
183
|
+
const resumeHint = opts.resume ? '' : ' (add --resume to continue a partial run across quota windows)';
|
|
184
|
+
process.stdout.write(`\nRe-run with --yes to execute${resumeHint}. Paid calls are never started by this dry-run.\n\n`);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
const result = await runIdeaLaneBench({ handles, resume: opts.resume, caseId: opts.case });
|
|
188
|
+
const selected = chooseLaneDefault(result.observations);
|
|
189
|
+
process.stdout.write(`\nresults: ${result.path}\n`);
|
|
190
|
+
process.stdout.write(selected
|
|
191
|
+
? `default lane assignment: ${selected}\n\n`
|
|
192
|
+
: 'default remains provisional — blind-score recall/evidence precision before selection.\n\n');
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
18
195
|
if (workflow !== 'code-review') {
|
|
19
|
-
process.stderr.write(`bench supports
|
|
196
|
+
process.stderr.write(`bench supports "code-review", "idea-refinement", or "idea-v3" (got "${workflow}")\n`);
|
|
20
197
|
return 1;
|
|
21
198
|
}
|
|
22
199
|
const arms = (opts.arms ?? 'A,B,C,D')
|
|
@@ -24,7 +201,7 @@ export async function benchCommand(workflow, opts = {}) {
|
|
|
24
201
|
.map((s) => s.trim().toUpperCase())
|
|
25
202
|
.filter((a) => VALID_ARMS.includes(a));
|
|
26
203
|
if (arms.length === 0) {
|
|
27
|
-
process.stderr.write(`no valid arms in "${opts.arms}". Valid: A,B,C,D\n`);
|
|
204
|
+
process.stderr.write(`no valid arms in "${opts.arms}". Valid: A,B,C,D,E,L\n`);
|
|
28
205
|
return 1;
|
|
29
206
|
}
|
|
30
207
|
const set = opts.set ?? 'build';
|
package/dist/cli/doctor.js
CHANGED
|
@@ -18,19 +18,18 @@ const READONLY_LABEL = {
|
|
|
18
18
|
};
|
|
19
19
|
const pad = (s, w) => s.padEnd(w);
|
|
20
20
|
/**
|
|
21
|
-
* `aiki doctor`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* A cache entry is reused only when its provider version still matches (an upgrade re-smokes).
|
|
21
|
+
* The shared check engine behind `aiki doctor` and the TUI startup preflight: detection + flag
|
|
22
|
+
* probe + (optional) smoke test per provider. `onRow` fires as each provider finishes, so a UI
|
|
23
|
+
* can show live progress. Smoke results are cached 6h in `.aiki/smoke-cache.json` (§242);
|
|
24
|
+
* `fresh` bypasses the cache. A cache entry is reused only when its provider version still
|
|
25
|
+
* matches (an upgrade re-smokes).
|
|
27
26
|
*/
|
|
28
|
-
export async function
|
|
27
|
+
export async function runDoctorChecks(opts = {}) {
|
|
29
28
|
const runSmoke = opts.smoke !== false;
|
|
30
29
|
const cache = runSmoke ? await readSmokeCache() : {};
|
|
31
30
|
const now = Date.now();
|
|
32
31
|
const updates = {};
|
|
33
|
-
const
|
|
32
|
+
const check = async (id) => {
|
|
34
33
|
const det = await detect(id);
|
|
35
34
|
if (det.status !== 'READY')
|
|
36
35
|
return { det };
|
|
@@ -44,23 +43,35 @@ export async function doctor(opts = {}) {
|
|
|
44
43
|
const smoke = await smokeTest(id, flags);
|
|
45
44
|
updates[id] = toEntry(smoke, det.version ?? null, new Date(now));
|
|
46
45
|
return { det, flags, smoke };
|
|
46
|
+
};
|
|
47
|
+
const rows = await Promise.all(PROVIDER_IDS.map(async (id) => {
|
|
48
|
+
const row = await check(id);
|
|
49
|
+
opts.onRow?.(row);
|
|
50
|
+
return row;
|
|
47
51
|
}));
|
|
48
52
|
if (Object.keys(updates).length)
|
|
49
53
|
await writeSmokeCache({ ...cache, ...updates });
|
|
54
|
+
return {
|
|
55
|
+
rows,
|
|
56
|
+
ready: rows.filter((row) => isReady(row, runSmoke)).length,
|
|
57
|
+
fixes: rows.flatMap(fixLines),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* `aiki doctor` — runs the checks, prints the table + actionable fixes. Exit 0 iff ≥2 providers
|
|
62
|
+
* are "ready" (§5, §8 quorum). With smoke on, ready = smoke passed; with --no-smoke, ready = detected.
|
|
63
|
+
*/
|
|
64
|
+
export async function doctor(opts = {}) {
|
|
65
|
+
const runSmoke = opts.smoke !== false;
|
|
66
|
+
const { rows, ready, fixes } = await runDoctorChecks(opts);
|
|
50
67
|
const lines = [];
|
|
51
68
|
lines.push('');
|
|
52
69
|
lines.push(` aiki doctor — provider status${runSmoke ? '' : ' (smoke skipped)'}`);
|
|
53
70
|
lines.push('');
|
|
54
71
|
lines.push(` ${pad('PROVIDER', 10)}${pad('VERSION', 11)}${pad('STATUS', 15)}${pad('JSON', 6)}${pad('READ-ONLY', 11)}${pad('SMOKE', 14)}ROLE`);
|
|
55
72
|
lines.push(` ${'─'.repeat(82)}`);
|
|
56
|
-
|
|
57
|
-
const fixes = [];
|
|
58
|
-
for (const row of rows) {
|
|
73
|
+
for (const row of rows)
|
|
59
74
|
lines.push(renderRow(row, runSmoke));
|
|
60
|
-
if (isReady(row, runSmoke))
|
|
61
|
-
ready++;
|
|
62
|
-
fixes.push(...fixLines(row));
|
|
63
|
-
}
|
|
64
75
|
lines.push('');
|
|
65
76
|
lines.push(` ${ready}/3 providers ready. Engine minimum quorum: 2.`);
|
|
66
77
|
if (rows.some((r) => r.cached))
|
|
@@ -97,19 +108,40 @@ function renderSmoke(detected, runSmoke, smoke) {
|
|
|
97
108
|
return `✔ ${(smoke.durationMs / 1000).toFixed(1)}s`;
|
|
98
109
|
return `✖ ${smoke.error ?? 'FAIL'}`;
|
|
99
110
|
}
|
|
100
|
-
|
|
111
|
+
/** The actionable fix for a failing row, or undefined when the row is healthy.
|
|
112
|
+
* Fixes are user-facing (show display name) but reference the real binary for commands. */
|
|
113
|
+
function fixFor(row) {
|
|
101
114
|
const { det, smoke } = row;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (det.status !== 'READY' && det.hint)
|
|
105
|
-
return [` ${name}: ${det.hint}`];
|
|
115
|
+
if (det.status !== 'READY')
|
|
116
|
+
return det.hint;
|
|
106
117
|
if (smoke && !smoke.ok) {
|
|
107
|
-
|
|
118
|
+
return smoke.error === 'AUTH'
|
|
108
119
|
? `run \`${det.id}\` once to log in`
|
|
109
120
|
: smoke.error === 'QUOTA'
|
|
110
|
-
? '
|
|
121
|
+
? 'retry later — quota/rate limit resets on its own'
|
|
111
122
|
: (smoke.detail ?? 'smoke failed');
|
|
112
|
-
return [` ${name}: ${fix}`];
|
|
113
123
|
}
|
|
114
|
-
return
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function fixLines(row) {
|
|
127
|
+
const fix = fixFor(row);
|
|
128
|
+
return fix ? [` ${DISPLAY_NAME[row.det.id]}: ${fix}`] : [];
|
|
129
|
+
}
|
|
130
|
+
/** One TUI preflight row: status icon + human label + the fix to show when it failed. */
|
|
131
|
+
export function preflightLine(row) {
|
|
132
|
+
const name = DISPLAY_NAME[row.det.id];
|
|
133
|
+
if (row.det.status !== 'READY')
|
|
134
|
+
return { ok: false, label: `${name} — not installed`, fix: fixFor(row) };
|
|
135
|
+
const version = row.det.version ? ` ${row.det.version}` : '';
|
|
136
|
+
const { smoke } = row;
|
|
137
|
+
if (!smoke)
|
|
138
|
+
return { ok: true, label: `${name}${version} — CLI detected` };
|
|
139
|
+
if (smoke.ok) {
|
|
140
|
+
const timing = row.cached ? 'cached ≤6h' : `${(smoke.durationMs / 1000).toFixed(1)}s`;
|
|
141
|
+
return { ok: true, label: `${name}${version} — ready (smoke ${timing})` };
|
|
142
|
+
}
|
|
143
|
+
const reason = smoke.error === 'AUTH' ? 'auth failed'
|
|
144
|
+
: smoke.error === 'QUOTA' ? 'quota/rate limited'
|
|
145
|
+
: `smoke failed (${smoke.error ?? 'unknown'})`;
|
|
146
|
+
return { ok: false, label: `${name}${version} — ${reason}`, fix: fixFor(row) };
|
|
115
147
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -46,14 +46,16 @@ program
|
|
|
46
46
|
.description('Headless run of a workflow (§5). idea-refinement: text/file. code-review: git diff or --diff.')
|
|
47
47
|
.argument('<workflow>', 'workflow id: idea-refinement | code-review')
|
|
48
48
|
.argument('[input]', 'idea-refinement: inline text or a path to a .md file')
|
|
49
|
-
.option('--budget <n>', 'max provider calls for this run (default
|
|
49
|
+
.option('--budget <n>', 'max provider calls for this run (default is mode-aware)', (v) => parseInt(v, 10))
|
|
50
50
|
.option('--base <ref>', 'code-review: base git ref to diff from (default: detected default branch)')
|
|
51
51
|
.option('--head <ref>', 'code-review: head git ref to diff to (default HEAD)')
|
|
52
52
|
.option('--diff <file>', 'code-review: review a patch file instead of computing a git diff')
|
|
53
|
+
.option('--evidence <path>', 'idea-refinement: local source file/directory (stores paths + hashes, not copies)')
|
|
54
|
+
.option('--mode <mode>', 'idea-refinement: quick | council | research (default council)')
|
|
53
55
|
.option('--cheap', 'code-review: Gemini+Codex review, Claude judges only disputes (~⅓ the Opus; experimental)')
|
|
54
56
|
.option('--yes', 'skip the run-cost confirmation prompt')
|
|
55
57
|
.action(async (workflow, input, opts) => {
|
|
56
|
-
process.exit(await runCommand(workflow, input, { budget: opts.budget, base: opts.base, head: opts.head, diff: opts.diff, cheap: opts.cheap, yes: opts.yes }));
|
|
58
|
+
process.exit(await runCommand(workflow, input, { budget: opts.budget, base: opts.base, head: opts.head, diff: opts.diff, evidence: opts.evidence, mode: opts.mode, cheap: opts.cheap, yes: opts.yes }));
|
|
57
59
|
});
|
|
58
60
|
program
|
|
59
61
|
.command('show')
|
|
@@ -89,14 +91,37 @@ program
|
|
|
89
91
|
});
|
|
90
92
|
program
|
|
91
93
|
.command('bench')
|
|
92
|
-
.description('Run
|
|
93
|
-
.argument('<workflow>', '
|
|
94
|
-
.option('--arms <list>', 'comma-separated arms to run
|
|
94
|
+
.description('Run code-review, idea lane, or frozen idea-v3 benchmark arms; writes bench/results/*.json.')
|
|
95
|
+
.argument('<workflow>', 'code-review | idea-refinement | idea-v3')
|
|
96
|
+
.option('--arms <list>', 'comma-separated arms to run (code review defaults A,B,C,D; idea-v3 defaults B,C,D2,R on build)')
|
|
95
97
|
.option('--set <name>', 'task set: build | holdout', 'build')
|
|
96
98
|
.option('--resume', 'continue the latest results file: keep already-scored case×arm pairs, retry the rest')
|
|
97
99
|
.option('--yes', 'actually run; without it, print the pre-run Opus-call estimate and exit')
|
|
100
|
+
.option('--import <file>', 'idea-refinement: import blind adjudications into the latest campaign file (offline, frozen R0 scorer)')
|
|
101
|
+
.option('--case <id>', 'idea-refinement: restrict the metered run to one build case (combine with --resume)')
|
|
102
|
+
.option('--baseline-provider <id>', 'idea-v3 build tuning: provider used for B/C (claude | codex | agy)', 'claude')
|
|
103
|
+
.option('--d2-import <file>', 'idea-v3 build: observations produced by the archived R0 runner at commit 680fba3')
|
|
104
|
+
.option('--export-blind <dir>', 'idea-v3: export 3 independently ordered offline rating packets from a complete campaign')
|
|
105
|
+
.option('--import-ratings <file>', 'idea-v3: score a locked three-rater resolution once (offline, frozen R0 scorer)')
|
|
106
|
+
.option('--freeze-protocol <draft-file>', 'idea-v3: freeze a complete scored build protocol once (offline)')
|
|
107
|
+
.option('--publish-results <scores-file>', 'idea-v3: write RESULTS-IDEA-V3.md from frozen holdout scores (offline)')
|
|
108
|
+
.option('--campaign <file>', 'idea-v3 offline operation: explicit campaign file (default: latest)')
|
|
98
109
|
.action(async (workflow, opts) => {
|
|
99
|
-
process.exit(await benchCommand(workflow, {
|
|
110
|
+
process.exit(await benchCommand(workflow, {
|
|
111
|
+
arms: opts.arms,
|
|
112
|
+
set: opts.set,
|
|
113
|
+
resume: opts.resume,
|
|
114
|
+
yes: opts.yes,
|
|
115
|
+
import: opts.import,
|
|
116
|
+
case: opts.case,
|
|
117
|
+
baselineProvider: opts.baselineProvider,
|
|
118
|
+
d2Import: opts.d2Import,
|
|
119
|
+
exportBlind: opts.exportBlind,
|
|
120
|
+
importRatings: opts.importRatings,
|
|
121
|
+
freezeProtocol: opts.freezeProtocol,
|
|
122
|
+
publishResults: opts.publishResults,
|
|
123
|
+
campaign: opts.campaign,
|
|
124
|
+
}));
|
|
100
125
|
});
|
|
101
126
|
program
|
|
102
127
|
.command('models')
|
package/dist/cli/resolve.js
CHANGED
|
@@ -9,12 +9,12 @@ import { createInterface } from 'node:readline';
|
|
|
9
9
|
import { scoreFindings } from '../orchestration/stages/cr-report.js';
|
|
10
10
|
import { readJsonArtifact, resolveRunId, runDir } from '../storage/runs-read.js';
|
|
11
11
|
import { appendFeedback, buildFeedbackEntries, FeedbackError, parseVerdictFlags, VERDICT_VOCAB } from '../storage/feedback.js';
|
|
12
|
+
import { adaptLegacyDecisionGraph } from '../orchestration/legacy-idea-adapter.js';
|
|
12
13
|
/** Build the annotatable list for an idea-refinement run (adjudicated contradictions). */
|
|
13
|
-
function ideaItems(judge,
|
|
14
|
+
function ideaItems(judge, graph) {
|
|
14
15
|
return judge.adjudications.map((a) => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
return { id: a.id, ruling: a.ruling, label: `[${a.ruling}] ${dispute || a.reasoning}` };
|
|
16
|
+
const claim = graph?.claims.find((item) => item.id === a.id);
|
|
17
|
+
return { id: a.id, ruling: a.ruling, label: `[${a.ruling}] ${claim?.proposition ?? a.reasoning}` };
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
/** Build the annotatable list for a code-review run (kept findings from the report). */
|
|
@@ -114,8 +114,9 @@ export async function resolve(runArg, opts = {}) {
|
|
|
114
114
|
process.stdout.write(` run ${match.runId} has no adjudicated disputes to annotate.\n`);
|
|
115
115
|
return 0;
|
|
116
116
|
}
|
|
117
|
-
const
|
|
118
|
-
|
|
117
|
+
const storedGraph = await readJsonArtifact(dir, '07-decision-graph.json');
|
|
118
|
+
const legacyMap = storedGraph ? null : await readJsonArtifact(dir, '07-disagreement-map.json');
|
|
119
|
+
items = ideaItems(judge, storedGraph ?? (legacyMap ? adaptLegacyDecisionGraph(legacyMap) : null));
|
|
119
120
|
itemType = 'adjudication';
|
|
120
121
|
}
|
|
121
122
|
if (items.length === 0) {
|
package/dist/cli/resume.js
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveRunsRoot } from '../storage/paths.js';
|
|
|
9
9
|
import { buildReplayCache } from '../storage/replay.js';
|
|
10
10
|
import { findSession } from '../storage/sessions.js';
|
|
11
11
|
import { readJsonArtifact, resolveRunId, runDir } from '../storage/runs-read.js';
|
|
12
|
+
import { EvidencePack } from '../orchestration/evidence-pack.js';
|
|
12
13
|
export async function resumeCommand(runArg, opts = {}) {
|
|
13
14
|
if (!runArg) {
|
|
14
15
|
process.stderr.write('usage: aiki resume <session-id> (see `aiki sessions`)\n');
|
|
@@ -46,6 +47,11 @@ export async function resumeCommand(runArg, opts = {}) {
|
|
|
46
47
|
}
|
|
47
48
|
workflow = meta.workflow;
|
|
48
49
|
}
|
|
50
|
+
const previousMeta = await readJsonArtifact(oldDir, 'meta.json');
|
|
51
|
+
if (!previousMeta) {
|
|
52
|
+
process.stderr.write(`cannot read meta.json for ${oldId} — nothing to resume.\n`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
49
55
|
// Recover the original input the run was started with.
|
|
50
56
|
const inputFile = workflow === 'code-review' ? 'diff.patch' : 'idea.md';
|
|
51
57
|
let input;
|
|
@@ -56,6 +62,16 @@ export async function resumeCommand(runArg, opts = {}) {
|
|
|
56
62
|
process.stderr.write(`cannot recover the input (inputs/${inputFile}) for ${oldId} — nothing to resume.\n`);
|
|
57
63
|
return 1;
|
|
58
64
|
}
|
|
65
|
+
let evidencePack;
|
|
66
|
+
const savedEvidencePack = await readJsonArtifact(oldDir, 'inputs/evidence-pack.json');
|
|
67
|
+
if (savedEvidencePack) {
|
|
68
|
+
const parsed = EvidencePack.safeParse(savedEvidencePack);
|
|
69
|
+
if (!parsed.success) {
|
|
70
|
+
process.stderr.write(`cannot validate inputs/evidence-pack.json for ${oldId} — nothing to resume.\n`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
evidencePack = parsed.data;
|
|
74
|
+
}
|
|
59
75
|
const replay = await buildReplayCache(oldDir);
|
|
60
76
|
if (replay.size === 0) {
|
|
61
77
|
process.stderr.write(`no completed calls found for ${oldId} — start a fresh run instead.\n`);
|
|
@@ -74,6 +90,7 @@ export async function resumeCommand(runArg, opts = {}) {
|
|
|
74
90
|
}
|
|
75
91
|
process.stdout.write(` resuming ${oldId} (${workflow}) — replaying ${replay.size} completed call(s); only the rest will hit a model.\n`);
|
|
76
92
|
const outcome = await runEngine(workflow, input, {
|
|
93
|
+
mode: previousMeta.mode,
|
|
77
94
|
budget: cfg.budget,
|
|
78
95
|
deadlineMs: cfg.deadlineMs,
|
|
79
96
|
roleOverrides: cfg.roles,
|
|
@@ -82,6 +99,7 @@ export async function resumeCommand(runArg, opts = {}) {
|
|
|
82
99
|
replay,
|
|
83
100
|
resumedFrom: oldId,
|
|
84
101
|
providerModels: cfg.models,
|
|
102
|
+
evidencePack,
|
|
85
103
|
});
|
|
86
104
|
if (outcome.ok) {
|
|
87
105
|
process.stdout.write(`\n ✔ resumed run ${outcome.runId} complete — ${outcome.callCount} new provider call(s)\n artifacts: ${outcome.dir}\n\n`);
|