aiki-cli 0.3.0 → 0.3.2

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 (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -8
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +10 -7
  6. package/dist/council/view.js +146 -46
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +467 -80
  9. package/dist/orchestration/decision-graph.js +31 -0
  10. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  11. package/dist/orchestration/modes.js +16 -4
  12. package/dist/orchestration/preflight.js +43 -9
  13. package/dist/orchestration/quick-analysis.js +35 -6
  14. package/dist/orchestration/stages/s10-render.js +179 -79
  15. package/dist/orchestration/stages/s4-analyze.js +3 -1
  16. package/dist/orchestration/stages/s6-positions.js +18 -0
  17. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  18. package/dist/orchestration/stages/s8-verify.js +26 -9
  19. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  20. package/dist/orchestration/stages/s9-judge.js +36 -13
  21. package/dist/orchestration/stages/s9b-plan.js +208 -35
  22. package/dist/orchestration/url-sources.js +200 -0
  23. package/dist/schemas/index.js +134 -6
  24. package/dist/skills/idea-refinement/analyst.md +5 -5
  25. package/dist/skills/idea-refinement/chair.md +18 -0
  26. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  27. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  28. package/dist/skills/idea-refinement/planner.md +25 -19
  29. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  30. package/dist/skills/idea-refinement/verifier.md +17 -0
  31. package/dist/storage/runs.js +2 -1
  32. package/dist/tui/app.js +19 -4
  33. package/dist/workflows/idea-refinement.js +51 -15
  34. package/package.json +1 -1
@@ -3,37 +3,25 @@
3
3
  // sensitivity, coverage, contribution, and confidence are DERIVED here rather than invented by the judge.
4
4
  // A truly missing required field is a template bug (fail loudly); degraded-but-valid
5
5
  // states (S8 skipped, items UNVERIFIED, empty consensus) render normally. User-facing → DISPLAY_NAME.
6
+ import { ReaderBrief as ReaderBriefSchema, readerBriefIssues } from '../../schemas/index.js';
6
7
  import { DISPLAY_NAME } from '../../providers/types.js';
7
8
  import { overlap, tokenize } from '../cluster.js';
8
- import { renderDecisionDossierMarkdown } from '../decision-dossier.js';
9
+ import { interpretClaimOutcome } from '../decision-graph.js';
10
+ import { buildReaderProjection, renderDecisionDossierMarkdown, sanitizeReaderText } from '../decision-dossier.js';
9
11
  import { callCategory } from '../modes.js';
10
12
  /** Pure graph-backed decision audit. */
11
13
  export function deriveAudit(graph, judgeReport) {
12
- const ruling = new Map(judgeReport.adjudications.map((a) => [a.id, a.ruling]));
14
+ const ruling = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
13
15
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
14
16
  return graph.claims.map((claim) => {
15
17
  const positions = claim.position_ids.map((id) => positionById.get(id));
16
18
  const providers = [...new Set(positions.map((position) => position.provider))];
17
- let status;
18
- let confidence;
19
- if (claim.state === 'UNCERTAIN' || claim.evidence_state !== 'SUPPORTED') {
20
- [status, confidence] = ['unverified', 'LOW'];
21
- }
22
- else if (claim.state === 'SHARED_CONCERN' || (claim.state === 'UNIQUE' && positions[0]?.stance === 'OPPOSE')) {
23
- [status, confidence] = ['failed', providers.length >= 2 ? 'HIGH' : 'MEDIUM'];
24
- }
25
- else if (claim.state === 'DISAGREEMENT') {
26
- const result = ruling.get(claim.id);
27
- if (result === 'UPHOLD')
28
- [status, confidence] = ['failed', 'MEDIUM'];
29
- else if (result === 'REJECT')
30
- [status, confidence] = ['held', 'MEDIUM'];
31
- else
32
- [status, confidence] = ['unverified', 'LOW'];
33
- }
34
- else {
35
- [status, confidence] = ['held', providers.length >= 2 ? 'HIGH' : 'MEDIUM'];
36
- }
19
+ const outcome = interpretClaimOutcome(graph, claim, ruling.get(claim.id));
20
+ const status = outcome.decisionEffect === 'HELD' ? 'held'
21
+ : outcome.decisionEffect === 'FAILED' ? 'failed' : 'unverified';
22
+ const confidence = status === 'unverified' ? 'LOW'
23
+ : claim.state === 'DISAGREEMENT' ? 'MEDIUM'
24
+ : providers.length >= 2 ? 'HIGH' : 'MEDIUM';
37
25
  return { id: claim.id, statement: claim.proposition, providers, status, confidence };
38
26
  });
39
27
  }
@@ -96,8 +84,10 @@ const DEGRADATION_FLAGS = ['low_diversity', 'synthesis_suspect', 'plan_fallback'
96
84
  * self-confidence never enters it, and consensus alone can never reach the High band. */
97
85
  export function computeConfidence(graph, flags) {
98
86
  const loadBearing = graph.claims.filter((claim) => claim.load_bearing);
99
- const verificationCoverage = loadBearing.length
100
- ? loadBearing.filter((claim) => claim.evidence_state === 'SUPPORTED').length / loadBearing.length : 0;
87
+ const factual = loadBearing.filter((claim) => claim.nature === 'FACTUAL');
88
+ const verificationClaims = factual.length ? factual : loadBearing;
89
+ const verificationCoverage = verificationClaims.length
90
+ ? verificationClaims.filter((claim) => claim.evidence_state === 'SUPPORTED').length / verificationClaims.length : 0;
101
91
  const independentConvergence = graph.claims.length
102
92
  ? graph.claims.filter((claim) => claim.state === 'CONSENSUS' || claim.state === 'SHARED_CONCERN').length / graph.claims.length : 0;
103
93
  const evidenceQuality = graph.evidence.length
@@ -109,22 +99,93 @@ export function computeConfidence(graph, flags) {
109
99
  score = Math.min(score, 79); // consensus alone never yields High
110
100
  score = Math.max(0, Math.min(100, score));
111
101
  const label = score >= 80 ? 'High' : score >= 60 ? 'Medium' : 'Low';
112
- return { score, label, verificationCoverage, independentConvergence, evidenceQuality, stability, criticalRiskPenalty };
102
+ return {
103
+ score,
104
+ label,
105
+ verificationCoverage,
106
+ ...(factual.length ? { verificationScope: 'FACTUAL' } : {}),
107
+ independentConvergence,
108
+ evidenceQuality,
109
+ stability,
110
+ criticalRiskPenalty,
111
+ };
113
112
  }
114
113
  const STANCE_MAP = { SUPPORT: 'AGREE', OPPOSE: 'DISAGREE', MIXED: 'CONDITIONAL', UNKNOWN: 'UNKNOWN' };
115
114
  const VERIFICATION_MAP = { SUPPORTED: 'VERIFIED', CONFLICTED: 'PARTIAL', UNVERIFIED: 'UNVERIFIED' };
116
115
  const SEVERITY_MAP = { DECISIVE: 'High', MATERIAL: 'Medium', LOW: 'Low' };
117
- function claimRuling(claim, adjudication) {
118
- // UPHOLD/REJECT inverts ONLY on a disagreement (the ruling is on the objection); for every other
119
- // state the ruling is evidence-based, matching deriveAudit's semantics.
120
- if (claim.state === 'DISAGREEMENT') {
121
- if (!adjudication || adjudication.ruling === 'UNRESOLVED')
122
- return 'UNRESOLVED';
123
- return adjudication.ruling === 'UPHOLD' ? 'REJECTED' : 'ACCEPTED';
124
- }
125
- if (claim.state === 'UNCERTAIN')
126
- return 'UNRESOLVED';
127
- return claim.evidence_state === 'SUPPORTED' ? 'ACCEPTED' : 'CONDITIONAL';
116
+ function claimRuling(graph, claim, adjudication) {
117
+ const truth = interpretClaimOutcome(graph, claim, adjudication).propositionTruth;
118
+ return truth === 'HOLDS' ? 'ACCEPTED' : truth === 'FAILS' ? 'REJECTED' : 'UNRESOLVED';
119
+ }
120
+ function fallbackReaderBrief(args) {
121
+ const labelFor = (id) => args.graph.claims.find((claim) => claim.id === id)?.proposition ?? null;
122
+ const clean = (value, max) => {
123
+ const text = sanitizeReaderText(value, labelFor);
124
+ if (text.length <= max)
125
+ return text;
126
+ const clipped = text.slice(0, max - 1);
127
+ const boundary = clipped.lastIndexOf(' ');
128
+ return `${clipped.slice(0, boundary > max * 0.6 ? boundary : max - 1).trimEnd()}…`;
129
+ };
130
+ const keyPoints = args.judgeReport.key_points ?? [];
131
+ const missing = args.missingRequestedOutputs.length
132
+ ? `Requested deliverables unavailable: ${args.missingRequestedOutputs.join(', ')}.`
133
+ : 'No additional requested deliverable was synthesized.';
134
+ const firstAction = args.actionPlan && !('kind' in args.actionPlan) ? args.actionPlan.actions[0]?.action : undefined;
135
+ const nextStep = firstAction
136
+ ?? args.openQuestions[0]
137
+ ?? args.judgeReport.strongest_counter_case?.reasoning
138
+ ?? 'Review the unresolved decision evidence before committing.';
139
+ const adjudicationById = new Map(args.judgeReport.adjudications.map((item) => [item.id, item]));
140
+ const acceptedClaims = new Set(args.graph.claims
141
+ .filter((claim) => claim.evidence_state === 'SUPPORTED'
142
+ && interpretClaimOutcome(args.graph, claim, adjudicationById.get(claim.id)).propositionTruth === 'HOLDS')
143
+ .map((claim) => claim.id));
144
+ const sourceIds = args.graph.evidence
145
+ .filter((evidence) => {
146
+ if (evidence.source_kind !== 'PRIMARY' && evidence.source_kind !== 'SECONDARY')
147
+ return false;
148
+ try {
149
+ const url = new URL(evidence.url ?? '');
150
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
151
+ return false;
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ return args.graph.edges.some((edge) => edge.from === evidence.id && edge.type === 'SUPPORTS' && acceptedClaims.has(edge.to));
157
+ })
158
+ .map((evidence) => evidence.id)
159
+ .slice(0, 8);
160
+ const raw = {
161
+ headline: clean(args.judgeReport.verdict, 160),
162
+ bottom_line: clean(args.judgeReport.verdict, 1200),
163
+ sections: [
164
+ {
165
+ heading: 'Why',
166
+ summary: clean(keyPoints[0] ?? args.judgeReport.verdict, 1000),
167
+ bullets: [...keyPoints.slice(1, 3), ...(args.judgeReport.conditions ?? []).slice(0, 2)].map((item) => clean(item, 500)),
168
+ },
169
+ {
170
+ heading: 'What remains',
171
+ summary: missing,
172
+ bullets: [args.openQuestions[0], args.judgeReport.strongest_counter_case?.reasoning]
173
+ .filter((item) => Boolean(item)).map((item) => clean(item, 500)),
174
+ },
175
+ ],
176
+ next_step: clean(nextStep, 600),
177
+ caveats: [
178
+ ...(args.missingRequestedOutputs.length ? [missing] : []),
179
+ ...(args.flags.has('plan_skipped') || args.flags.has('plan_fallback')
180
+ ? ['The requested synthesis was unavailable; this concise summary uses the recorded chair decision only.'] : []),
181
+ ],
182
+ source_ids: sourceIds,
183
+ };
184
+ const brief = ReaderBriefSchema.parse(raw);
185
+ const issues = readerBriefIssues(brief, args.graph.claims.map((claim) => claim.id));
186
+ if (issues.length)
187
+ throw new Error(`fallback reader brief leaked graph ids: ${issues.join(', ')}`);
188
+ return brief;
128
189
  }
129
190
  function verificationStatusByClaim(verifications) {
130
191
  const statuses = new Map();
@@ -154,6 +215,18 @@ function buildDossier(args) {
154
215
  }
155
216
  const recommendationIds = (judgeReport.recommendation_claim_ids ?? [])
156
217
  .filter((id) => claimById.has(id));
218
+ const recommendationPositionIds = new Set(recommendationIds.flatMap((id) => claimById.get(id)?.position_ids ?? []));
219
+ const seatStats = [...graph.positions.reduce((stats, position) => {
220
+ const provider = position.id.split('/')[0].replace(/-coverage-fill$/, '');
221
+ const entry = stats.get(provider) ?? { provider, positions: 0, evidenced: 0, survivedIntoDecision: 0 };
222
+ entry.positions += 1;
223
+ if (position.evidence_ids.length)
224
+ entry.evidenced += 1;
225
+ if (recommendationPositionIds.has(position.id))
226
+ entry.survivedIntoDecision += 1;
227
+ stats.set(provider, entry);
228
+ return stats;
229
+ }, new Map()).values()];
157
230
  const anchors = recommendationIds.length
158
231
  ? recommendationIds
159
232
  : graph.claims.filter((claim) => claim.load_bearing).slice(0, 8).map((claim) => claim.id);
@@ -191,6 +264,8 @@ function buildDossier(args) {
191
264
  id: item.id,
192
265
  source: item.locator ?? (item.source_kind === 'MODEL_KNOWLEDGE' ? `${disp(item.provider)} model knowledge` : item.source_kind),
193
266
  sourceKind: item.source_kind,
267
+ ...(item.title ? { title: item.title } : {}),
268
+ ...(item.url ? { url: item.url } : {}),
194
269
  date: item.accessed_at ?? 'Not recorded',
195
270
  freshness: item.freshness,
196
271
  verificationStatus,
@@ -264,7 +339,7 @@ function buildDossier(args) {
264
339
  linkedClaimIds: [...new Set(related)],
265
340
  };
266
341
  });
267
- const experiments = actionPlan && !('kind' in actionPlan)
342
+ const experiments = actionPlan && !('kind' in actionPlan) && actionPlan.actions.length > 0
268
343
  ? {
269
344
  status: 'AVAILABLE',
270
345
  note: actionPlan.sequencing_note,
@@ -279,11 +354,28 @@ function buildDossier(args) {
279
354
  }
280
355
  : {
281
356
  status: 'DEGRADED',
282
- note: actionPlan && 'kind' in actionPlan
283
- ? `Planner unavailable: ${actionPlan.reason}. Unresolved: ${actionPlan.unresolved_questions.join('; ')}`
284
- : 'No planner artifact was recorded.',
357
+ note: actionPlan && !('kind' in actionPlan)
358
+ ? 'The answer is available, but no validation action survived anchor checks.'
359
+ : actionPlan && 'kind' in actionPlan
360
+ ? `Planner unavailable: ${actionPlan.reason}. Unresolved: ${actionPlan.unresolved_questions.join('; ')}`
361
+ : 'No planner artifact was recorded.',
285
362
  actions: [],
286
363
  };
364
+ const featureBacklog = actionPlan && !('kind' in actionPlan) ? actionPlan.feature_backlog : undefined;
365
+ const implementationPlan = actionPlan && !('kind' in actionPlan) ? actionPlan.implementation_plan : undefined;
366
+ const missingRequestedOutputs = [
367
+ ...(args.requestedOutputs.includes('FEATURE_BACKLOG') && !featureBacklog ? ['FEATURE_BACKLOG'] : []),
368
+ ...(args.requestedOutputs.includes('IMPLEMENTATION_PLAN') && !implementationPlan ? ['IMPLEMENTATION_PLAN'] : []),
369
+ ];
370
+ const persistedReaderBrief = actionPlan && !('kind' in actionPlan) ? actionPlan.reader_brief : undefined;
371
+ const readerBrief = persistedReaderBrief ?? (args.newDecisionContract ? fallbackReaderBrief({
372
+ graph,
373
+ judgeReport,
374
+ actionPlan,
375
+ openQuestions: mergeOpenQuestions(seats),
376
+ missingRequestedOutputs,
377
+ flags: args.flags,
378
+ }) : undefined);
287
379
  const counter = judgeReport.strongest_counter_case;
288
380
  const contributions = args.models.map((model) => ({
289
381
  provider: model.provider,
@@ -322,10 +414,15 @@ function buildDossier(args) {
322
414
  coverage,
323
415
  sensitivity,
324
416
  experiments,
417
+ ...(featureBacklog ? { featureBacklog } : {}),
418
+ ...(implementationPlan ? { implementationPlan } : {}),
419
+ ...(readerBrief ? { readerBrief } : {}),
420
+ missingRequestedOutputs,
325
421
  counterCase: counter
326
422
  ? { available: true, reasoning: counter.reasoning, claimIds: counter.claim_ids.filter((id) => claimById.has(id)) }
327
423
  : { available: false, reasoning: 'No graph-anchored counter-case was recorded.', claimIds: [] },
328
424
  contributions,
425
+ seatStats,
329
426
  technical: {
330
427
  submissions: seats.map((seat) => ({
331
428
  provider: seat.provider,
@@ -344,12 +441,19 @@ export function buildDecisionReport(ctx, args) {
344
441
  const { contract, seats, graph, verifications, judgeReport, actionPlan } = args;
345
442
  const mode = ctx.mode ?? 'council';
346
443
  const flags = new Set(ctx.flags);
444
+ const newDecisionContract = 'success_bar' in contract;
445
+ const hasReaderBrief = Boolean(actionPlan && !('kind' in actionPlan) && actionPlan.reader_brief);
446
+ if (newDecisionContract && !hasReaderBrief && !flags.has('plan_skipped') && !flags.has('plan_fallback')) {
447
+ flags.add('plan_fallback');
448
+ }
347
449
  const confidence = computeConfidence(graph, flags);
348
450
  const status = statusFrom(judgeReport);
349
451
  const rulingById = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
350
452
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
351
453
  const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
352
454
  const openQuestions = mergeOpenQuestions(seats);
455
+ const requestedOutputs = contract.requested_outputs
456
+ ?? ['DECISION'];
353
457
  const claims = graph.claims.map((claim) => {
354
458
  const stances = {};
355
459
  for (const id of claim.position_ids) {
@@ -361,7 +465,7 @@ export function buildDecisionReport(ctx, args) {
361
465
  text: claim.proposition,
362
466
  stances,
363
467
  verification: VERIFICATION_MAP[claim.evidence_state],
364
- ruling: claimRuling(claim, rulingById.get(claim.id)),
468
+ ruling: claimRuling(graph, claim, rulingById.get(claim.id)),
365
469
  loadBearing: claim.load_bearing,
366
470
  sensitivity: claim.sensitivity,
367
471
  };
@@ -393,9 +497,6 @@ export function buildDecisionReport(ctx, args) {
393
497
  const consensusType = mode === 'quick' ? 'single_analyst' : disagreements.length === 0
394
498
  ? (claims.some((claim) => claim.ruling === 'UNRESOLVED') ? 'convergent_with_unresolved_claims' : 'unanimous')
395
499
  : disagreements.every((d) => d.status === 'RESOLVED') ? 'majority_with_dissent' : 'contested';
396
- const risks = graph.claims
397
- .filter((claim) => claim.load_bearing && claim.evidence_state !== 'SUPPORTED')
398
- .map((claim) => ({ risk: claim.proposition, severity: SEVERITY_MAP[claim.sensitivity] ?? 'Medium' }));
399
500
  // Frame the slot as what it is — a decisive assumption that is NOT proven. Echoing the bare
400
501
  // proposition inverts the meaning when the claim is phrased affirmatively (run 20260714-2321:
401
502
  // "a cutover can preserve compliance" read as reassurance, the opposite of the verdict).
@@ -403,6 +504,18 @@ export function buildDecisionReport(ctx, args) {
403
504
  const criticalWarning = criticalClaim
404
505
  ? `Unverified decisive assumption (if false, STOP): ${criticalClaim.proposition} — evidence ${criticalClaim.evidence_state}.`
405
506
  : null;
507
+ // Same framing problem as the critical warning above: an unsettled load-bearing claim rendered as
508
+ // its bare (often affirmatively-phrased) proposition reads as an endorsement, not a risk. Frame
509
+ // each explicitly, sort worst-first, and skip the claim already surfaced as the critical warning
510
+ // so it isn't double-counted.
511
+ const severityRank = { High: 0, Medium: 1, Low: 2 };
512
+ const risks = graph.claims
513
+ .filter((claim) => claim.load_bearing && claim.evidence_state !== 'SUPPORTED' && claim.id !== criticalClaim?.id)
514
+ .map((claim) => ({
515
+ risk: `Rests on unsettled claim: ${claim.proposition} (evidence ${claim.evidence_state}).`,
516
+ severity: SEVERITY_MAP[claim.sensitivity] ?? 'Medium',
517
+ }))
518
+ .sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
406
519
  const verificationResults = verifications.verifications.map((v) => {
407
520
  if ('claim_id' in v) {
408
521
  return {
@@ -467,6 +580,9 @@ export function buildDecisionReport(ctx, args) {
467
580
  actionPlan,
468
581
  rebuttals: args.rebuttals,
469
582
  rubric: args.rubric ?? [],
583
+ requestedOutputs,
584
+ newDecisionContract,
585
+ flags,
470
586
  });
471
587
  const decisionSnapshot = judgeReport.decision_snapshot ? {
472
588
  decisiveNumbers: judgeReport.decision_snapshot.decisive_numbers.map((item) => ({
@@ -552,55 +668,39 @@ export function buildDecisionReport(ctx, args) {
552
668
  ? actionPlan.actions.map((action) => ({ order: action.order, action: action.action, why: action.why, effort: action.effort, killSignal: action.kill_signal }))
553
669
  : [],
554
670
  openQuestions,
555
- flags: [...ctx.flags],
671
+ flags: [...flags],
556
672
  receipt: { calls: ctx.calls.length, budget: ctx.budget.limit, byProvider, modelTimeMs, categories: receiptCategories(ctx) },
557
673
  dossier,
558
674
  };
559
675
  }
560
- const pct = (n) => `${Math.round(n * 100)}%`;
561
676
  export { renderDecisionDossierMarkdown };
562
677
  export function renderReport(ctx, args) {
563
678
  return renderDecisionDossierMarkdown(buildDecisionReport(ctx, args));
564
679
  }
565
680
  // ── Terminal summary (level 1) ───────────────────────────────────────────────
566
- const RULE = '─'.repeat(52);
681
+ const RULE = '─'.repeat(48);
682
+ function terminalLine(value) {
683
+ return value.replace(/\s+/g, ' ').trim();
684
+ }
567
685
  export function renderTerminalSummary(report, paths) {
568
- const summary = report.consensusSummary;
569
- const c = report.confidenceBreakdown;
570
- const snapshot = report.decisionSnapshot;
571
- const decisiveLines = snapshot ? [
572
- 'Decision numbers:',
573
- ...snapshot.decisiveNumbers.slice(0, 3).map((item) => `${item.label}: ${item.value} ${item.meaning}`),
574
- ...(snapshot.payback ? [`Payback (${snapshot.payback.status.replaceAll('_', ' ').toLowerCase()}): ${snapshot.payback.result}`] : []),
575
- `Options: ${snapshot.options.map((option) => `${option.label} — ${option.commitment} (${option.commitmentKind.replace('_', ' ').toLowerCase()})`).join(' · ')}`,
576
- ] : ['Decisive result:', report.verdict.primaryReason];
577
- const checks = [
578
- `[${c.verificationCoverage >= 0.5 ? 'PASS' : 'WARN'}] Verification coverage ${pct(c.verificationCoverage)} of load-bearing claims`,
579
- `[PASS] Minority opinion preserved (${report.minority.dissent.length} dissent item(s))`,
580
- `[${report.verification.refuted === 0 ? 'PASS' : 'WARN'}] Verifier refutations: ${report.verification.refuted}`,
581
- ...(report.flags.length ? [`[WARN] Degradation flags: ${report.flags.join(', ')}`] : []),
582
- `[WARN] Structural score ${c.score}/100 is heuristic — not yet benchmark-calibrated`,
583
- ];
686
+ const projection = report.dossier.readerBrief ? buildReaderProjection(report) : undefined;
687
+ const candidates = projection
688
+ ? [...projection.sections.flatMap((section) => [section.summary, ...section.bullets]), ...projection.caveats, projection.nextStep]
689
+ : [...(report.keyFindings ?? []), ...report.verdict.conditions, report.verdict.primaryReason];
690
+ const takeaways = [...new Set(candidates.map(terminalLine).filter(Boolean))].slice(0, 3);
691
+ const nextStep = projection?.nextStep ?? report.recommendedActions[0]?.action ?? 'Open the report and choose the smallest decisive next action.';
584
692
  return [
585
- report.mode === 'quick' ? 'SINGLE-MODEL DECISION REPORT' : 'MULTI-MODEL DECISION REPORT',
693
+ report.mode === 'quick' ? 'AIKI · SINGLE-MODEL DECISION' : 'AIKI · COUNCIL DECISION',
586
694
  RULE,
587
- `Verdict: ${report.verdict.summary}`,
588
- `Decision state: ${report.verdict.status}`,
589
- `Evidence coverage: ${pct(c.verificationCoverage)} of load-bearing claims independently verified`,
590
- 'Coverage note: unchecked inputs remain; this is not a probability of correctness.',
591
- `${report.mode === 'quick' ? 'Claims' : 'Consensus'}: ${report.verdict.consensusType.replaceAll('_', ' ')} — ${summary.accepted} accepted · ${summary.rejected} rejected · ${summary.unresolved} unresolved`,
592
- '',
593
- ...decisiveLines,
695
+ `Verdict: ${terminalLine(projection?.headline ?? report.verdict.summary)}`,
696
+ terminalLine(projection?.bottomLine ?? report.verdict.primaryReason),
697
+ ...(projection?.warnings[0] ? [`Warning: ${projection.warnings[0].message}`] : []),
594
698
  '',
595
- ...(report.verdict.criticalWarning ? ['Critical warning:', report.verdict.criticalWarning, ''] : []),
596
- ...(report.minority.dissent[0] ? ['Critical dissent:', report.minority.dissent[0], ''] : []),
597
- 'Verification:',
598
- ...checks,
699
+ 'Key takeaways:',
700
+ ...takeaways.map((item) => `- ${item}`),
599
701
  '',
600
- ...(report.recommendedActions[0] ? ['Recommended action:', report.recommendedActions[0].action, ''] : []),
601
- ...(snapshot?.tripwire ? ['Go/no-go tripwire:', `${snapshot.tripwire.metric}: ${snapshot.tripwire.threshold} — ${snapshot.tripwire.decisionRule}`, ''] : []),
602
- `Full report: ${paths.markdownPath}`,
603
- `Audit JSON: ${paths.jsonPath}`,
702
+ `Next step: ${terminalLine(nextStep)}`,
703
+ `Report: ${paths.markdownPath}`,
604
704
  ].join('\n');
605
705
  }
606
706
  export async function s10Render(ctx, args) {
@@ -21,10 +21,12 @@ async function runSeat(ctx, seat, label, lane, prompt) {
21
21
  await ctx.writer.writeRoleOutput(label, output);
22
22
  return { provider: seat, sample: label, lane, output };
23
23
  }
24
- export async function s4Analyze(ctx, prompts) {
24
+ export async function s4Analyze(ctx, prompts, needsSourceFallback = false) {
25
25
  const seats = ctx.roles.s4;
26
26
  const settled = await Promise.allSettled(seats.map((seat, index) => {
27
27
  const lane = IDEA_LANES[index % IDEA_LANES.length];
28
+ if (needsSourceFallback && ctx.mode !== 'quick' && seat === 'codex')
29
+ ctx.addFlag('source_fallback_search');
28
30
  return runSeat(ctx, seat, seat, lane, prompts[lane]);
29
31
  }));
30
32
  const survivors = [];
@@ -1,5 +1,23 @@
1
1
  // S6 — preserve validated analyst submissions for graph compilation. No lexical merging: positions
2
2
  // keep their original text and are grouped only by stable references in S7.
3
+ /** A surviving scout seat with little output or evidence is weak. ponytail: thresholds are blunt on
4
+ * purpose; raise them only with benchmark evidence. */
5
+ export function detectWeakSeat(positions, mode) {
6
+ if (mode === 'quick')
7
+ return [];
8
+ const seats = new Map();
9
+ for (const position of positions) {
10
+ const provider = position.id.split('/')[0].replace(/-coverage-fill$/, '');
11
+ const seat = seats.get(provider) ?? { positions: 0, evidenced: 0 };
12
+ seat.positions += 1;
13
+ if (position.evidence_ids?.length)
14
+ seat.evidenced += 1;
15
+ seats.set(provider, seat);
16
+ }
17
+ if (seats.size < 2)
18
+ return [];
19
+ return [...seats].filter(([, seat]) => seat.positions < 3 || seat.evidenced / seat.positions < 0.5).map(([provider]) => provider);
20
+ }
3
21
  export function collectPositions(seats) {
4
22
  return seats.map((seat) => ({ provider: seat.provider, source_id: seat.sample ?? seat.provider, submission: seat.output }));
5
23
  }
@@ -4,6 +4,7 @@ import { compileDecisionGraph, coverageHoleQueue, positionId } from '../decision
4
4
  import { IdeaRoleOutputModel } from '../../schemas/index.js';
5
5
  import { isFatal } from '../context.js';
6
6
  import { jsonCall } from '../jsonStage.js';
7
+ import { detectWeakSeat } from './s6-positions.js';
7
8
  import { overlapCoefficient, tokenize } from '../cluster.js';
8
9
  const GROUP_SIMILARITY = 0.8;
9
10
  export function buildCoverageFillPrompt(task, holes) {
@@ -71,6 +72,8 @@ export async function s7DecisionGraph(ctx, submissions, rubric, task = '') {
71
72
  const completed = await fillCoverage(ctx, submissions, rubric, task);
72
73
  const groups = deterministicClaimGroups(completed);
73
74
  const graph = compileDecisionGraph(completed, rubric, groups);
75
+ if (detectWeakSeat(graph.positions, ctx.mode ?? 'council').length)
76
+ ctx.addFlag('weak_seat');
74
77
  await ctx.writer.writeJson('decision-graph', graph);
75
78
  return graph;
76
79
  }
@@ -4,6 +4,7 @@ import { selectEscalations } from '../decision-graph.js';
4
4
  import { ClaimVerificationSet } from '../../schemas/index.js';
5
5
  import { isFatal, StageError } from '../context.js';
6
6
  import { jsonCall } from '../jsonStage.js';
7
+ import { loadSkill } from '../skills.js';
7
8
  const S8_PROMPT = `ROLE: Independent verifier. Check each anonymous decision claim below against the
8
9
  provided evidence cards and deterministic calculation checks. Output ONLY JSON:
9
10
  {"verifications": [{"claim_id": "<G-id>", "status": "VERIFIED|PARTIAL|CONTRADICTED|UNVERIFIABLE",
@@ -12,8 +13,21 @@ provided evidence cards and deterministic calculation checks. Output ONLY JSON:
12
13
  VERIFIED = the proposition is supported. CONTRADICTED = it is refuted. PARTIAL = evidence is mixed or
13
14
  incomplete. UNVERIFIABLE = the supplied sources cannot decide. Cite only supplied evidence IDs. Model
14
15
  knowledge cannot verify a current, numeric, legal, medical, financial, market, or regulatory fact.
15
- Judge each item independently; no verdict quota. JSON only.
16
+ Judge each item independently; no verdict quota. JSON only.{{SKILL}}
16
17
  ITEMS: {{ITEMS_JSON}}`;
18
+ export function selectVerificationTargets(claims, max) {
19
+ return claims
20
+ .map((claim, index) => ({ claim, index }))
21
+ .sort((left, right) => Number(right.claim.nature === 'FACTUAL') - Number(left.claim.nature === 'FACTUAL') || left.index - right.index)
22
+ .slice(0, max)
23
+ .map(({ claim }) => claim);
24
+ }
25
+ export function selectVerificationEscalations(graph, max = 8) {
26
+ const candidates = selectEscalations(graph, { max: graph.claims.length });
27
+ const byId = new Map(candidates.map((candidate) => [candidate.claim_id, candidate]));
28
+ const claims = candidates.map((candidate) => graph.claims.find((claim) => claim.id === candidate.claim_id));
29
+ return selectVerificationTargets(claims, max).map((claim) => byId.get(claim.id));
30
+ }
17
31
  function evidenceIdsForClaim(graph, claimId) {
18
32
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
19
33
  const claim = graph.claims.find((item) => item.id === claimId);
@@ -25,12 +39,12 @@ function evidenceIdsForClaim(graph, claimId) {
25
39
  .flatMap((calculation) => calculation.inputs.flatMap((input) => input.evidence_ids));
26
40
  return new Set([...positionEvidence, ...calculationEvidence]);
27
41
  }
28
- function verifierInput(graph) {
42
+ function verifierInput(graph, skill = '') {
29
43
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
30
44
  const evidenceRefs = new Map(graph.evidence.map((evidence, index) => [`E${index + 1}`, evidence.id]));
31
45
  const aliasByEvidence = new Map([...evidenceRefs].map(([alias, id]) => [id, alias]));
32
46
  const evidenceById = new Map(graph.evidence.map((evidence) => [evidence.id, evidence]));
33
- const items = selectEscalations(graph, { max: 8 }).map((escalation) => {
47
+ const items = selectVerificationEscalations(graph).map((escalation) => {
34
48
  const claim = graph.claims.find((item) => item.id === escalation.claim_id);
35
49
  const linkedEvidenceIds = evidenceIdsForClaim(graph, claim.id);
36
50
  return {
@@ -64,13 +78,16 @@ function verifierInput(graph) {
64
78
  evidence_hole: graph.holes.evidence.find((hole) => hole.claim_id === claim.id)?.reason,
65
79
  };
66
80
  });
67
- return { prompt: S8_PROMPT.replace('{{ITEMS_JSON}}', JSON.stringify(items, null, 2)), evidenceRefs };
81
+ const prompt = S8_PROMPT
82
+ .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
83
+ .replace('{{ITEMS_JSON}}', JSON.stringify(items, null, 2));
84
+ return { prompt, evidenceRefs };
68
85
  }
69
- export function buildVerifierPrompt(graph) {
70
- return verifierInput(graph).prompt;
86
+ export function buildVerifierPrompt(graph, skill = '') {
87
+ return verifierInput(graph, skill).prompt;
71
88
  }
72
89
  /** Validate every S8 claim/evidence reference before the chair can see it. */
73
- export function claimVerificationRefIssues(graph, verifications, expectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id)) {
90
+ export function claimVerificationRefIssues(graph, verifications, expectedIds = selectVerificationEscalations(graph).map((item) => item.claim_id)) {
74
91
  const expected = new Set(expectedIds);
75
92
  const evidence = new Map(graph.evidence.map((item) => [item.id, item]));
76
93
  const seen = new Set();
@@ -111,7 +128,7 @@ export function claimVerificationRefIssues(graph, verifications, expectedIds = s
111
128
  return issues;
112
129
  }
113
130
  export async function s8Verify(ctx, graph) {
114
- const escalations = selectEscalations(graph, { max: 8 });
131
+ const escalations = selectVerificationEscalations(graph);
115
132
  if (escalations.length === 0) {
116
133
  const empty = { verifications: [] };
117
134
  await ctx.writer.writeJson('verifications', empty);
@@ -138,7 +155,7 @@ export async function s8Verify(ctx, graph) {
138
155
  return skipped;
139
156
  }
140
157
  try {
141
- const input = verifierInput(graph);
158
+ const input = verifierInput(graph, loadSkill('idea-refinement', 'verifier'));
142
159
  // Optional work gets no model repair: one logical optional call must remain one call.
143
160
  const result = await jsonCall(ctx, ctx.handle(ctx.roles.verifier), 'S8', input.prompt, ClaimVerificationSet, { repair: false });
144
161
  const translated = {
@@ -5,6 +5,7 @@ import { selectRebuttalEscalations, } from '../decision-graph.js';
5
5
  import { isFatal, StageError } from '../context.js';
6
6
  import { jsonCall } from '../jsonStage.js';
7
7
  import { IDEA_MODE_PLANS } from '../modes.js';
8
+ import { loadSkill } from '../skills.js';
8
9
  /** Internal protocol caps only. R6 owns exposing modes and changing the surrounding topology. */
9
10
  export const REBUTTAL_LIMITS = {
10
11
  quick: { maxNodes: 0, optionalCalls: 0 },
@@ -22,7 +23,7 @@ Output ONLY JSON:
22
23
  CONCEDE = your position changes. COUNTER = retain it using a cited supplied card or a precise logical
23
24
  rebuttal. NARROW = state the smaller proposition actually supported. UNRESOLVED = evidence cannot decide.
24
25
  Return exactly one event per assigned claim. Cite only evidence IDs shown for that claim. Do not invent
25
- sources, URLs, claim IDs, or evidence IDs. JSON only.
26
+ sources, URLs, claim IDs, or evidence IDs. JSON only.{{SKILL}}
26
27
  NODES: {{NODES_JSON}}`;
27
28
  function claimAuthors(graph, claimId) {
28
29
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
@@ -70,7 +71,7 @@ export function planRebuttalCalls(graph, escalations, scouts, judge) {
70
71
  return claim_ids?.length ? [{ provider, claim_ids }] : [];
71
72
  });
72
73
  }
73
- function rebuttalInput(graph, verifications, escalationById, assignment) {
74
+ function rebuttalInput(graph, verifications, escalationById, assignment, skill = '') {
74
75
  const claimIds = new Set(assignment.claim_ids);
75
76
  const positionRefs = new Map();
76
77
  const evidenceRefs = new Map();
@@ -130,11 +131,16 @@ function rebuttalInput(graph, verifications, escalationById, assignment) {
130
131
  };
131
132
  });
132
133
  return {
133
- prompt: REBUTTAL_PROMPT.replace('{{NODES_JSON}}', JSON.stringify(nodes, null, 2)),
134
+ prompt: buildRebuttalPrompt(nodes, skill),
134
135
  evidenceRefs,
135
136
  allowedEvidence,
136
137
  };
137
138
  }
139
+ export function buildRebuttalPrompt(nodes, skill = '') {
140
+ return REBUTTAL_PROMPT
141
+ .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
142
+ .replace('{{NODES_JSON}}', JSON.stringify(nodes, null, 2));
143
+ }
138
144
  function translateEvents(graph, assignment, input, output, startIndex) {
139
145
  const expected = new Set(assignment.claim_ids);
140
146
  const seen = new Set();
@@ -189,8 +195,9 @@ export async function s8bRebuttal(ctx, graph, verifications, mode = ctx.mode) {
189
195
  const assignments = planned.slice(0, optionalCalls);
190
196
  const escalationById = new Map(escalations.map((item) => [item.claim_id, item]));
191
197
  const events = [];
198
+ const skill = loadSkill('idea-refinement', 'rebuttal');
192
199
  for (const assignment of assignments) {
193
- const input = rebuttalInput(graph, verifications, escalationById, assignment);
200
+ const input = rebuttalInput(graph, verifications, escalationById, assignment, skill);
194
201
  try {
195
202
  const output = await jsonCall(ctx, ctx.handle(assignment.provider), `S8b-${assignment.provider}`, input.prompt, RebuttalResponseSet, { repair: false });
196
203
  events.push(...translateEvents(graph, assignment, input, output, events.length));