aiki-cli 0.2.2 → 0.3.1

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 (60) hide show
  1. package/CHANGELOG.md +113 -6
  2. package/README.md +119 -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 +66 -8
  17. package/dist/council/view.js +446 -117
  18. package/dist/orchestration/calculations.js +97 -0
  19. package/dist/orchestration/context.js +37 -9
  20. package/dist/orchestration/decision-dossier.js +320 -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 +39 -0
  28. package/dist/orchestration/preflight.js +203 -0
  29. package/dist/orchestration/quick-analysis.js +93 -0
  30. package/dist/orchestration/stages/cr-ladder.js +80 -0
  31. package/dist/orchestration/stages/s10-render.js +574 -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 +118 -85
  40. package/dist/orchestration/url-sources.js +200 -0
  41. package/dist/providers/codex.js +2 -1
  42. package/dist/providers/spawn.js +5 -0
  43. package/dist/schemas/index.js +638 -15
  44. package/dist/skills/idea-refinement/analyst.md +18 -14
  45. package/dist/skills/idea-refinement/economics-delivery.md +7 -0
  46. package/dist/skills/idea-refinement/market-adoption.md +7 -0
  47. package/dist/storage/runs.js +12 -4
  48. package/dist/tui/app.js +53 -14
  49. package/dist/tui/format.js +4 -5
  50. package/dist/tui/smart-entry.js +17 -1
  51. package/dist/tui/timeline.js +2 -4
  52. package/dist/workflows/code-review.js +4 -2
  53. package/dist/workflows/idea-refinement.js +118 -46
  54. package/package.json +12 -4
  55. package/dist/orchestration/stages/s0-grill.js +0 -79
  56. package/dist/orchestration/stages/s1-intent.js +0 -25
  57. package/dist/orchestration/stages/s2-misread.js +0 -76
  58. package/dist/orchestration/stages/s3-prompts.js +0 -55
  59. package/dist/orchestration/stages/s6-claims.js +0 -56
  60. 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 { readerClaimLabel, readerClaimRefs, renderDecisionDossierMarkdown, stripReaderClaimIds } 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,169 @@ 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)}</li>`).join('')}</ul></div></details>`
382
+ : '';
383
+ const startHere = dossier.experiments.actions[0]?.action ?? 'No executable next step was produced.';
384
+ const recommendationLead = stripReaderClaimIds(dossier.recommendation.reason);
385
+ const recommendationDetail = stripReaderClaimIds(dossier.recommendation.summary);
386
+ const factLabels = ['Decisive result', 'Consequence', 'Supporting signal'];
387
+ const facts = keyFindings.slice(0, 3).map((finding, index) => `<article class="decision-fact"><span>${factLabels[index]}</span><p>${escapeHtml(finding)}</p></article>`).join('');
388
+ const snapshot = report.decisionSnapshot;
389
+ const decisiveNumbers = snapshot ? `<div class="decision-numbers">
390
+ <span class="section-eyebrow">Decisive numbers</span>
391
+ <div class="table-wrap"><table class="snapshot-table decisive-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)}</td></tr>`).join('')}</tbody></table></div>
392
+ ${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)}</p></div>` : ''}
393
+ </div>` : '';
394
+ const optionComparison = snapshot ? `<div class="option-comparison">
395
+ <span class="section-eyebrow">Options at a glance</span>
396
+ <div class="table-wrap"><table class="snapshot-table options-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)}</td></tr>`).join('')}</tbody></table></div>
397
+ </div>` : '';
398
+ 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)}</p></div>` : '';
399
+ const topCounterCase = dossier.counterCase.reasoning;
400
+ const unknowns = criticalUnknowns.length
401
+ ? `<ul class="critical-unknowns">${criticalUnknowns.map((unknown) => `<li>${escapeHtml(unknown)}</li>`).join('')}</ul>`
402
+ : '<p class="muted">No verdict-flipping unknown was recorded.</p>';
403
+ const recommendation = `<div class="verdict tone-${tone}">
404
+ <div class="decision-status"><span class="pill">Council recommendation</span><span>${escapeHtml(dossier.recommendation.status.replaceAll('_', ' '))}</span></div>
405
+ ${decisiveNumbers}
406
+ <p class="verdict-text">${escapeHtml(recommendationLead)}</p>
407
+ <p class="verdict-detail">${escapeHtml(recommendationDetail)}</p>
408
+ <div class="evidence-coverage ${coverageLabel.toLowerCase()}">
409
+ <div><span class="fk">Evidence coverage</span><strong>${coverageLabel} · ${dossierPct(confidence.verificationCoverage)}</strong></div>
410
+ <div class="coverage-track"><span style="width:${Math.round(confidence.verificationCoverage * 100)}%"></span></div>
411
+ <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>
412
+ </div>
413
+ ${snapshot ? '' : `<div class="decision-facts">${facts}</div>`}
414
+ <div class="action-callout"><span>Do this first</span><p>${escapeHtml(startHere)}</p></div>
415
+ ${optionComparison}
416
+ ${tripwire}
417
+ <div class="decision-safety">
418
+ <article><span class="tag">What could overturn this</span><p>${escapeHtml(topCounterCase)}</p></article>
419
+ <article><span class="tag">Critical unknowns</span>${unknowns}</article>
420
+ </div>
421
+ ${report.verdict.criticalWarning ? `<div class="critical-warning"><span class="tag">Critical warning</span><p>${escapeHtml(report.verdict.criticalWarning)}</p></div>` : ''}
422
+ <p class="council-read"><strong>Council read:</strong> ${escapeHtml(dossierCouncilRead(report))}</p>
423
+ ${conditions}
424
+ ${dossier.recommendation.claimIds.length ? `<details class="decision-evidence"><summary>Evidence behind this recommendation (${dossier.recommendation.claimIds.length})</summary><ul>${dossier.recommendation.claimIds.map((id) => `<li>${escapeHtml(readerClaimLabel(report, id))}</li>`).join('')}</ul></details>` : ''}
425
+ ${dossierWarning(report, ['synthesis_suspect'])}
426
+ ${dossier.recommendation.claimIds.length ? '' : '<div class="warns"><span class="warn">⚑ DEGRADED: recommendation has no stored graph anchor</span></div>'}
427
+ </div>`;
428
+ const claimChain = dossierTable(['Claim', 'Ruling', 'Evidence status', 'Depends on'], dossier.claimChain.map((claim) => [claim.text, claim.ruling, claim.evidenceStatus, readerClaimRefs(report, claim.dependsOn)]));
429
+ 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, readerClaimRefs(report, item.claimIds)]))}`;
430
+ const disagreementCards = report.disagreements.length
431
+ ? report.disagreements.map((item) => `<article class="card debate-card"><h3>${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('')
432
+ : `<p class="muted">${report.mode === 'quick' ? 'No cross-model disagreement analysis runs in quick mode.' : 'No genuine disagreements were stored.'}</p>`;
433
+ const changes = dossier.positionChanges.length
434
+ ? dossierTable(['Event', 'Claim', 'Responder', 'Change', 'Evidence', 'Detail'], dossier.positionChanges.map((event) => [event.eventId, readerClaimLabel(report, event.claimId), providerName(event.responder), event.response, dossierRefs(event.evidenceIds), event.narrowedProposition ?? event.reasoning]))
435
+ : '<p class="muted">No CONCEDE, COUNTER, or NARROW event was recorded.</p>';
436
+ const minority = `<h3>Minority report</h3>${report.minority.dissent.length
437
+ ? `<ul class="checks">${report.minority.dissent.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`
438
+ : '<p class="muted">No minority dissent was recorded.</p>'}
439
+ ${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>` : ''}
440
+ <p class="lede">Decision-blocking status: <strong>${escapeHtml(report.minority.blocksDecision)}</strong></p>`;
441
+ const disagreement = `${dossierWarning(report, ['single_model', 'low_diversity'])}${disagreementCards}<h3>Position changes</h3>${changes}${minority}`;
442
+ const shared = dossier.sharedConcerns.length
443
+ ? `<h3>Shared concerns</h3><ul class="agree">${dossier.sharedConcerns.map((item) => `<li><p>${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(item.providerIds.map(providerName).join(', '))} · ${escapeHtml(item.evidenceStatus)}</span></li>`).join('')}</ul>`
444
+ : '<h3>Shared concerns</h3><p class="muted">None recorded.</p>';
445
+ const unique = dossier.uniqueSupportedInsights.length
446
+ ? `<h3>Unique supported insights</h3><ul class="agree">${dossier.uniqueSupportedInsights.map((item) => `<li><p>${escapeHtml(item.text)}</p><span class="who-names">${escapeHtml(providerName(item.providerId))} · ${escapeHtml(item.verificationStatus)}</span></li>`).join('')}</ul>`
447
+ : '<h3>Unique supported insights</h3><p class="muted">None recorded.</p>';
448
+ const coverage = dossierTable(['Dimension', 'Status', 'Related claims'], dossier.coverage.map((item) => [item.label, item.status, readerClaimRefs(report, item.claimIds)]));
449
+ const sensitivity = dossierTable(['Fact', 'Sensitivity', 'If false', 'What would change it', 'Linked claims'], dossier.sensitivity.map((item) => [item.fact, item.sensitivity, item.impactIfFalse, item.whatWouldChangeIt, readerClaimRefs(report, item.linkedClaimIds)]));
450
+ 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>` : ''}${dossier.experiments.actions.length ? `<div class="experiment-list">${dossier.experiments.actions.map((action) => `<article class="experiment-card"><div class="experiment-head"><span>${String(action.order).padStart(2, '0')}</span><h4>${escapeHtml(action.action)}</h4><strong>${escapeHtml(action.effort)}</strong></div><p>${escapeHtml(action.why)}</p><dl><div><dt>Validates</dt><dd>${escapeHtml(readerClaimLabel(report, action.validates))}</dd></div><div><dt>Stop if</dt><dd>${escapeHtml(action.killSignal)}</dd></div></dl></article>`).join('')}</div><p class="lede">${escapeHtml(dossier.experiments.note)}</p>` : '<p class="muted">No executable experiment was produced.</p>'}`;
451
+ const missingDeliverables = (dossier.missingRequestedOutputs ?? []).length
452
+ ? `<div class="warns"><span class="warn">⚑ DEGRADED: requested output missing: ${escapeHtml(dossier.missingRequestedOutputs.join(', '))}</span></div>`
453
+ : '';
454
+ const backlog = dossier.featureBacklog;
455
+ const featurePriorities = backlog ? `<h3>Feature priorities</h3><div class="feature-groups">${[
456
+ ['MUST', 'Build for the first usable release', backlog.must],
457
+ ['SHOULD', 'Add after the core path is stable', backlog.should],
458
+ ['LATER', 'Keep out of the current build', backlog.later],
459
+ ].filter(([, , items]) => items.length).map(([priority, description, items]) => `<section class="feature-group priority-${priority.toLowerCase()}"><header><div><span>${priority}</span><h4>${escapeHtml(description)}</h4></div><strong>${items.length}</strong></header><ul>${items.map((item) => `<li><div class="feature-title"><strong>${escapeHtml(item.feature)}</strong><span>${escapeHtml(item.effort)}</span></div><p>${escapeHtml(item.user_value)}</p><small>${escapeHtml(item.rationale)}</small></li>`).join('')}</ul></section>`).join('')}</div>${backlog.wont.length ? `<details class="inline-fold"><summary>Not in this scope (${backlog.wont.length})</summary><ul>${backlog.wont.map((item) => `<li><strong>${escapeHtml(item.feature)}</strong> — ${escapeHtml(item.reason)}</li>`).join('')}</ul></details>` : ''}` : '';
460
+ const implementationPlan = dossier.implementationPlan ? `<h3>Implementation plan</h3><ol class="milestone-list">${dossier.implementationPlan.milestones.map((milestone) => `<li><div class="milestone-marker"><span>${String(milestone.order).padStart(2, '0')}</span><small>${escapeHtml(milestone.timebox)}</small></div><article><h4>${escapeHtml(milestone.outcome)}</h4><ul>${milestone.tasks.map((task) => `<li>${escapeHtml(task)}</li>`).join('')}</ul><div class="acceptance"><span>Done when</span><p>${escapeHtml(milestone.acceptance_test)}</p></div></article></li>`).join('')}</ol>` : '';
461
+ const actionPlan = `${missingDeliverables}${featurePriorities}${implementationPlan}<h3>Validation plan</h3>${experiments}`;
462
+ const counterCase = dossier.counterCase.available
463
+ ? `<article class="card risk-card"><p>${escapeHtml(dossier.counterCase.reasoning)}</p><div class="field"><span class="fk">Evidence behind this counter-case</span><p>${escapeHtml(readerClaimRefs(report, dossier.counterCase.claimIds))}</p></div></article>`
464
+ : `<div class="warns"><span class="warn">⚑ DEGRADED: ${escapeHtml(dossier.counterCase.reasoning)}</span></div>`;
465
+ 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 contributions', 'Count'], dossier.contributions.map((item) => [item.name, readerClaimRefs(report, item.verifiedUniqueClaimIds), String(item.verifiedUniqueClaimIds.length)]))}`;
466
+ const receipt = `<div class="receipt">
467
+ <span>mode ${escapeHtml(report.mode)}</span><span>${report.receipt.calls}/${report.receipt.budget} provider calls</span>
468
+ <span>discovery ${report.receipt.categories.discovery}</span><span>verification ${report.receipt.categories.verification}</span>
469
+ <span>repair ${report.receipt.categories.repair}</span><span>planning ${report.receipt.categories.planning}</span>
470
+ <span>by provider ${escapeHtml(Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none')}</span>
471
+ <span>model time ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s</span>
472
+ </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>'}`;
473
+ const risks = dossierTable(['Risk', 'Severity'], report.risks.map((item) => [item.risk, item.severity]));
474
+ const questions = report.openQuestions.length
475
+ ? `<ul class="checks">${report.openQuestions.map((question) => `<li>${escapeHtml(question)}</li>`).join('')}</ul>`
476
+ : '<p class="muted">No verdict-flipping open question was recorded.</p>';
477
+ const runDetails = `<article class="card">
478
+ <div class="field"><span class="fk">Report</span><p><code>${escapeHtml(report.reportId)}</code> · ${escapeHtml(report.generatedAt)}</p></div>
479
+ <details class="inline-fold compact"><summary>View original request</summary><p>${escapeHtml(report.task.original)}</p></details>
480
+ <div class="field"><span class="fk">Normalized question</span><p>${escapeHtml(report.task.normalized)}</p></div>
481
+ <div class="field"><span class="fk">Constraints</span><p>${escapeHtml(report.task.constraints.join('; ') || 'none recorded')}</p></div>
482
+ <div class="field"><span class="fk">Success criteria</span><p>${escapeHtml(report.task.successCriteria.join('; ') || 'none recorded')}</p></div>
483
+ <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>
484
+ <div class="field"><span class="fk">Structural score</span><p>${confidence.score}/100 (${escapeHtml(confidence.label)}) · heuristic, not benchmark-calibrated</p></div>
485
+ </article>${receipt}`;
486
+ const technical = `<details class="fold"><summary>Original submissions and graph events</summary><div class="fold-body">
487
+ <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>
488
+ <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>
489
+ <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>
490
+ <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>
491
+ </div></details>`;
492
+ const readerBody = [
493
+ section('01', 'Decision', recommendation, 60),
494
+ section('02', 'Action plan', actionPlan, 100, 'Requested product priorities, implementation milestones, and the smallest decisive validation test.'),
495
+ section('03', 'Why this decision', claimChain, 140, 'The load-bearing claim chain behind the recommendation.'),
496
+ section('04', 'What could change the decision', `<h3>Decision-sensitive facts</h3>${sensitivity}<h3>Strongest counter-case</h3>${counterCase}`, 180),
497
+ section('05', 'Evidence and verification', evidence, 220),
498
+ section('06', 'Risks, gaps, and open questions', `<h3>Risks</h3>${risks}<details class="inline-fold"><summary>View coverage ledger (${dossier.coverage.length})</summary>${coverage}</details><h3>Open questions</h3>${questions}`, 260),
499
+ section('07', 'Disagreement and dissent', disagreement, 300),
500
+ section('08', 'What the council added', `${shared}${unique}<h3>Verified unique contributions</h3>${contributions}`, 340),
501
+ section('09', 'Run details', runDetails, 380),
502
+ ].join('');
503
+ return `${stripReaderClaimIds(readerBody)}${section('10', 'Technical audit', technical, 420)}`;
504
+ }
313
505
  function renderLegacyIdeaBody(view) {
314
506
  const risks = view.risks ?? [];
315
507
  const agreements = view.agreements ?? [];
@@ -412,9 +604,10 @@ function renderIdeaBody(view) {
412
604
  ${conditions}
413
605
  ${glance}
414
606
  </section>`;
607
+ const synthesisWarning = view.flags.includes('synthesis_suspect') ? '<div class="warns"><span class="warn">⚑ synthesis suspect</span></div>' : '';
415
608
  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
- : '';
609
+ ? section('01', "Chairman's reasoning", `${synthesisWarning}<ul class="reasons">${view.keyPoints.map((p) => `<li>${escapeHtml(p)}</li>`).join('')}</ul>`, 100)
610
+ : synthesisWarning ? section('01', "Chairman's reasoning", `${synthesisWarning}<p class="muted">No reliable chairman reasoning was produced.</p>`, 100) : '';
418
611
  const scorecard = view.scorecard?.length
419
612
  ? `<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
613
  : '';
@@ -440,11 +633,13 @@ function renderIdeaBody(view) {
440
633
  <div class="field"><span class="fk">Chair</span><p>${escapeHtml(d.chair)}${d.reasoning ? ` — ${escapeHtml(d.reasoning)}` : ''}</p></div>
441
634
  </article>`).join('')
442
635
  : '<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>
636
+ const plan = view.actionPlan && 'kind' in view.actionPlan
637
+ ? `<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>`
638
+ : view.actionPlan
639
+ ? `<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
640
  ${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
641
  </tbody></table><p class="lede">${escapeHtml(view.actionPlan.sequencing_note)}</p></div>`
447
- : '<p class="muted">No validation plan artifact recorded.</p>';
642
+ : '<p class="muted">No validation plan artifact recorded.</p>';
448
643
  const questions = view.openQuestions?.length
449
644
  ? `<ul class="checks">${view.openQuestions.map((q) => `<li>${escapeHtml(q)}</li>`).join('')}</ul>`
450
645
  : '<p class="muted">No verdict-flipping open questions recorded.</p>';
@@ -478,15 +673,43 @@ function renderIdeaBody(view) {
478
673
  ${renderTechnical(view)}
479
674
  `;
480
675
  }
676
+ function renderQuickIdeaBody(view) {
677
+ const tone = recommendationTone(view.recommendation, view.signal?.tone ?? 'caution');
678
+ const label = recommendationLabel(view.recommendation, view.signal?.label ?? 'Quick analysis');
679
+ const conditions = view.conditions?.length
680
+ ? `<div class="conditions"><span class="fk">Conditions</span><ul>${view.conditions.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div>`
681
+ : '';
682
+ 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>`;
683
+ const reasoning = view.keyPoints.length
684
+ ? `<ul class="reasons">${view.keyPoints.map((point) => `<li>${escapeHtml(point)}</li>`).join('')}</ul>`
685
+ : '<p class="muted">No structured reasoning recorded.</p>';
686
+ const risks = view.risks?.length
687
+ ? `<ul class="checks">${view.risks.map((risk) => `<li>${escapeHtml(risk.assumption)} — ${escapeHtml(risk.challenge)}</li>`).join('')}</ul>`
688
+ : '<p class="muted">No load-bearing risk was supported strongly enough to list.</p>';
689
+ const plan = view.actionPlan && !('kind' in view.actionPlan)
690
+ ? `<ul class="checks">${view.actionPlan.actions.map((action) => `<li><strong>${escapeHtml(action.action)}</strong> — ${escapeHtml(action.kill_signal)}</li>`).join('')}</ul>`
691
+ : '<p class="muted">No executable plan was produced.</p>';
692
+ 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('');
693
+ const receipt = view.receipt?.length ? `<div class="receipt">${view.receipt.map((line) => `<span>${escapeHtml(line)}</span>`).join('')}</div>` : '';
694
+ return `${hero}
695
+ ${section('01', 'Analyst reasoning', reasoning, 100, 'One structured analyst; no council or independent-consensus claim.')}
696
+ ${section('02', 'Load-bearing risks', risks, 180)}
697
+ ${section('03', 'Validation plan', plan, 240)}
698
+ ${receipt ? section('04', 'Receipt', receipt, 300) : ''}
699
+ ${section('05', 'Analyst output', `<div class="model-grid">${analyst}</div>`, 360)}`;
700
+ }
481
701
  /** Serialize a council view to clean Markdown — for the HTML "Copy" button (paste into a coding assistant). */
482
702
  function councilMarkdown(view) {
703
+ if (view.decisionReport?.dossier)
704
+ return renderDecisionDossierMarkdown(view.decisionReport);
483
705
  const isIdea = view.workflow !== 'code-review';
706
+ const quick = isIdea && view.mode === 'quick';
484
707
  const reportV3 = isIdea && Boolean(view.recommendation || view.actionPlan);
485
708
  const L = [];
486
709
  L.push(`# ${isIdea && view.topic ? cleanTopic(view.topic) : isIdea ? 'Idea refinement' : 'Code review'}`, '');
487
710
  const panel = view.columns.map((c) => c.title).join(', ');
488
711
  if (panel)
489
- L.push(`Panel: ${panel}${view.moderator ? ` · Chair: ${view.moderator}` : ''}`, '');
712
+ L.push(quick ? `Analyst: ${panel}` : `Panel: ${panel}${view.moderator ? ` · Chair: ${view.moderator}` : ''}`, '');
490
713
  if (view.recommendation) {
491
714
  L.push('## Bottom line', '', `**${recommendationLabel(view.recommendation, '')}** — ${view.verdict}`, '');
492
715
  if (view.conditions?.length) {
@@ -500,11 +723,16 @@ function councilMarkdown(view) {
500
723
  L.push('## Verdict', '', view.verdict, '');
501
724
  }
502
725
  if (view.keyPoints?.length) {
503
- L.push("## Chairman's reasoning", '');
726
+ L.push(quick ? '## Analyst reasoning' : "## Chairman's reasoning", '');
727
+ if (view.flags.includes('synthesis_suspect'))
728
+ L.push('⚠ synthesis_suspect — the chair output required deterministic repair or degradation handling.', '');
504
729
  for (const p of view.keyPoints)
505
730
  L.push(`- ${p}`);
506
731
  L.push('');
507
732
  }
733
+ else if (view.flags.includes('synthesis_suspect')) {
734
+ L.push("## Chairman's reasoning", '', '⚠ synthesis_suspect — no reliable chairman reasoning was produced.', '');
735
+ }
508
736
  if (isIdea) {
509
737
  if (reportV3 && view.scorecard?.length) {
510
738
  L.push('## Dimension scorecard (best-effort)', '');
@@ -537,14 +765,23 @@ function councilMarkdown(view) {
537
765
  }
538
766
  if (reportV3 && view.actionPlan) {
539
767
  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}`);
768
+ if ('kind' in view.actionPlan) {
769
+ const planFlags = view.flags.filter((flag) => flag === 'plan_fallback' || flag === 'plan_skipped');
770
+ L.push(`Planner unavailable: ${view.actionPlan.reason}${planFlags.length ? ` (${planFlags.join(', ')})` : ''}.`, '', 'Unresolved questions:');
771
+ for (const question of view.actionPlan.unresolved_questions)
772
+ L.push(`- ${question}`);
546
773
  }
547
- L.push('', view.actionPlan.sequencing_note, '');
774
+ else {
775
+ for (const a of view.actionPlan.actions) {
776
+ L.push(`${a.order}. ${a.action}`);
777
+ L.push(` - Why: ${a.why}`);
778
+ L.push(` - Validates: ${a.validates}`);
779
+ L.push(` - Effort: ${a.effort}`);
780
+ L.push(` - Kill signal: ${a.kill_signal}`);
781
+ }
782
+ L.push('', view.actionPlan.sequencing_note);
783
+ }
784
+ L.push('');
548
785
  }
549
786
  if (reportV3 && view.openQuestions?.length) {
550
787
  L.push('## Open questions that flip the verdict', '');
@@ -553,7 +790,7 @@ function councilMarkdown(view) {
553
790
  L.push('');
554
791
  }
555
792
  if (view.columns.length) {
556
- L.push('## How each model saw it', '');
793
+ L.push(quick ? '## Analyst output' : '## How each model saw it', '');
557
794
  for (const c of view.columns) {
558
795
  L.push(`### ${c.title}`);
559
796
  for (const l of c.lines.slice(0, 10))
@@ -640,7 +877,7 @@ function renderTechnical(view) {
640
877
  const dissent = view.dissent.length ? `<ul>${view.dissent.map((d) => `<li>${escapeHtml(d)}</li>`).join('')}</ul>` : '<p class="muted">None recorded.</p>';
641
878
  return `
642
879
  <details class="fold reveal" style="animation-delay:0ms">
643
- <summary>Full council analysis (technical)</summary>
880
+ <summary>${view.mode === 'quick' ? 'Full single-analyst output (technical)' : 'Full council analysis (technical)'}</summary>
644
881
  <div class="fold-body">
645
882
  <h4 class="fold-h">Each model, in its own words</h4>
646
883
  <div class="cols">${columns || '<p class="muted">No model output recorded.</p>'}</div>
@@ -653,18 +890,21 @@ function renderTechnical(view) {
653
890
  }
654
891
  export function renderCouncilHtml(view) {
655
892
  const isIdea = view.workflow !== 'code-review';
656
- const kicker = isIdea ? 'aiki · idea refinement' : 'aiki · code review';
893
+ const quick = isIdea && view.mode === 'quick';
894
+ const kicker = quick ? 'aiki · quick analysis' : isIdea ? 'aiki · idea refinement' : 'aiki · code review';
657
895
  const title = isIdea && view.topic ? cleanTopic(view.topic) : (isIdea ? 'Idea refinement' : 'Code review');
658
896
  const panel = view.columns.map((c) => c.title);
659
897
  const metaBits = [
660
- panel.length ? `Panel: ${panel.join(' · ')}` : '',
661
- view.moderator ? `Chair: ${view.moderator}` : '',
898
+ panel.length ? `${quick ? 'Analyst' : 'Panel'}: ${panel.join(' · ')}` : '',
899
+ !quick && view.moderator ? `Chair: ${view.moderator}` : '',
662
900
  view.calls,
663
901
  ].filter(Boolean);
664
902
  const flags = view.flags.length
665
903
  ? `<div class="warns">${view.flags.map((f) => `<span class="warn">⚑ ${escapeHtml(f.replaceAll('_', ' '))}</span>`).join('')}</div>`
666
904
  : '';
667
- const body = isIdea ? renderIdeaBody(view) : renderReviewBody(view);
905
+ const body = isIdea && view.decisionReport?.dossier
906
+ ? renderDossierIdeaBody(view.decisionReport)
907
+ : quick ? renderQuickIdeaBody(view) : isIdea ? renderIdeaBody(view) : renderReviewBody(view);
668
908
  // Embed the report as Markdown for the Copy button. Escape "<" so a "</script>" in content can't break out.
669
909
  const md = JSON.stringify(councilMarkdown(view)).replace(/</g, '\\u003c');
670
910
  return `<!doctype html>
@@ -672,44 +912,88 @@ export function renderCouncilHtml(view) {
672
912
  <head>
673
913
  <meta charset="utf-8">
674
914
  <meta name="viewport" content="width=device-width, initial-scale=1">
675
- <title>${escapeHtml(isIdea ? 'Idea refinement' : 'Code review')} — aiki council</title>
915
+ <title>${escapeHtml(quick ? 'Quick analysis' : isIdea ? 'Idea refinement' : 'Code review')} — aiki</title>
676
916
  <style>
677
917
  :root{
678
918
  color-scheme: light;
679
919
  --serif:"Iowan Old Style","Palatino Linotype",Palatino,Charter,Georgia,"Times New Roman",serif;
680
920
  --sans:-apple-system,BlinkMacSystemFont,"Avenir Next","Segoe UI",system-ui,sans-serif;
681
921
  --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;
922
+ --paper:#fbfaf7; --panel:#f1efe9; --ink:#171a18; --soft:#59625e; --faint:#8b928e; --line:#dcded8;
923
+ --good:#176b52; --good-bg:#e5f3ed; --risk:#a63a30; --risk-bg:#f8e8e4;
924
+ --caution:#a96813; --caution-bg:#f8eedc; --slate:#4f5e59; --accent:#155f55;
685
925
  }
686
926
  *{box-sizing:border-box;}
687
927
  html{-webkit-text-size-adjust:100%;}
688
928
  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;}
929
+ main{max-width:940px;margin:0 auto;padding:34px 24px 90px;}
690
930
  a{color:var(--accent);}
691
- h1,h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
931
+ h1{font-family:var(--serif);font-weight:600;letter-spacing:-.025em;}
932
+ h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
692
933
  p{margin:0 0 .6em;}
693
934
 
694
935
  /* masthead */
695
- .mast{border-bottom:2px solid var(--ink);padding-bottom:22px;margin-bottom:30px;}
936
+ .mast{border-bottom:1px solid var(--ink);padding-bottom:22px;margin-bottom:30px;}
696
937
  .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;}
938
+ .mast h1{font-size:clamp(32px,5.4vw,50px);line-height:1.05;margin:12px 0 18px;max-width:18ch;}
698
939
  .mmeta{display:flex;flex-wrap:wrap;gap:7px;}
699
940
  .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
941
  .warns{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;}
701
942
  .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
943
 
703
944
  /* 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;}
945
+ .verdict{position:relative;background:#fff;border:1px solid var(--line);border-radius:18px;
946
+ padding:30px 32px 26px;margin-bottom:18px;overflow:hidden;box-shadow:0 18px 50px rgba(32,45,40,.07);}
706
947
  .verdict::before{content:"";position:absolute;left:0;top:0;bottom:0;width:6px;}
707
948
  .tone-good::before{background:var(--good);} .tone-caution::before{background:var(--caution);} .tone-risk::before{background:var(--risk);}
708
949
  .pill{display:inline-block;font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.02em;
709
- padding:5px 13px;border-radius:100px;margin-bottom:14px;}
950
+ padding:5px 13px;border-radius:100px;}
710
951
  .tone-good .pill{background:var(--good-bg);color:var(--good);} .tone-caution .pill{background:var(--caution-bg);color:var(--caution);}
711
952
  .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;}
953
+ .decision-status{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px;}
954
+ .decision-status>span:last-child{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;color:var(--soft);text-transform:uppercase;}
955
+ .verdict-text{font-family:var(--sans);font-weight:700;font-size:clamp(18px,2.3vw,21px);line-height:1.42;color:var(--ink);margin:0;max-width:68ch;letter-spacing:-.012em;}
956
+ .verdict-detail{font-size:14.5px;line-height:1.62;color:var(--soft);margin:9px 0 0;max-width:76ch;}
957
+ .section-eyebrow{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--accent);margin-bottom:9px;}
958
+ .decision-numbers{margin:0 0 20px;}.snapshot-table{width:100%;border-collapse:collapse;font-size:13.5px;line-height:1.4;}
959
+ .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);}
960
+ .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;}
961
+ .snapshot-table .number-value{font-family:var(--serif);font-size:17px;font-weight:700;color:var(--ink);white-space:nowrap;}
962
+ .snapshot-table code,.payback-result code,.tripwire code{font-size:9.5px;color:var(--faint);white-space:nowrap;}
963
+ .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;}
964
+ .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);}
965
+ .evidence-coverage{margin:24px 0 18px;padding:14px 16px;background:var(--paper);border:1px solid var(--line);border-radius:12px;}
966
+ .evidence-coverage>div:first-child{display:flex;align-items:end;justify-content:space-between;gap:12px;}
967
+ .evidence-coverage strong{font-family:var(--mono);font-size:13px;color:var(--ink);}
968
+ .coverage-track{height:5px;margin:10px 0;border-radius:99px;background:var(--line);overflow:hidden;}
969
+ .coverage-track span{display:block;height:100%;border-radius:inherit;background:var(--accent);}
970
+ .evidence-coverage.low .coverage-track span{background:var(--risk);}.evidence-coverage.medium .coverage-track span{background:var(--caution);}
971
+ .evidence-coverage p{font-size:13.5px;color:var(--soft);margin:0;}
972
+ .decision-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;margin:18px 0;}
973
+ .decision-fact{padding:16px;background:var(--paper);border:1px solid var(--line);border-radius:12px;}
974
+ .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;}
975
+ .decision-fact p{margin:0;font-size:14.5px;line-height:1.5;}
976
+ .action-callout{position:relative;margin:20px 0;padding:20px 22px 20px 26px;background:var(--ink);color:var(--paper);border-radius:12px;}
977
+ .action-callout::before{content:"→";position:absolute;right:20px;top:12px;font-family:var(--serif);font-size:34px;color:#8fc7b9;}
978
+ .action-callout>span{color:#8fc7b9;}.action-callout p{font-size:17px;line-height:1.5;margin:0;padding-right:34px;}
979
+ .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;}
980
+ .basis-chip.target_cap{background:var(--caution-bg);color:var(--caution);}.basis-chip.unknown{background:var(--risk-bg);color:var(--risk);}
981
+ .tripwire{margin:14px 0;padding:16px 18px;border:1px solid #b9d5cd;border-left:4px solid var(--accent);border-radius:10px;background:#f2f8f6;}
982
+ .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);}
983
+ .decision-safety{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:18px;}
984
+ .decision-safety article{padding:17px 18px;background:var(--paper);border:1px solid var(--line);border-radius:12px;}
985
+ .decision-safety article:first-child{border-left:4px solid var(--risk);}
986
+ .decision-safety p{font-size:14px;margin:0;}
987
+ .critical-unknowns{margin:0;padding-left:18px;}.critical-unknowns li{font-size:13.5px;margin:0 0 6px;}
988
+ .critical-warning{margin-top:12px;padding:14px 16px;background:var(--risk-bg);border:1px solid #e1b8b1;border-radius:10px;}
989
+ .critical-warning .tag{color:var(--risk);}.critical-warning p{font-size:13.5px;margin:0;}
990
+ .council-read{margin:15px 0 0;font-size:13.5px;color:var(--soft);}
991
+ .decision-details{margin-top:14px;border-top:1px solid var(--line);padding-top:12px;}
992
+ .decision-details summary{cursor:pointer;font-family:var(--mono);font-size:11px;color:var(--soft);text-transform:uppercase;letter-spacing:.08em;}
993
+ .decision-details>div{padding-top:10px;font-size:13.5px;}.decision-details ul{margin:8px 0;padding-left:18px;}
994
+ .decision-evidence{margin-top:12px;border-top:1px solid var(--line);padding-top:12px;}
995
+ .decision-evidence summary{cursor:pointer;font-family:var(--mono);font-size:10.5px;color:var(--faint);letter-spacing:.07em;text-transform:uppercase;}
996
+ .decision-evidence ul{margin:10px 0 0;padding-left:20px;}.decision-evidence li{font-size:13px;color:var(--soft);margin-bottom:5px;}
713
997
 
714
998
  /* top action bar + copy */
715
999
  .bar{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;gap:12px;
@@ -719,6 +1003,7 @@ p{margin:0 0 .6em;}
719
1003
  border:0;border-radius:8px;padding:8px 15px;transition:background .15s ease,transform .1s ease;}
720
1004
  .copy-btn:hover{filter:brightness(1.05);} .copy-btn:active{transform:translateY(1px);}
721
1005
  .copy-btn.ok{background:var(--good);}
1006
+ button:focus-visible,summary:focus-visible,a:focus-visible{outline:3px solid rgba(21,95,85,.28);outline-offset:3px;}
722
1007
 
723
1008
  /* chairman's reasoning */
724
1009
  .reasons{margin:0;padding:0;list-style:none;}
@@ -738,6 +1023,7 @@ p{margin:0 0 .6em;}
738
1023
 
739
1024
  /* bottom line */
740
1025
  .bottomline{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:34px;}
1026
+ .verdict .bottomline{margin:18px 0 0;}
741
1027
  .bottomline > div{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;}
742
1028
  .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
1029
  .bottomline p{margin:0;font-size:15px;color:var(--ink);}
@@ -794,12 +1080,30 @@ p{margin:0 0 .6em;}
794
1080
  .score span{font-size:13.5px;color:var(--ink);}
795
1081
  .score strong{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
796
1082
  .score.contested{border-left:4px solid var(--caution);} .score.examined{border-left:4px solid var(--good);} .score.unexamined{border-left:4px solid var(--risk);}
797
- .table-wrap{overflow-x:auto;}
1083
+ .table-wrap{overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin;}
798
1084
  .data-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:13.5px;}
799
1085
  .data-table th,.data-table td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:9px 10px;}
800
1086
  .data-table th{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--soft);background:var(--paper);}
801
1087
  .data-table tr:last-child td{border-bottom:0;}
802
1088
  .data-table code{font-family:var(--mono);font-size:12px;color:var(--accent);}
1089
+ .feature-groups{display:grid;gap:14px;margin-bottom:26px;}
1090
+ .feature-group{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden;}
1091
+ .feature-group>header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 18px;border-bottom:1px solid var(--line);background:var(--paper);}
1092
+ .feature-group>header div>span{display:block;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.13em;color:var(--accent);}
1093
+ .feature-group>header h4{font-size:14px;margin:2px 0 0;}.feature-group>header>strong{font-family:var(--serif);font-size:24px;color:var(--soft);}
1094
+ .feature-group ul{list-style:none;margin:0;padding:0 18px;}.feature-group li{padding:14px 0;border-bottom:1px solid var(--line);}.feature-group li:last-child{border-bottom:0;}
1095
+ .feature-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px;}.feature-title>strong{font-size:15px;}.feature-title>span{font-family:var(--mono);font-size:10px;border:1px solid var(--line);border-radius:99px;padding:1px 7px;color:var(--soft);}
1096
+ .feature-group p{font-size:13.5px;margin:3px 0;color:var(--ink);}.feature-group small{display:block;font-size:12.5px;line-height:1.45;color:var(--soft);}
1097
+ .priority-must{border-left:4px solid var(--accent);}.priority-should{border-left:4px solid var(--caution);}.priority-later{border-left:4px solid var(--slate);}
1098
+ .milestone-list{list-style:none;margin:0 0 28px;padding:0;}.milestone-list>li{display:grid;grid-template-columns:90px 1fr;gap:18px;position:relative;padding-bottom:18px;}
1099
+ .milestone-list>li:not(:last-child)::before{content:"";position:absolute;left:23px;top:37px;bottom:0;border-left:1px solid var(--line);}
1100
+ .milestone-marker{position:relative;z-index:1;display:flex;flex-direction:column;align-items:flex-start;}.milestone-marker>span{display:grid;place-items:center;width:47px;height:35px;border-radius:9px;background:var(--ink);color:var(--paper);font-family:var(--mono);font-size:12px;}.milestone-marker small{font-size:11px;color:var(--soft);margin-top:5px;}
1101
+ .milestone-list article{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;}.milestone-list h4{font-size:16px;margin:0 0 8px;}.milestone-list article ul{margin:0;padding-left:18px;}.milestone-list article li{font-size:13.5px;margin-bottom:4px;}
1102
+ .acceptance{margin-top:12px;padding-top:10px;border-top:1px solid var(--line);}.acceptance span{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--good);}.acceptance p{font-size:13px;margin:2px 0 0;}
1103
+ .experiment-list{display:grid;gap:12px;margin-bottom:14px;}.experiment-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 18px;}
1104
+ .experiment-head{display:grid;grid-template-columns:auto 1fr auto;align-items:start;gap:10px;}.experiment-head>span{font-family:var(--mono);font-size:11px;color:var(--accent);}.experiment-head h4{font-size:15px;margin:0;}.experiment-head>strong{font-family:var(--mono);font-size:10px;border:1px solid var(--line);border-radius:99px;padding:2px 7px;color:var(--soft);}
1105
+ .experiment-card>p{font-size:13.5px;color:var(--soft);margin:8px 0 12px 27px;}.experiment-card dl{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:0 0 0 27px;}.experiment-card dl>div{background:var(--paper);border-radius:8px;padding:9px 11px;}.experiment-card dt{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);}.experiment-card dd{font-size:12.5px;line-height:1.45;margin:2px 0 0;}
1106
+ .inline-fold{margin:14px 0;border:1px solid var(--line);border-radius:10px;background:var(--panel);}.inline-fold>summary{cursor:pointer;padding:11px 14px;font-family:var(--mono);font-size:11px;letter-spacing:.05em;color:var(--soft);}.inline-fold>ul,.inline-fold>.table-wrap,.inline-fold>p{margin:0 14px 14px;}.inline-fold.compact{background:var(--paper);}.inline-fold.compact>p{font-size:13.5px;color:var(--soft);}
803
1107
  .debate-card p{margin-bottom:8px;}
804
1108
  .redteam{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
805
1109
  .redteam>div{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;}
@@ -831,7 +1135,16 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
831
1135
  @keyframes rise{to{opacity:1;transform:none;}}
832
1136
  @media (max-width:640px){
833
1137
  main{padding:34px 16px 60px;}
834
- .glance,.bottomline,.checks,.redteam{grid-template-columns:1fr;}
1138
+ .verdict{padding:24px 20px 20px;}
1139
+ .glance,.bottomline,.checks,.redteam,.decision-safety{grid-template-columns:1fr;}
1140
+ .decision-status{align-items:flex-start;flex-direction:column;gap:8px;}.decision-status>span:last-child{text-align:left;}.pill{white-space:nowrap;}
1141
+ .snapshot-table{min-width:0;}.snapshot-table thead{display:none;}.snapshot-table tbody,.snapshot-table tr,.snapshot-table td{display:block;width:100%;}
1142
+ .snapshot-table tr{padding:12px 0;border-bottom:1px solid var(--line);}.snapshot-table td{padding:3px 8px;border:0;white-space:normal;}
1143
+ .snapshot-table td::before{display:block;font-family:var(--mono);font-size:9px;line-height:1.4;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);}
1144
+ .decisive-table td:nth-child(1)::before{content:"Metric";}.decisive-table td:nth-child(2)::before{content:"Value";}.decisive-table td:nth-child(3)::before{content:"What it means";}
1145
+ .options-table td:nth-child(1)::before{content:"Path";}.options-table td:nth-child(2)::before{content:"Commitment";}.options-table td:nth-child(3)::before{content:"Basis";}.options-table td:nth-child(4)::before{content:"Trade-off";}
1146
+ .payback-result{grid-template-columns:1fr;}.payback-result p{grid-column:1;}
1147
+ .milestone-list>li{grid-template-columns:64px 1fr;gap:10px;}.experiment-card dl{grid-template-columns:1fr;}.experiment-card>p,.experiment-card dl{margin-left:0;}
835
1148
  }
836
1149
  @media (prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none;animation:none;}}
837
1150
  @media print{.reveal{opacity:1;transform:none;animation:none;}.fold[open]{break-inside:avoid;}}
@@ -840,7 +1153,7 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
840
1153
  <body>
841
1154
  <div class="bar">
842
1155
  <span class="bar-label">${escapeHtml(kicker)}</span>
843
- <button class="copy-btn" onclick="copyReport(this)">Copy report (Markdown)</button>
1156
+ <button id="copy-report" class="copy-btn">Copy report (Markdown)</button>
844
1157
  </div>
845
1158
  <main>
846
1159
  <header class="mast">
@@ -849,16 +1162,32 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
849
1162
  ${flags}
850
1163
  </header>
851
1164
  ${body}
852
- <footer>Generated by aiki · ${escapeHtml(view.runId)} · a local model council. This is analysis, not advice — verify before acting.</footer>
1165
+ <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
1166
  </main>
854
1167
  <script>
855
1168
  const REPORT_MD = ${md};
856
- function copyReport(btn){
857
- navigator.clipboard.writeText(REPORT_MD).then(function(){
858
- var o=btn.textContent; btn.textContent='✓ Copied'; btn.classList.add('ok');
859
- setTimeout(function(){ btn.textContent=o; btn.classList.remove('ok'); }, 1600);
860
- }).catch(function(){ btn.textContent='Copy failed — select the text manually'; });
1169
+ async function copyReport(btn){
1170
+ var original=btn.textContent;
1171
+ try {
1172
+ var copied=false;
1173
+ if(navigator.clipboard && navigator.clipboard.writeText){
1174
+ try{ await navigator.clipboard.writeText(REPORT_MD); copied=true; }catch(error){}
1175
+ }
1176
+ if(!copied){
1177
+ var field=document.createElement('textarea');
1178
+ field.value=REPORT_MD; field.setAttribute('readonly','');
1179
+ field.style.position='fixed'; field.style.opacity='0';
1180
+ document.body.appendChild(field); field.select();
1181
+ copied=document.execCommand('copy'); field.remove();
1182
+ if(!copied) throw new Error('copy unavailable');
1183
+ }
1184
+ btn.textContent='✓ Copied'; btn.classList.add('ok');
1185
+ }catch(error){
1186
+ btn.textContent='Copy failed — select the text manually';
1187
+ }
1188
+ setTimeout(function(){ btn.textContent=original; btn.classList.remove('ok'); }, 1800);
861
1189
  }
1190
+ document.getElementById('copy-report').addEventListener('click',function(){ copyReport(this); });
862
1191
  </script>
863
1192
  </body>
864
1193
  </html>`;