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
@@ -1,9 +1,12 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
3
  import { DISPLAY_NAME } from '../providers/types.js';
4
+ import { RoleOutput as RoleOutputSchema } from '../schemas/index.js';
4
5
  import { deriveAudit, deriveScorecard, mergeOpenQuestions } from '../orchestration/stages/s10-render.js';
6
+ import { renderDecisionDossierMarkdown } from '../orchestration/decision-dossier.js';
5
7
  import { IDEA_RUBRIC } from '../workflows/idea-refinement.js';
6
8
  import { listArtifacts, readJsonArtifact } from '../storage/runs-read.js';
9
+ import { adaptIdeaOutput, adaptLegacyDecisionGraph } from '../orchestration/legacy-idea-adapter.js';
7
10
  function providerName(id) {
8
11
  return id in DISPLAY_NAME ? DISPLAY_NAME[id] : id;
9
12
  }
@@ -23,9 +26,8 @@ function roleColumn(provider, role) {
23
26
  title: providerName(provider),
24
27
  lines: [
25
28
  `Strongest version: ${role.strongest_version}`,
26
- ...role.assumptions.map((a) => `${a.load_bearing ? 'Load-bearing' : 'Assumption'}: ${a.statement}`),
27
- ...role.attacks.map((a) => `Attack: ${a.argument}`),
28
- ...role.open_questions.map((q) => `Question: ${q}`),
29
+ ...role.positions.map((position) => `${position.load_bearing ? 'Load-bearing' : position.stance}: ${position.proposition}`),
30
+ ...role.decision_questions.map(({ question }) => `Question: ${question}`),
29
31
  ],
30
32
  };
31
33
  }
@@ -47,27 +49,28 @@ function codeReviewRows(map, judge) {
47
49
  ...map.single_reviewer.map((a) => row('single', a)),
48
50
  ];
49
51
  }
50
- function ideaRows(map, judge) {
52
+ function graphRows(graph, judge) {
51
53
  const rulings = judgeRulings(judge);
52
- return [
53
- ...map.consensus.map((c) => ({ kind: 'consensus', title: c.statement, detail: c.evidence ?? '', providers: c.providers })),
54
- ...map.contradictions.map((d) => ({
55
- kind: 'dispute',
56
- title: d.id,
57
- detail: d.attacks.map((a) => `${providerName(a.provider)}: ${a.argument}`).join(' · '),
58
- providers: d.attacks.map((a) => a.provider),
59
- ruling: rulings.get(d.id),
60
- })),
61
- ...map.unique.map((c) => ({ kind: 'unique', title: c.statement, detail: c.evidence ?? '', providers: c.providers })),
62
- ];
54
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
55
+ return graph.claims.map((claim) => {
56
+ const positions = claim.position_ids.map((id) => positionById.get(id));
57
+ const providers = [...new Set(positions.map((position) => position.provider))];
58
+ const kind = claim.state === 'DISAGREEMENT' ? 'dispute'
59
+ : claim.state === 'CONSENSUS' || claim.state === 'SHARED_CONCERN' ? 'consensus'
60
+ : 'unique';
61
+ return {
62
+ kind,
63
+ title: claim.proposition,
64
+ detail: positions.map((position) => `${providerName(position.provider)} ${position.stance}: ${position.reasoning}`).join(' · '),
65
+ providers,
66
+ ruling: rulings.get(claim.id),
67
+ };
68
+ });
63
69
  }
64
70
  const SEV_ORDER = { HIGH: 0, MED: 1, MEDIUM: 1, LOW: 2 };
65
71
  function sevRank(s) {
66
72
  return SEV_ORDER[s.toUpperCase()] ?? 1;
67
73
  }
68
- function maxSeverity(sevs) {
69
- return [...sevs].sort((a, b) => sevRank(a) - sevRank(b))[0] ?? 'MED';
70
- }
71
74
  function clip(s, n) {
72
75
  const t = s.trim();
73
76
  return t.length > n ? `${t.slice(0, n - 1).trimEnd()}…` : t;
@@ -107,20 +110,21 @@ function chairPhrase(ruling) {
107
110
  return 'the idea holds here';
108
111
  return 'left to you';
109
112
  }
110
- function ideaDebates(map, judge) {
111
- const claims = new Map([...map.consensus, ...map.unique].map((c) => [c.id, c]));
112
- const adj = new Map((judge?.adjudications ?? []).map((a) => [a.id, a]));
113
- return map.contradictions.map((d) => {
114
- const a = adj.get(d.id);
115
- const claimantProviders = [...new Set(d.claim_ids.flatMap((id) => claims.get(id)?.providers ?? []))];
116
- const attackerProviders = [...new Set(d.attacks.map((x) => x.provider))];
113
+ function graphDebates(graph, judge) {
114
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
115
+ const adjudication = new Map((judge?.adjudications ?? []).map((item) => [item.id, item]));
116
+ return graph.claims.filter((claim) => claim.state === 'DISAGREEMENT').map((claim) => {
117
+ const positions = claim.position_ids.map((id) => positionById.get(id));
118
+ const supporting = positions.filter((position) => position.stance === 'SUPPORT');
119
+ const opposing = positions.filter((position) => position.stance === 'OPPOSE');
120
+ const ruling = adjudication.get(claim.id);
117
121
  return {
118
- claim: d.claim_ids.map((id) => claims.get(id)?.statement ?? id).join(' / '),
119
- claimantProviders,
120
- attackerProviders,
121
- challenge: d.attacks.map((x) => `${providerName(x.provider)}: ${x.argument}`).join('\n\n'),
122
- chair: chairPhrase(a?.ruling),
123
- reasoning: a?.reasoning ?? '',
122
+ claim: claim.proposition,
123
+ claimantProviders: [...new Set(supporting.map((position) => position.provider))],
124
+ attackerProviders: [...new Set(opposing.map((position) => position.provider))],
125
+ challenge: opposing.map((position) => `${providerName(position.provider)}: ${position.reasoning}`).join('\n\n'),
126
+ chair: chairPhrase(ruling?.ruling),
127
+ reasoning: ruling?.reasoning ?? '',
124
128
  };
125
129
  });
126
130
  }
@@ -143,41 +147,50 @@ function receiptLines(meta) {
143
147
  .join(' · ');
144
148
  return [
145
149
  `Calls: ${meta.call_count ?? calls.length}/${meta.budget?.limit ?? '?'}`,
150
+ ...(meta.receipt ? [`Categories: discovery ${meta.receipt.discovery} · verification ${meta.receipt.verification} · repair ${meta.receipt.repair} · planning ${meta.receipt.planning}`] : []),
146
151
  `Per-provider: ${providerCounts}`,
147
152
  `Recorded model time: ${(ms / 1000).toFixed(1)}s`,
148
153
  profiles ? `Models: ${profiles}` : '',
149
154
  meta.flags?.length ? `Flags: ${meta.flags.join(', ')}` : '',
150
155
  ].filter(Boolean);
151
156
  }
152
- /** Turn the idea disagreement map + judge report into the human decision story. Deterministic. */
153
- function ideaNarrative(map, judge) {
154
- const claims = new Map();
155
- for (const c of [...map.consensus, ...map.unique])
156
- claims.set(c.id, c.statement);
157
- const adj = new Map((judge?.adjudications ?? []).map((a) => [a.id, a]));
158
- const disputes = map.contradictions.map((d) => {
159
- const a = adj.get(d.id);
160
- return {
161
- ruling: a?.ruling ?? '',
162
- assumption: d.claim_ids.map((id) => claims.get(id) ?? id).join(' '),
163
- challenge: d.attacks.map((x) => `${providerName(x.provider)}: ${x.argument}`).join('\n\n'),
164
- severity: maxSeverity(d.attacks.map((x) => x.severity)),
165
- reasoning: a?.reasoning ?? '',
166
- providers: d.attacks.map((x) => x.provider),
167
- };
157
+ function graphNarrative(graph, judge) {
158
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
159
+ const ruling = new Map((judge?.adjudications ?? []).map((item) => [item.id, item]));
160
+ const agreements = graph.claims
161
+ .filter((claim) => claim.state === 'CONSENSUS')
162
+ .map((claim) => ({
163
+ statement: claim.proposition,
164
+ providers: [...new Set(claim.position_ids.map((id) => positionById.get(id).provider))],
165
+ }));
166
+ const risks = graph.claims.flatMap((claim) => {
167
+ const decision = ruling.get(claim.id);
168
+ if (claim.state !== 'SHARED_CONCERN' && decision?.ruling !== 'UPHOLD')
169
+ return [];
170
+ const positions = claim.position_ids.map((id) => positionById.get(id));
171
+ return [{
172
+ assumption: claim.proposition,
173
+ severity: claim.sensitivity === 'DECISIVE' ? 'HIGH' : claim.sensitivity === 'MATERIAL' ? 'MED' : 'LOW',
174
+ challenge: positions.filter((position) => position.stance === 'OPPOSE').map((position) => `${providerName(position.provider)}: ${position.reasoning}`).join('\n\n'),
175
+ reasoning: decision?.reasoning ?? 'Multiple analysts independently raised this concern.',
176
+ providers: [...new Set(positions.map((position) => position.provider))],
177
+ }];
178
+ }).sort((a, b) => sevRank(a.severity) - sevRank(b.severity));
179
+ const defended = graph.claims.flatMap((claim) => {
180
+ const decision = ruling.get(claim.id);
181
+ if (decision?.ruling !== 'REJECT')
182
+ return [];
183
+ const positions = claim.position_ids.map((id) => positionById.get(id));
184
+ return [{
185
+ assumption: claim.proposition,
186
+ challenge: positions.filter((position) => position.stance === 'OPPOSE').map((position) => position.reasoning).join('\n\n'),
187
+ reasoning: decision.reasoning,
188
+ }];
168
189
  });
169
- const risks = disputes
170
- .filter((d) => d.ruling === 'UPHOLD')
171
- .sort((a, b) => sevRank(a.severity) - sevRank(b.severity))
172
- .map(({ assumption, severity, challenge, reasoning, providers }) => ({ assumption, severity, challenge, reasoning, providers }));
173
- const defended = disputes
174
- .filter((d) => d.ruling === 'REJECT')
175
- .map(({ assumption, challenge, reasoning }) => ({ assumption, challenge, reasoning }));
176
- const agreements = map.consensus.map((c) => ({ statement: c.statement, providers: c.providers }));
177
- const blindSpots = map.blind_spots ?? [];
190
+ const blindSpots = graph.holes.coverage.map((hole) => hole.label);
178
191
  const nextSteps = [
179
- ...risks.map((r) => `Pressure-test the assumption “${clip(r.assumption, 130)}” — the council found it doesn't hold as stated.`),
180
- ...blindSpots.map((b) => `Work out: ${b}.`),
192
+ ...risks.map((risk) => `Pressure-test “${clip(risk.assumption, 130)}”.`),
193
+ ...blindSpots.map((spot) => `Work out: ${spot}.`),
181
194
  ];
182
195
  return {
183
196
  signal: computeSignal(risks.length, agreements.length),
@@ -193,21 +206,25 @@ function ideaNarrative(map, judge) {
193
206
  async function loadRoleOutputs(dir) {
194
207
  const artifacts = await listArtifacts(dir);
195
208
  const roleFiles = artifacts.filter((f) => f.startsWith('04-role-outputs/') && f.endsWith('.json'));
196
- const roles = await Promise.all(roleFiles.map(async (file) => ({
197
- provider: basename(file, '.json'),
198
- role: JSON.parse(await readFile(join(dir, file), 'utf8')),
199
- })));
209
+ const roles = await Promise.all(roleFiles.map(async (file) => {
210
+ const raw = JSON.parse(await readFile(join(dir, file), 'utf8'));
211
+ const role = typeof raw === 'object' && raw !== null && 'workflow' in raw && raw.workflow === 'idea-refinement'
212
+ ? adaptIdeaOutput(raw)
213
+ : RoleOutputSchema.parse(raw);
214
+ return { provider: basename(file, '.json'), role };
215
+ }));
200
216
  return roles.sort((a, b) => providerName(a.provider).localeCompare(providerName(b.provider)));
201
217
  }
202
218
  export async function loadCouncilView(runId, dir) {
203
219
  const meta = await readJsonArtifact(dir, 'meta.json');
204
220
  if (!meta)
205
221
  return null;
206
- const [judge, actionPlan, roles, intent] = await Promise.all([
222
+ const [judge, actionPlan, roles, intent, decisionReport] = await Promise.all([
207
223
  readJsonArtifact(dir, '09-judge-report.json'),
208
224
  readJsonArtifact(dir, '09b-action-plan.json'),
209
225
  loadRoleOutputs(dir),
210
226
  readJsonArtifact(dir, '01-intent-contract.json'),
227
+ readJsonArtifact(dir, '10-decision-report.json'),
211
228
  ]);
212
229
  const columns = roles.map(({ provider, role }) => roleColumn(provider, role));
213
230
  const verdict = judge?.verdict ?? (meta.exit_status === 'ok' ? 'Run completed without a judge verdict artifact.' : `Run ${meta.exit_status}.`);
@@ -226,18 +243,21 @@ export async function loadCouncilView(runId, dir) {
226
243
  }
227
244
  }
228
245
  else {
229
- const map = await readJsonArtifact(dir, '07-disagreement-map.json');
230
- if (map) {
231
- rows = ideaRows(map, judge);
246
+ const storedGraph = await readJsonArtifact(dir, '07-decision-graph.json');
247
+ const legacyMap = storedGraph ? null : await readJsonArtifact(dir, '07-disagreement-map.json');
248
+ const graph = storedGraph ?? (legacyMap ? adaptLegacyDecisionGraph(legacyMap) : null);
249
+ if (graph) {
250
+ rows = graphRows(graph, judge);
232
251
  stats = [
233
- `${map.consensus.length} consensus`,
234
- `${map.contradictions.length} disputes`,
235
- `${map.unique.length} unique`,
236
- `${map.blind_spots.length} blind spots`,
252
+ `${graph.claims.filter((claim) => claim.state === 'CONSENSUS').length} consensus`,
253
+ `${graph.claims.filter((claim) => claim.state === 'SHARED_CONCERN').length} shared concerns`,
254
+ `${graph.claims.filter((claim) => claim.state === 'DISAGREEMENT').length} disputes`,
255
+ `${graph.claims.filter((claim) => claim.state === 'UNIQUE').length} unique`,
256
+ `${graph.holes.coverage.length} coverage holes`,
237
257
  ];
238
- narrative = ideaNarrative(map, judge);
258
+ narrative = graphNarrative(graph, judge);
239
259
  const reportV3 = Boolean(judge?.recommendation || actionPlan);
240
- if (actionPlan)
260
+ if (actionPlan && !('kind' in actionPlan))
241
261
  narrative.nextSteps = actionPlan.actions.map((a) => a.action);
242
262
  if (reportV3) {
243
263
  const ideaSeats = ideaSeatOutputs(roles);
@@ -245,9 +265,9 @@ export async function loadCouncilView(runId, dir) {
245
265
  ...narrative,
246
266
  recommendation: judge?.recommendation,
247
267
  conditions: judge?.conditions,
248
- scorecard: deriveScorecard(IDEA_RUBRIC, map),
249
- audit: judge ? deriveAudit(map, judge) : [],
250
- debates: ideaDebates(map, judge),
268
+ scorecard: deriveScorecard(IDEA_RUBRIC, graph),
269
+ audit: judge ? deriveAudit(graph, judge) : [],
270
+ debates: graphDebates(graph, judge),
251
271
  actionPlan: actionPlan ?? undefined,
252
272
  openQuestions: mergeOpenQuestions(ideaSeats),
253
273
  receipt: receiptLines(meta),
@@ -256,9 +276,17 @@ export async function loadCouncilView(runId, dir) {
256
276
  }
257
277
  }
258
278
  const moderator = meta.roles?.judge ? providerName(meta.roles.judge) : undefined;
279
+ const hydratedDecisionReport = decisionReport?.dossier ? {
280
+ ...decisionReport,
281
+ keyFindings: decisionReport.keyFindings?.length ? decisionReport.keyFindings : judge?.key_points ?? [decisionReport.verdict.primaryReason],
282
+ criticalUnknowns: decisionReport.criticalUnknowns?.length
283
+ ? decisionReport.criticalUnknowns
284
+ : (narrative.openQuestions ?? []).slice(0, 3),
285
+ } : undefined;
259
286
  return {
260
287
  runId,
261
288
  workflow: meta.workflow,
289
+ mode: meta.mode,
262
290
  verdict,
263
291
  keyPoints: judge?.key_points ?? [],
264
292
  confidence: judge?.confidence_notes ?? '',
@@ -268,8 +296,9 @@ export async function loadCouncilView(runId, dir) {
268
296
  stats,
269
297
  calls: `${meta.call_count}/${meta.budget.limit} provider calls`,
270
298
  flags: meta.flags ?? [],
271
- topic: intent?.task,
299
+ topic: intent?.task ?? decisionReport?.task.original,
272
300
  moderator,
301
+ ...(hydratedDecisionReport ? { decisionReport: hydratedDecisionReport } : {}),
273
302
  ...narrative,
274
303
  };
275
304
  }
@@ -310,6 +339,155 @@ function section(index, title, inner, delay, note = '') {
310
339
  ${inner}
311
340
  </section>`;
312
341
  }
342
+ function dossierTable(headers, rows) {
343
+ if (!rows.length)
344
+ return '<p class="muted">None recorded.</p>';
345
+ return `<div class="table-wrap"><table class="data-table"><thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join('')}</tr></thead><tbody>${rows
346
+ .map((row) => `<tr>${row.map((value) => `<td>${escapeHtml(value)}</td>`).join('')}</tr>`)
347
+ .join('')}</tbody></table></div>`;
348
+ }
349
+ function dossierRefs(ids) {
350
+ return ids.length ? ids.join(', ') : 'none recorded';
351
+ }
352
+ function dossierPct(value) {
353
+ return `${Math.round(value * 100)}%`;
354
+ }
355
+ function dossierCoverageLabel(value) {
356
+ return value >= 0.75 ? 'High' : value >= 0.5 ? 'Medium' : 'Low';
357
+ }
358
+ function dossierCouncilRead(report) {
359
+ if (report.mode === 'quick')
360
+ return 'One structured analyst produced this result; no council, consensus, or independent-verification claim is being made.';
361
+ const scouts = report.models.filter((model) => model.roles.includes('scout')).length;
362
+ if (report.disagreements.length === 0) {
363
+ return `${scouts || 'The'} independent scout ${scouts === 1 ? 'analysis produced' : 'analyses produced'} no genuine opposing claim. The chair had less contested material to resolve.`;
364
+ }
365
+ const resolved = report.disagreements.filter((item) => item.status === 'RESOLVED').length;
366
+ return `${report.disagreements.length} genuine disagreement${report.disagreements.length === 1 ? '' : 's'} reached the chair; ${resolved} ${resolved === 1 ? 'was' : 'were'} resolved.`;
367
+ }
368
+ function dossierWarning(report, flags) {
369
+ const active = flags.filter((flag) => report.flags.includes(flag));
370
+ return active.length ? `<div class="warns"><span class="warn">⚑ DEGRADED: ${escapeHtml(active.join(', '))}</span></div>` : '';
371
+ }
372
+ function renderDossierIdeaBody(report) {
373
+ const dossier = report.dossier;
374
+ const confidence = report.confidenceBreakdown;
375
+ const coverageLabel = dossierCoverageLabel(confidence.verificationCoverage);
376
+ const keyFindings = report.keyFindings?.length ? report.keyFindings : [dossier.recommendation.reason];
377
+ const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
378
+ const tone = dossier.recommendation.status === 'ACCEPTED' ? 'good'
379
+ : dossier.recommendation.status === 'REJECTED' ? 'risk' : 'caution';
380
+ const conditions = dossier.recommendation.conditions.length
381
+ ? `<details class="decision-details"><summary>Conditions and decision state</summary><div><p><strong>Internal state:</strong> ${escapeHtml(dossier.recommendation.status)}</p><ul>${dossier.recommendation.conditions.map((condition) => `<li>${escapeHtml(condition.text)} <code>${escapeHtml(dossierRefs(condition.claimIds))}</code></li>`).join('')}</ul></div></details>`
382
+ : '';
383
+ const startHere = dossier.experiments.actions[0]?.action ?? 'No executable next step was produced.';
384
+ const factLabels = ['Decisive result', 'Consequence', 'Supporting signal'];
385
+ const facts = keyFindings.slice(0, 3).map((finding, index) => `<article class="decision-fact"><span>${factLabels[index]}</span><p>${escapeHtml(finding)}</p></article>`).join('');
386
+ const snapshot = report.decisionSnapshot;
387
+ const decisiveNumbers = snapshot ? `<div class="decision-numbers">
388
+ <span class="section-eyebrow">Decisive numbers</span>
389
+ <div class="table-wrap"><table class="snapshot-table"><thead><tr><th>Metric</th><th>Value</th><th>What it means</th></tr></thead><tbody>${snapshot.decisiveNumbers.map((item) => `<tr><td>${escapeHtml(item.label)}</td><td class="number-value">${escapeHtml(item.value)}</td><td>${escapeHtml(item.meaning)} <code>${escapeHtml(dossierRefs(item.claimIds))}</code></td></tr>`).join('')}</tbody></table></div>
390
+ ${snapshot.payback ? `<div class="payback-result"><span>Payback · ${escapeHtml(snapshot.payback.status.replaceAll('_', ' '))}</span><strong>${escapeHtml(snapshot.payback.result)}</strong><p>${escapeHtml(snapshot.payback.basis)} <code>${escapeHtml(dossierRefs(snapshot.payback.claimIds))}</code></p></div>` : ''}
391
+ </div>` : '';
392
+ const optionComparison = snapshot ? `<div class="option-comparison">
393
+ <span class="section-eyebrow">Options at a glance</span>
394
+ <div class="table-wrap"><table class="snapshot-table"><thead><tr><th>Path</th><th>Commitment</th><th>Basis</th><th>Trade-off</th></tr></thead><tbody>${snapshot.options.map((option) => `<tr><td>${escapeHtml(option.label)}</td><td class="number-value">${escapeHtml(option.commitment)}</td><td><span class="basis-chip ${option.commitmentKind.toLowerCase()}">${escapeHtml(option.commitmentKind.replace('_', ' '))}</span></td><td>${escapeHtml(option.tradeoff)}${option.claimIds.length ? ` <code>${escapeHtml(dossierRefs(option.claimIds))}</code>` : ''}</td></tr>`).join('')}</tbody></table></div>
395
+ </div>` : '';
396
+ const tripwire = snapshot?.tripwire ? `<div class="tripwire"><span class="tag">Go/no-go tripwire</span><strong>${escapeHtml(snapshot.tripwire.metric)} · ${escapeHtml(snapshot.tripwire.threshold)}</strong><p>${escapeHtml(snapshot.tripwire.decisionRule)} <code>${escapeHtml(dossierRefs(snapshot.tripwire.claimIds))}</code></p></div>` : '';
397
+ const topCounterCase = dossier.counterCase.reasoning;
398
+ const unknowns = criticalUnknowns.length
399
+ ? `<ul class="critical-unknowns">${criticalUnknowns.map((unknown) => `<li>${escapeHtml(unknown)}</li>`).join('')}</ul>`
400
+ : '<p class="muted">No verdict-flipping unknown was recorded.</p>';
401
+ const recommendation = `<div class="verdict tone-${tone}">
402
+ <span class="pill">Council recommendation</span>
403
+ ${decisiveNumbers}
404
+ <p class="verdict-text">${escapeHtml(dossier.recommendation.summary)}</p>
405
+ <div class="evidence-coverage ${coverageLabel.toLowerCase()}">
406
+ <div><span class="fk">Evidence coverage</span><strong>${coverageLabel} · ${dossierPct(confidence.verificationCoverage)}</strong></div>
407
+ <div class="coverage-track"><span style="width:${Math.round(confidence.verificationCoverage * 100)}%"></span></div>
408
+ <p>${confidence.verificationCoverage < 0.5 ? 'Important inputs remain unchecked. Confirm them before committing.' : 'Most load-bearing claims received independent checking.'} This is not a probability that the recommendation is correct.</p>
409
+ </div>
410
+ ${snapshot ? '' : `<div class="decision-facts">${facts}</div>`}
411
+ <div class="action-callout"><span>Do this first</span><p>${escapeHtml(startHere)}</p></div>
412
+ ${optionComparison}
413
+ ${tripwire}
414
+ <div class="decision-safety">
415
+ <article><span class="tag">What could overturn this</span><p>${escapeHtml(topCounterCase)}</p></article>
416
+ <article><span class="tag">Critical unknowns</span>${unknowns}</article>
417
+ </div>
418
+ ${report.verdict.criticalWarning ? `<div class="critical-warning"><span class="tag">Critical warning</span><p>${escapeHtml(report.verdict.criticalWarning)}</p></div>` : ''}
419
+ <p class="council-read"><strong>Council read:</strong> ${escapeHtml(dossierCouncilRead(report))}</p>
420
+ ${conditions}
421
+ <div class="evidence-anchors"><span>Evidence anchors</span><code>${escapeHtml(dossierRefs(dossier.recommendation.claimIds))}</code></div>
422
+ ${dossierWarning(report, ['synthesis_suspect'])}
423
+ ${dossier.recommendation.claimIds.length ? '' : '<div class="warns"><span class="warn">⚑ DEGRADED: recommendation has no stored graph anchor</span></div>'}
424
+ </div>`;
425
+ const claimChain = dossierTable(['Claim ID', 'Claim', 'Ruling', 'Evidence status', 'Depends on'], dossier.claimChain.map((claim) => [claim.claimId, claim.text, claim.ruling, claim.evidenceStatus, dossierRefs(claim.dependsOn)]));
426
+ const evidence = `${dossierWarning(report, ['verification_skipped', 'research_ungrounded'])}${dossierTable(['Evidence ID', 'Source', 'Date', 'Freshness', 'Verification', 'Linked claims'], dossier.evidence.map((item) => [item.id, `${item.source} (${item.sourceKind})`, item.date, item.freshness, item.verificationStatus, dossierRefs(item.claimIds)]))}`;
427
+ const disagreementCards = report.disagreements.length
428
+ ? report.disagreements.map((item) => `<article class="card debate-card"><h3><code>${escapeHtml(item.id)}</code> ${escapeHtml(item.topic)}</h3>${item.sides.map((side) => `<div class="field"><span class="fk">${escapeHtml(side.stance)} · ${escapeHtml(side.providers.map(providerName).join(', '))}</span><p>${escapeHtml(side.reasoning.join(' '))}</p></div>`).join('')}<div class="field"><span class="fk">Ruling</span><p>${escapeHtml(item.status)} · ${escapeHtml(item.ruling)}</p></div>${item.reasoning ? `<div class="field"><span class="fk">Why</span><p>${escapeHtml(item.reasoning)}</p></div>` : ''}</article>`).join('')
429
+ : `<p class="muted">${report.mode === 'quick' ? 'No cross-model disagreement analysis runs in quick mode.' : 'No genuine disagreements were stored.'}</p>`;
430
+ const changes = dossier.positionChanges.length
431
+ ? dossierTable(['Event', 'Claim', 'Responder', 'Change', 'Evidence', 'Detail'], dossier.positionChanges.map((event) => [event.eventId, event.claimId, providerName(event.responder), event.response, dossierRefs(event.evidenceIds), event.narrowedProposition ?? event.reasoning]))
432
+ : '<p class="muted">No CONCEDE, COUNTER, or NARROW event was recorded.</p>';
433
+ const minority = `<h3>Minority report</h3>${report.minority.dissent.length
434
+ ? `<ul class="checks">${report.minority.dissent.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`
435
+ : '<p class="muted">No minority dissent was recorded.</p>'}
436
+ ${report.minority.uniqueOppositions.length ? `<ul class="checks">${report.minority.uniqueOppositions.map((item) => `<li><strong>${escapeHtml(providerName(item.provider))}</strong> uniquely opposed: ${escapeHtml(item.proposition)}</li>`).join('')}</ul>` : ''}
437
+ <p class="lede">Decision-blocking status: <strong>${escapeHtml(report.minority.blocksDecision)}</strong></p>`;
438
+ const disagreement = `${dossierWarning(report, ['single_model', 'low_diversity'])}${disagreementCards}<h3>Position changes</h3>${changes}${minority}`;
439
+ const shared = dossier.sharedConcerns.length
440
+ ? `<h3>Shared concerns</h3><ul class="agree">${dossier.sharedConcerns.map((item) => `<li><p><code>${escapeHtml(item.claimId)}</code> ${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(item.providerIds.map(providerName).join(', '))} · ${escapeHtml(item.evidenceStatus)}</span></li>`).join('')}</ul>`
441
+ : '<h3>Shared concerns</h3><p class="muted">None recorded.</p>';
442
+ const unique = dossier.uniqueSupportedInsights.length
443
+ ? `<h3>Unique supported insights</h3><ul class="agree">${dossier.uniqueSupportedInsights.map((item) => `<li><p><code>${escapeHtml(item.claimId)}</code> ${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(providerName(item.providerId))} · ${escapeHtml(item.verificationStatus)}</span></li>`).join('')}</ul>`
444
+ : '<h3>Unique supported insights</h3><p class="muted">None recorded.</p>';
445
+ const coverage = dossierTable(['Dimension', 'Status', 'Claim IDs'], dossier.coverage.map((item) => [item.label, item.status, dossierRefs(item.claimIds)]));
446
+ const sensitivity = dossierTable(['Claim ID', 'Fact', 'Sensitivity', 'If false', 'What would change it', 'Linked claims'], dossier.sensitivity.map((item) => [item.claimId, item.fact, item.sensitivity, item.impactIfFalse, item.whatWouldChangeIt, dossierRefs(item.linkedClaimIds)]));
447
+ const experiments = `${dossierWarning(report, ['plan_fallback', 'plan_skipped'])}${dossier.experiments.status === 'DEGRADED' ? `<div class="warns"><span class="warn">⚑ DEGRADED: ${escapeHtml(dossier.experiments.note)}</span></div>` : ''}${dossierTable(['#', 'Experiment', 'Why', 'Validates', 'Effort', 'Kill signal'], dossier.experiments.actions.map((action) => [String(action.order), action.action, action.why, action.validates, action.effort, action.killSignal]))}${dossier.experiments.actions.length ? `<p class="lede">${escapeHtml(dossier.experiments.note)}</p>` : ''}`;
448
+ const counterCase = dossier.counterCase.available
449
+ ? `<article class="card risk-card"><p>${escapeHtml(dossier.counterCase.reasoning)}</p><div class="field"><span class="fk">Evidence anchors</span><p><code>${escapeHtml(dossierRefs(dossier.counterCase.claimIds))}</code></p></div></article>`
450
+ : `<div class="warns"><span class="warn">⚑ DEGRADED: ${escapeHtml(dossier.counterCase.reasoning)}</span></div>`;
451
+ const contributions = `${dossierWarning(report, ['verification_skipped', 'single_model', 'low_diversity'])}<p class="lede">Only unique claims that survived independent verification receive credit.</p>${dossierTable(['Provider', 'Verified unique claim IDs', 'Count'], dossier.contributions.map((item) => [item.name, dossierRefs(item.verifiedUniqueClaimIds), String(item.verifiedUniqueClaimIds.length)]))}`;
452
+ const receipt = `<div class="receipt">
453
+ <span>mode ${escapeHtml(report.mode)}</span><span>${report.receipt.calls}/${report.receipt.budget} provider calls</span>
454
+ <span>discovery ${report.receipt.categories.discovery}</span><span>verification ${report.receipt.categories.verification}</span>
455
+ <span>repair ${report.receipt.categories.repair}</span><span>planning ${report.receipt.categories.planning}</span>
456
+ <span>by provider ${escapeHtml(Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none')}</span>
457
+ <span>model time ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s</span>
458
+ </div>${report.flags.length ? `<div class="warns">${report.flags.map((flag) => `<span class="warn">⚑ ${escapeHtml(flag)}</span>`).join('')}</div>` : '<p class="muted">No degradation flags.</p>'}`;
459
+ const risks = dossierTable(['Risk', 'Severity'], report.risks.map((item) => [item.risk, item.severity]));
460
+ const questions = report.openQuestions.length
461
+ ? `<ul class="checks">${report.openQuestions.map((question) => `<li>${escapeHtml(question)}</li>`).join('')}</ul>`
462
+ : '<p class="muted">No verdict-flipping open question was recorded.</p>';
463
+ const runDetails = `<article class="card">
464
+ <div class="field"><span class="fk">Report</span><p><code>${escapeHtml(report.reportId)}</code> · ${escapeHtml(report.generatedAt)}</p></div>
465
+ <div class="field"><span class="fk">Original task</span><p>${escapeHtml(report.task.original)}</p></div>
466
+ <div class="field"><span class="fk">Normalized question</span><p>${escapeHtml(report.task.normalized)}</p></div>
467
+ <div class="field"><span class="fk">Constraints</span><p>${escapeHtml(report.task.constraints.join('; ') || 'none recorded')}</p></div>
468
+ <div class="field"><span class="fk">Success criteria</span><p>${escapeHtml(report.task.successCriteria.join('; ') || 'none recorded')}</p></div>
469
+ <div class="field"><span class="fk">Models and roles</span><p>${escapeHtml(report.models.map((model) => `${model.name} (${model.roles.join(', ')})`).join(' · ') || 'none recorded')}</p></div>
470
+ <div class="field"><span class="fk">Structural score</span><p>${confidence.score}/100 (${escapeHtml(confidence.label)}) · heuristic, not benchmark-calibrated</p></div>
471
+ </article>${receipt}`;
472
+ const technical = `<details class="fold"><summary>Original submissions and graph events</summary><div class="fold-body">
473
+ <h4 class="fold-h">Original submissions</h4><ul>${dossier.technical.submissions.map((item) => `<li><strong>${escapeHtml(item.name)}:</strong> ${escapeHtml(item.strongestVersion)} <code>${escapeHtml(dossierRefs(item.positionIds))}</code></li>`).join('') || '<li>none</li>'}</ul>
474
+ <h4 class="fold-h">Original positions</h4><ul>${dossier.technical.positions.map((position) => `<li><code>${escapeHtml(position.id)}</code> [${escapeHtml(providerName(position.provider))} ${escapeHtml(position.stance)}] ${escapeHtml(position.proposition)} · evidence <code>${escapeHtml(dossierRefs(position.evidenceIds))}</code></li>`).join('') || '<li>none</li>'}</ul>
475
+ <h4 class="fold-h">Graph edges</h4><ul>${dossier.technical.edges.map((edge) => `<li><code>${escapeHtml(edge.from)} —${escapeHtml(edge.type)}→ ${escapeHtml(edge.to)}</code></li>`).join('') || '<li>none</li>'}</ul>
476
+ <h4 class="fold-h">Position-change events</h4><ul>${dossier.technical.events.map((event) => `<li><code>${escapeHtml(event.eventId)}</code>: ${escapeHtml(providerName(event.responder))} ${escapeHtml(event.response)} <code>${escapeHtml(event.claimId)}</code> — ${escapeHtml(event.reasoning)}</li>`).join('') || '<li>none</li>'}</ul>
477
+ </div></details>`;
478
+ return [
479
+ section('01', 'Decision', recommendation, 60),
480
+ section('02', 'Action plan', experiments, 100, 'The smallest useful test, its effort, and the signal that should stop the idea.'),
481
+ section('03', 'Why this decision', claimChain, 140, 'The load-bearing claim chain behind the recommendation.'),
482
+ section('04', 'What could change the decision', `<h3>Decision-sensitive facts</h3>${sensitivity}<h3>Strongest counter-case</h3>${counterCase}`, 180),
483
+ section('05', 'Evidence and verification', evidence, 220),
484
+ section('06', 'Risks, gaps, and open questions', `<h3>Risks</h3>${risks}<h3>Coverage</h3>${coverage}<h3>Open questions</h3>${questions}`, 260),
485
+ section('07', 'Disagreement and dissent', disagreement, 300),
486
+ section('08', 'What the council added', `${shared}${unique}<h3>Verified unique contributions</h3>${contributions}`, 340),
487
+ section('09', 'Run details', runDetails, 380),
488
+ section('10', 'Technical audit', technical, 420),
489
+ ].join('');
490
+ }
313
491
  function renderLegacyIdeaBody(view) {
314
492
  const risks = view.risks ?? [];
315
493
  const agreements = view.agreements ?? [];
@@ -412,9 +590,10 @@ function renderIdeaBody(view) {
412
590
  ${conditions}
413
591
  ${glance}
414
592
  </section>`;
593
+ const synthesisWarning = view.flags.includes('synthesis_suspect') ? '<div class="warns"><span class="warn">⚑ synthesis suspect</span></div>' : '';
415
594
  const chairman = view.keyPoints?.length
416
- ? section('01', "Chairman's reasoning", `<ul class="reasons">${view.keyPoints.map((p) => `<li>${escapeHtml(p)}</li>`).join('')}</ul>`, 100)
417
- : '';
595
+ ? section('01', "Chairman's reasoning", `${synthesisWarning}<ul class="reasons">${view.keyPoints.map((p) => `<li>${escapeHtml(p)}</li>`).join('')}</ul>`, 100)
596
+ : synthesisWarning ? section('01', "Chairman's reasoning", `${synthesisWarning}<p class="muted">No reliable chairman reasoning was produced.</p>`, 100) : '';
418
597
  const scorecard = view.scorecard?.length
419
598
  ? `<div class="score-grid">${view.scorecard.map((s) => `<div class="score ${s.status}"><span>${escapeHtml(s.label)}</span><strong>${escapeHtml(s.status)}</strong></div>`).join('')}</div>`
420
599
  : '';
@@ -440,11 +619,13 @@ function renderIdeaBody(view) {
440
619
  <div class="field"><span class="fk">Chair</span><p>${escapeHtml(d.chair)}${d.reasoning ? ` — ${escapeHtml(d.reasoning)}` : ''}</p></div>
441
620
  </article>`).join('')
442
621
  : '<p class="muted">No contradictions reached the chair.</p>';
443
- const plan = view.actionPlan
444
- ? `<div class="table-wrap"><table class="data-table plan-table"><thead><tr><th>#</th><th>Action</th><th>Why</th><th>Validates</th><th>Effort</th><th>Kill signal</th></tr></thead><tbody>
622
+ const plan = view.actionPlan && 'kind' in view.actionPlan
623
+ ? `<div class="card"><div class="warns"><span class="warn">⚑ ${view.flags.filter((f) => f === 'plan_fallback' || f === 'plan_skipped').map((f) => escapeHtml(f.replaceAll('_', ' '))).join(' · ')}</span></div><p><strong>Planner unavailable: ${escapeHtml(view.actionPlan.reason)}</strong></p><ul class="checks">${view.actionPlan.unresolved_questions.map((q) => `<li>${escapeHtml(q)}</li>`).join('')}</ul></div>`
624
+ : view.actionPlan
625
+ ? `<div class="table-wrap"><table class="data-table plan-table"><thead><tr><th>#</th><th>Action</th><th>Why</th><th>Validates</th><th>Effort</th><th>Kill signal</th></tr></thead><tbody>
445
626
  ${view.actionPlan.actions.map((a) => `<tr><td>${a.order}</td><td>${escapeHtml(a.action)}</td><td>${escapeHtml(a.why)}</td><td><code>${escapeHtml(a.validates)}</code></td><td>${escapeHtml(a.effort)}</td><td>${escapeHtml(a.kill_signal)}</td></tr>`).join('')}
446
627
  </tbody></table><p class="lede">${escapeHtml(view.actionPlan.sequencing_note)}</p></div>`
447
- : '<p class="muted">No validation plan artifact recorded.</p>';
628
+ : '<p class="muted">No validation plan artifact recorded.</p>';
448
629
  const questions = view.openQuestions?.length
449
630
  ? `<ul class="checks">${view.openQuestions.map((q) => `<li>${escapeHtml(q)}</li>`).join('')}</ul>`
450
631
  : '<p class="muted">No verdict-flipping open questions recorded.</p>';
@@ -478,15 +659,43 @@ function renderIdeaBody(view) {
478
659
  ${renderTechnical(view)}
479
660
  `;
480
661
  }
662
+ function renderQuickIdeaBody(view) {
663
+ const tone = recommendationTone(view.recommendation, view.signal?.tone ?? 'caution');
664
+ const label = recommendationLabel(view.recommendation, view.signal?.label ?? 'Quick analysis');
665
+ const conditions = view.conditions?.length
666
+ ? `<div class="conditions"><span class="fk">Conditions</span><ul>${view.conditions.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div>`
667
+ : '';
668
+ const hero = `<section class="verdict tone-${tone} reveal" style="animation-delay:60ms"><span class="pill">${escapeHtml(label)}</span><p class="verdict-text">${escapeHtml(view.verdict)}</p>${conditions}</section>`;
669
+ const reasoning = view.keyPoints.length
670
+ ? `<ul class="reasons">${view.keyPoints.map((point) => `<li>${escapeHtml(point)}</li>`).join('')}</ul>`
671
+ : '<p class="muted">No structured reasoning recorded.</p>';
672
+ const risks = view.risks?.length
673
+ ? `<ul class="checks">${view.risks.map((risk) => `<li>${escapeHtml(risk.assumption)} — ${escapeHtml(risk.challenge)}</li>`).join('')}</ul>`
674
+ : '<p class="muted">No load-bearing risk was supported strongly enough to list.</p>';
675
+ const plan = view.actionPlan && !('kind' in view.actionPlan)
676
+ ? `<ul class="checks">${view.actionPlan.actions.map((action) => `<li><strong>${escapeHtml(action.action)}</strong> — ${escapeHtml(action.kill_signal)}</li>`).join('')}</ul>`
677
+ : '<p class="muted">No executable plan was produced.</p>';
678
+ const analyst = view.columns.map((column) => `<article class="card model-card"><h3>${escapeHtml(column.title)}</h3><ul class="model-lines">${column.lines.map((line) => `<li>${escapeHtml(line)}</li>`).join('')}</ul></article>`).join('');
679
+ const receipt = view.receipt?.length ? `<div class="receipt">${view.receipt.map((line) => `<span>${escapeHtml(line)}</span>`).join('')}</div>` : '';
680
+ return `${hero}
681
+ ${section('01', 'Analyst reasoning', reasoning, 100, 'One structured analyst; no council or independent-consensus claim.')}
682
+ ${section('02', 'Load-bearing risks', risks, 180)}
683
+ ${section('03', 'Validation plan', plan, 240)}
684
+ ${receipt ? section('04', 'Receipt', receipt, 300) : ''}
685
+ ${section('05', 'Analyst output', `<div class="model-grid">${analyst}</div>`, 360)}`;
686
+ }
481
687
  /** Serialize a council view to clean Markdown — for the HTML "Copy" button (paste into a coding assistant). */
482
688
  function councilMarkdown(view) {
689
+ if (view.decisionReport?.dossier)
690
+ return renderDecisionDossierMarkdown(view.decisionReport);
483
691
  const isIdea = view.workflow !== 'code-review';
692
+ const quick = isIdea && view.mode === 'quick';
484
693
  const reportV3 = isIdea && Boolean(view.recommendation || view.actionPlan);
485
694
  const L = [];
486
695
  L.push(`# ${isIdea && view.topic ? cleanTopic(view.topic) : isIdea ? 'Idea refinement' : 'Code review'}`, '');
487
696
  const panel = view.columns.map((c) => c.title).join(', ');
488
697
  if (panel)
489
- L.push(`Panel: ${panel}${view.moderator ? ` · Chair: ${view.moderator}` : ''}`, '');
698
+ L.push(quick ? `Analyst: ${panel}` : `Panel: ${panel}${view.moderator ? ` · Chair: ${view.moderator}` : ''}`, '');
490
699
  if (view.recommendation) {
491
700
  L.push('## Bottom line', '', `**${recommendationLabel(view.recommendation, '')}** — ${view.verdict}`, '');
492
701
  if (view.conditions?.length) {
@@ -500,11 +709,16 @@ function councilMarkdown(view) {
500
709
  L.push('## Verdict', '', view.verdict, '');
501
710
  }
502
711
  if (view.keyPoints?.length) {
503
- L.push("## Chairman's reasoning", '');
712
+ L.push(quick ? '## Analyst reasoning' : "## Chairman's reasoning", '');
713
+ if (view.flags.includes('synthesis_suspect'))
714
+ L.push('⚠ synthesis_suspect — the chair output required deterministic repair or degradation handling.', '');
504
715
  for (const p of view.keyPoints)
505
716
  L.push(`- ${p}`);
506
717
  L.push('');
507
718
  }
719
+ else if (view.flags.includes('synthesis_suspect')) {
720
+ L.push("## Chairman's reasoning", '', '⚠ synthesis_suspect — no reliable chairman reasoning was produced.', '');
721
+ }
508
722
  if (isIdea) {
509
723
  if (reportV3 && view.scorecard?.length) {
510
724
  L.push('## Dimension scorecard (best-effort)', '');
@@ -537,14 +751,23 @@ function councilMarkdown(view) {
537
751
  }
538
752
  if (reportV3 && view.actionPlan) {
539
753
  L.push('## Validation plan', '');
540
- for (const a of view.actionPlan.actions) {
541
- L.push(`${a.order}. ${a.action}`);
542
- L.push(` - Why: ${a.why}`);
543
- L.push(` - Validates: ${a.validates}`);
544
- L.push(` - Effort: ${a.effort}`);
545
- L.push(` - Kill signal: ${a.kill_signal}`);
754
+ if ('kind' in view.actionPlan) {
755
+ const planFlags = view.flags.filter((flag) => flag === 'plan_fallback' || flag === 'plan_skipped');
756
+ L.push(`Planner unavailable: ${view.actionPlan.reason}${planFlags.length ? ` (${planFlags.join(', ')})` : ''}.`, '', 'Unresolved questions:');
757
+ for (const question of view.actionPlan.unresolved_questions)
758
+ L.push(`- ${question}`);
759
+ }
760
+ else {
761
+ for (const a of view.actionPlan.actions) {
762
+ L.push(`${a.order}. ${a.action}`);
763
+ L.push(` - Why: ${a.why}`);
764
+ L.push(` - Validates: ${a.validates}`);
765
+ L.push(` - Effort: ${a.effort}`);
766
+ L.push(` - Kill signal: ${a.kill_signal}`);
767
+ }
768
+ L.push('', view.actionPlan.sequencing_note);
546
769
  }
547
- L.push('', view.actionPlan.sequencing_note, '');
770
+ L.push('');
548
771
  }
549
772
  if (reportV3 && view.openQuestions?.length) {
550
773
  L.push('## Open questions that flip the verdict', '');
@@ -553,7 +776,7 @@ function councilMarkdown(view) {
553
776
  L.push('');
554
777
  }
555
778
  if (view.columns.length) {
556
- L.push('## How each model saw it', '');
779
+ L.push(quick ? '## Analyst output' : '## How each model saw it', '');
557
780
  for (const c of view.columns) {
558
781
  L.push(`### ${c.title}`);
559
782
  for (const l of c.lines.slice(0, 10))
@@ -640,7 +863,7 @@ function renderTechnical(view) {
640
863
  const dissent = view.dissent.length ? `<ul>${view.dissent.map((d) => `<li>${escapeHtml(d)}</li>`).join('')}</ul>` : '<p class="muted">None recorded.</p>';
641
864
  return `
642
865
  <details class="fold reveal" style="animation-delay:0ms">
643
- <summary>Full council analysis (technical)</summary>
866
+ <summary>${view.mode === 'quick' ? 'Full single-analyst output (technical)' : 'Full council analysis (technical)'}</summary>
644
867
  <div class="fold-body">
645
868
  <h4 class="fold-h">Each model, in its own words</h4>
646
869
  <div class="cols">${columns || '<p class="muted">No model output recorded.</p>'}</div>
@@ -653,18 +876,21 @@ function renderTechnical(view) {
653
876
  }
654
877
  export function renderCouncilHtml(view) {
655
878
  const isIdea = view.workflow !== 'code-review';
656
- const kicker = isIdea ? 'aiki · idea refinement' : 'aiki · code review';
879
+ const quick = isIdea && view.mode === 'quick';
880
+ const kicker = quick ? 'aiki · quick analysis' : isIdea ? 'aiki · idea refinement' : 'aiki · code review';
657
881
  const title = isIdea && view.topic ? cleanTopic(view.topic) : (isIdea ? 'Idea refinement' : 'Code review');
658
882
  const panel = view.columns.map((c) => c.title);
659
883
  const metaBits = [
660
- panel.length ? `Panel: ${panel.join(' · ')}` : '',
661
- view.moderator ? `Chair: ${view.moderator}` : '',
884
+ panel.length ? `${quick ? 'Analyst' : 'Panel'}: ${panel.join(' · ')}` : '',
885
+ !quick && view.moderator ? `Chair: ${view.moderator}` : '',
662
886
  view.calls,
663
887
  ].filter(Boolean);
664
888
  const flags = view.flags.length
665
889
  ? `<div class="warns">${view.flags.map((f) => `<span class="warn">⚑ ${escapeHtml(f.replaceAll('_', ' '))}</span>`).join('')}</div>`
666
890
  : '';
667
- const body = isIdea ? renderIdeaBody(view) : renderReviewBody(view);
891
+ const body = isIdea && view.decisionReport?.dossier
892
+ ? renderDossierIdeaBody(view.decisionReport)
893
+ : quick ? renderQuickIdeaBody(view) : isIdea ? renderIdeaBody(view) : renderReviewBody(view);
668
894
  // Embed the report as Markdown for the Copy button. Escape "<" so a "</script>" in content can't break out.
669
895
  const md = JSON.stringify(councilMarkdown(view)).replace(/</g, '\\u003c');
670
896
  return `<!doctype html>
@@ -672,44 +898,84 @@ export function renderCouncilHtml(view) {
672
898
  <head>
673
899
  <meta charset="utf-8">
674
900
  <meta name="viewport" content="width=device-width, initial-scale=1">
675
- <title>${escapeHtml(isIdea ? 'Idea refinement' : 'Code review')} — aiki council</title>
901
+ <title>${escapeHtml(quick ? 'Quick analysis' : isIdea ? 'Idea refinement' : 'Code review')} — aiki</title>
676
902
  <style>
677
903
  :root{
678
904
  color-scheme: light;
679
905
  --serif:"Iowan Old Style","Palatino Linotype",Palatino,Charter,Georgia,"Times New Roman",serif;
680
906
  --sans:-apple-system,BlinkMacSystemFont,"Avenir Next","Segoe UI",system-ui,sans-serif;
681
907
  --mono:"SF Mono","JetBrains Mono",ui-monospace,Menlo,Consolas,monospace;
682
- --paper:#ffffff; --panel:#f7f8fa; --ink:#1a1c1f; --soft:#5b6470; --faint:#9aa1ab; --line:#e6e8ec;
683
- --good:#15803d; --good-bg:#e9f7ef; --risk:#c0392b; --risk-bg:#fbeae7;
684
- --caution:#b45309; --caution-bg:#fdf2e2; --slate:#475569; --accent:#2563eb;
908
+ --paper:#fbfaf7; --panel:#f1efe9; --ink:#171a18; --soft:#59625e; --faint:#8b928e; --line:#dcded8;
909
+ --good:#176b52; --good-bg:#e5f3ed; --risk:#a63a30; --risk-bg:#f8e8e4;
910
+ --caution:#a96813; --caution-bg:#f8eedc; --slate:#4f5e59; --accent:#155f55;
685
911
  }
686
912
  *{box-sizing:border-box;}
687
913
  html{-webkit-text-size-adjust:100%;}
688
914
  body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.6;}
689
- main{max-width:800px;margin:0 auto;padding:34px 24px 90px;}
915
+ main{max-width:880px;margin:0 auto;padding:34px 24px 90px;}
690
916
  a{color:var(--accent);}
691
- h1,h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
917
+ h1{font-family:var(--serif);font-weight:600;letter-spacing:-.025em;}
918
+ h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
692
919
  p{margin:0 0 .6em;}
693
920
 
694
921
  /* masthead */
695
- .mast{border-bottom:2px solid var(--ink);padding-bottom:22px;margin-bottom:30px;}
922
+ .mast{border-bottom:1px solid var(--ink);padding-bottom:22px;margin-bottom:30px;}
696
923
  .kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);}
697
- .mast h1{font-size:clamp(28px,4.6vw,42px);line-height:1.12;margin:12px 0 16px;}
924
+ .mast h1{font-size:clamp(32px,5.4vw,50px);line-height:1.05;margin:12px 0 18px;max-width:18ch;}
698
925
  .mmeta{display:flex;flex-wrap:wrap;gap:7px;}
699
926
  .mmeta span{font-family:var(--mono);font-size:11.5px;color:var(--soft);border:1px solid var(--line);background:var(--panel);border-radius:100px;padding:3px 10px;}
700
927
  .warns{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;}
701
928
  .warn{font-family:var(--mono);font-size:11.5px;color:var(--caution);background:var(--caution-bg);border:1px solid #e6d09b;border-radius:6px;padding:3px 9px;}
702
929
 
703
930
  /* verdict hero */
704
- .verdict{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:14px;
705
- padding:26px 28px 22px;margin-bottom:18px;overflow:hidden;}
931
+ .verdict{position:relative;background:#fff;border:1px solid var(--line);border-radius:18px;
932
+ padding:30px 32px 26px;margin-bottom:18px;overflow:hidden;box-shadow:0 18px 50px rgba(32,45,40,.07);}
706
933
  .verdict::before{content:"";position:absolute;left:0;top:0;bottom:0;width:6px;}
707
934
  .tone-good::before{background:var(--good);} .tone-caution::before{background:var(--caution);} .tone-risk::before{background:var(--risk);}
708
935
  .pill{display:inline-block;font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.02em;
709
936
  padding:5px 13px;border-radius:100px;margin-bottom:14px;}
710
937
  .tone-good .pill{background:var(--good-bg);color:var(--good);} .tone-caution .pill{background:var(--caution-bg);color:var(--caution);}
711
938
  .tone-risk .pill{background:var(--risk-bg);color:var(--risk);}
712
- .verdict-text{font-family:var(--sans);font-weight:520;font-size:clamp(18px,2.3vw,21px);line-height:1.5;color:var(--ink);margin:0;}
939
+ .verdict-text{font-family:var(--serif);font-weight:600;font-size:clamp(22px,3vw,29px);line-height:1.32;color:var(--ink);margin:0;max-width:28ch;}
940
+ .section-eyebrow{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--accent);margin-bottom:9px;}
941
+ .decision-numbers{margin:4px 0 22px;}.snapshot-table{width:100%;border-collapse:collapse;font-size:13.5px;line-height:1.4;}
942
+ .snapshot-table th{padding:8px 10px;text-align:left;font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);border-bottom:1px solid var(--line);}
943
+ .snapshot-table td{padding:11px 10px;vertical-align:top;border-bottom:1px solid var(--line);}.snapshot-table tbody tr:last-child td{border-bottom:0;}
944
+ .snapshot-table .number-value{font-family:var(--serif);font-size:17px;font-weight:700;color:var(--ink);white-space:nowrap;}
945
+ .snapshot-table code,.payback-result code,.tripwire code{font-size:9.5px;color:var(--faint);white-space:nowrap;}
946
+ .payback-result{display:grid;grid-template-columns:auto 1fr;gap:3px 16px;align-items:baseline;margin-top:10px;padding:14px 16px;background:var(--paper);border:1px solid var(--line);border-radius:10px;}
947
+ .payback-result span{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);}.payback-result strong{font-family:var(--serif);font-size:18px;}.payback-result p{grid-column:1/-1;margin:2px 0 0;font-size:12.5px;color:var(--soft);}
948
+ .evidence-coverage{margin:24px 0 18px;padding:14px 16px;background:var(--paper);border:1px solid var(--line);border-radius:12px;}
949
+ .evidence-coverage>div:first-child{display:flex;align-items:end;justify-content:space-between;gap:12px;}
950
+ .evidence-coverage strong{font-family:var(--mono);font-size:13px;color:var(--ink);}
951
+ .coverage-track{height:5px;margin:10px 0;border-radius:99px;background:var(--line);overflow:hidden;}
952
+ .coverage-track span{display:block;height:100%;border-radius:inherit;background:var(--accent);}
953
+ .evidence-coverage.low .coverage-track span{background:var(--risk);}.evidence-coverage.medium .coverage-track span{background:var(--caution);}
954
+ .evidence-coverage p{font-size:13.5px;color:var(--soft);margin:0;}
955
+ .decision-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin:18px 0;}
956
+ .decision-fact{padding:16px;background:var(--paper);border:1px solid var(--line);border-radius:12px;}
957
+ .decision-fact span,.action-callout>span{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--accent);margin-bottom:7px;}
958
+ .decision-fact p{margin:0;font-size:14.5px;line-height:1.5;}
959
+ .action-callout{position:relative;margin:20px 0;padding:20px 22px 20px 26px;background:var(--ink);color:var(--paper);border-radius:12px;}
960
+ .action-callout::before{content:"→";position:absolute;right:20px;top:12px;font-family:var(--serif);font-size:34px;color:#8fc7b9;}
961
+ .action-callout>span{color:#8fc7b9;}.action-callout p{font-size:17px;line-height:1.5;margin:0;padding-right:34px;}
962
+ .option-comparison{margin:22px 0;}.basis-chip{display:inline-block;padding:3px 7px;border-radius:99px;background:var(--good-bg);color:var(--good);font-family:var(--mono);font-size:9px;letter-spacing:.04em;white-space:nowrap;}
963
+ .basis-chip.target_cap{background:var(--caution-bg);color:var(--caution);}.basis-chip.unknown{background:var(--risk-bg);color:var(--risk);}
964
+ .tripwire{margin:14px 0;padding:16px 18px;border:1px solid #b9d5cd;border-left:4px solid var(--accent);border-radius:10px;background:#f2f8f6;}
965
+ .tripwire .tag{display:block;color:var(--accent);margin-bottom:6px;}.tripwire strong{display:block;font-family:var(--serif);font-size:19px;}.tripwire p{margin:5px 0 0;font-size:13.5px;color:var(--soft);}
966
+ .decision-safety{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:18px;}
967
+ .decision-safety article{padding:17px 18px;background:var(--paper);border:1px solid var(--line);border-radius:12px;}
968
+ .decision-safety article:first-child{border-left:4px solid var(--risk);}
969
+ .decision-safety p{font-size:14px;margin:0;}
970
+ .critical-unknowns{margin:0;padding-left:18px;}.critical-unknowns li{font-size:13.5px;margin:0 0 6px;}
971
+ .critical-warning{margin-top:12px;padding:14px 16px;background:var(--risk-bg);border:1px solid #e1b8b1;border-radius:10px;}
972
+ .critical-warning .tag{color:var(--risk);}.critical-warning p{font-size:13.5px;margin:0;}
973
+ .council-read{margin:15px 0 0;font-size:13.5px;color:var(--soft);}
974
+ .decision-details{margin-top:14px;border-top:1px solid var(--line);padding-top:12px;}
975
+ .decision-details summary{cursor:pointer;font-family:var(--mono);font-size:11px;color:var(--soft);text-transform:uppercase;letter-spacing:.08em;}
976
+ .decision-details>div{padding-top:10px;font-size:13.5px;}.decision-details ul{margin:8px 0;padding-left:18px;}
977
+ .evidence-anchors{display:flex;gap:10px;align-items:center;margin-top:12px;font-family:var(--mono);font-size:10.5px;color:var(--faint);}
978
+ .evidence-anchors span{text-transform:uppercase;letter-spacing:.1em;}
713
979
 
714
980
  /* top action bar + copy */
715
981
  .bar{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;gap:12px;
@@ -738,6 +1004,7 @@ p{margin:0 0 .6em;}
738
1004
 
739
1005
  /* bottom line */
740
1006
  .bottomline{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:34px;}
1007
+ .verdict .bottomline{margin:18px 0 0;}
741
1008
  .bottomline > div{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;}
742
1009
  .tag{display:inline-block;font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin-bottom:7px;}
743
1010
  .bottomline p{margin:0;font-size:15px;color:var(--ink);}
@@ -831,7 +1098,9 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
831
1098
  @keyframes rise{to{opacity:1;transform:none;}}
832
1099
  @media (max-width:640px){
833
1100
  main{padding:34px 16px 60px;}
834
- .glance,.bottomline,.checks,.redteam{grid-template-columns:1fr;}
1101
+ .verdict{padding:24px 20px 20px;}
1102
+ .glance,.bottomline,.checks,.redteam,.decision-safety{grid-template-columns:1fr;}
1103
+ .snapshot-table{min-width:560px;}.payback-result{grid-template-columns:1fr;}.payback-result p{grid-column:1;}
835
1104
  }
836
1105
  @media (prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none;animation:none;}}
837
1106
  @media print{.reveal{opacity:1;transform:none;animation:none;}.fold[open]{break-inside:avoid;}}
@@ -849,7 +1118,7 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
849
1118
  ${flags}
850
1119
  </header>
851
1120
  ${body}
852
- <footer>Generated by aiki · ${escapeHtml(view.runId)} · a local model council. This is analysis, not advice — verify before acting.</footer>
1121
+ <footer>Generated by aiki · ${escapeHtml(view.runId)} · ${quick ? 'single-model quick analysis' : 'a local model council'}. This is analysis, not advice — verify before acting.</footer>
853
1122
  </main>
854
1123
  <script>
855
1124
  const REPORT_MD = ${md};