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.
Files changed (59) hide show
  1. package/CHANGELOG.md +98 -7
  2. package/README.md +109 -30
  3. package/dist/bench/arms.js +10 -9
  4. package/dist/bench/harness.js +13 -10
  5. package/dist/bench/idea-lane-rotation.js +237 -0
  6. package/dist/bench/idea-v3-bench.js +506 -0
  7. package/dist/bench/idea-v3-rating.js +582 -0
  8. package/dist/bench/results.js +10 -3
  9. package/dist/bench/scoring/decision-insights.js +112 -0
  10. package/dist/bench/scoring/seeded-bugs.js +4 -0
  11. package/dist/cli/bench.js +180 -3
  12. package/dist/cli/doctor.js +56 -24
  13. package/dist/cli/index.js +31 -6
  14. package/dist/cli/resolve.js +7 -6
  15. package/dist/cli/resume.js +18 -0
  16. package/dist/cli/run.js +63 -8
  17. package/dist/council/view.js +378 -109
  18. package/dist/orchestration/calculations.js +97 -0
  19. package/dist/orchestration/context.js +37 -9
  20. package/dist/orchestration/decision-dossier.js +262 -0
  21. package/dist/orchestration/decision-graph.js +256 -0
  22. package/dist/orchestration/engine.js +5 -2
  23. package/dist/orchestration/evidence-pack.js +46 -0
  24. package/dist/orchestration/idea-lanes.js +20 -0
  25. package/dist/orchestration/jsonStage.js +72 -0
  26. package/dist/orchestration/legacy-idea-adapter.js +102 -0
  27. package/dist/orchestration/modes.js +33 -0
  28. package/dist/orchestration/preflight.js +183 -0
  29. package/dist/orchestration/quick-analysis.js +81 -0
  30. package/dist/orchestration/stages/cr-ladder.js +80 -0
  31. package/dist/orchestration/stages/s10-render.js +562 -150
  32. package/dist/orchestration/stages/s4-analyze.js +12 -7
  33. package/dist/orchestration/stages/s5-drift.js +9 -9
  34. package/dist/orchestration/stages/s6-positions.js +10 -0
  35. package/dist/orchestration/stages/s7-decision-graph.js +76 -0
  36. package/dist/orchestration/stages/s8-verify.js +153 -46
  37. package/dist/orchestration/stages/s8b-rebuttal.js +208 -0
  38. package/dist/orchestration/stages/s9-judge.js +329 -108
  39. package/dist/orchestration/stages/s9b-plan.js +85 -75
  40. package/dist/providers/codex.js +2 -1
  41. package/dist/providers/spawn.js +5 -0
  42. package/dist/schemas/index.js +572 -13
  43. package/dist/skills/idea-refinement/analyst.md +18 -14
  44. package/dist/skills/idea-refinement/economics-delivery.md +7 -0
  45. package/dist/skills/idea-refinement/market-adoption.md +7 -0
  46. package/dist/storage/runs.js +11 -4
  47. package/dist/tui/app.js +37 -13
  48. package/dist/tui/format.js +4 -5
  49. package/dist/tui/smart-entry.js +17 -1
  50. package/dist/tui/timeline.js +2 -4
  51. package/dist/workflows/code-review.js +4 -2
  52. package/dist/workflows/idea-refinement.js +110 -46
  53. package/package.json +12 -4
  54. package/dist/orchestration/stages/s0-grill.js +0 -79
  55. package/dist/orchestration/stages/s1-intent.js +0 -25
  56. package/dist/orchestration/stages/s2-misread.js +0 -76
  57. package/dist/orchestration/stages/s3-prompts.js +0 -55
  58. package/dist/orchestration/stages/s6-claims.js +0 -56
  59. 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
- const VALID_ARMS = ['A', 'B', 'C', 'D', 'E'];
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 only "code-review" in v1 (got "${workflow}")\n`);
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';
@@ -18,19 +18,18 @@ const READONLY_LABEL = {
18
18
  };
19
19
  const pad = (s, w) => s.padEnd(w);
20
20
  /**
21
- * `aiki doctor` detection + flag probe + (optional) smoke test, table output, actionable
22
- * fixes. Exit 0 iff ≥2 providers are "ready" (§5, §8 quorum). With smoke on, ready = smoke
23
- * passed; with --no-smoke, ready = detected.
24
- *
25
- * Smoke results are cached 6h in `.aiki/smoke-cache.json` (§242); `--fresh` bypasses the cache.
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 doctor(opts = {}) {
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 rows = await Promise.all(PROVIDER_IDS.map(async (id) => {
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
- let ready = 0;
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
- function fixLines(row) {
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
- const name = DISPLAY_NAME[det.id];
103
- // Fixes are user-facing (show display name) but reference the real binary for commands.
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
- const fix = smoke.error === 'AUTH'
118
+ return smoke.error === 'AUTH'
108
119
  ? `run \`${det.id}\` once to log in`
109
120
  : smoke.error === 'QUOTA'
110
- ? 'provider quota/rate limited retry later'
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 12)', (v) => parseInt(v, 10))
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 benchmark arms A–D on a task set; writes bench/results/*.json + summary table (§17).')
93
- .argument('<workflow>', 'workflow id (v1: code-review)')
94
- .option('--arms <list>', 'comma-separated arms to run', 'A,B,C,D')
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, { arms: opts.arms, set: opts.set, resume: opts.resume, yes: opts.yes }));
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')
@@ -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, map) {
14
+ function ideaItems(judge, graph) {
14
15
  return judge.adjudications.map((a) => {
15
- const c = map?.contradictions.find((x) => x.id === a.id);
16
- const dispute = c?.attacks[0]?.argument ?? '';
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 map = await readJsonArtifact(dir, '07-disagreement-map.json');
118
- items = ideaItems(judge, map);
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) {
@@ -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`);