aiki-cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -8
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +10 -7
  6. package/dist/council/view.js +146 -46
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +467 -80
  9. package/dist/orchestration/decision-graph.js +31 -0
  10. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  11. package/dist/orchestration/modes.js +16 -4
  12. package/dist/orchestration/preflight.js +43 -9
  13. package/dist/orchestration/quick-analysis.js +35 -6
  14. package/dist/orchestration/stages/s10-render.js +179 -79
  15. package/dist/orchestration/stages/s4-analyze.js +3 -1
  16. package/dist/orchestration/stages/s6-positions.js +18 -0
  17. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  18. package/dist/orchestration/stages/s8-verify.js +26 -9
  19. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  20. package/dist/orchestration/stages/s9-judge.js +36 -13
  21. package/dist/orchestration/stages/s9b-plan.js +208 -35
  22. package/dist/orchestration/url-sources.js +200 -0
  23. package/dist/schemas/index.js +134 -6
  24. package/dist/skills/idea-refinement/analyst.md +5 -5
  25. package/dist/skills/idea-refinement/chair.md +18 -0
  26. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  27. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  28. package/dist/skills/idea-refinement/planner.md +25 -19
  29. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  30. package/dist/skills/idea-refinement/verifier.md +17 -0
  31. package/dist/storage/runs.js +2 -1
  32. package/dist/tui/app.js +19 -4
  33. package/dist/workflows/idea-refinement.js +51 -15
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2 — 2026-07-17
4
+
5
+ ### Added
6
+ - Public URL snapshots for idea prompts, including an npm registry adapter, private-network rejection,
7
+ auditable `FETCHED` / `BLOCKED` / `FAILED` status, and a full-council gate that stops before model calls
8
+ when a supplied source cannot be read.
9
+ - Requested-output planning: prompts that ask for a feature list or implementation plan now carry that
10
+ requirement through the decision contract, the existing planner call, machine JSON, Markdown, and HTML.
11
+
12
+ ### Changed
13
+ - **Report v4 answer-first decisions** — reports now lead with the requested deliverables and chair reasoning,
14
+ substitute readable claim labels for internal ids, dedupe and cap claim-named conditions, frame/cap risks,
15
+ summarize each council seat (including a plain-language `weak_seat` warning), and distinguish FACTUAL
16
+ claims from JUDGMENT calls so verification priority and coverage describe checkable facts honestly.
17
+ Deliverable detection now combines model-backed readings with a widened fallback that hears requests for
18
+ standout or "ultra-level" features.
19
+ - `council` now includes the complete source-investigation path, 8–10-call allowance, 12-call repair budget,
20
+ and 45-minute deadline. `research` remains accepted as a compatibility alias with identical behavior.
21
+ Preflight asks zero to four questions and may not repeat facts already present in the user prompt or a
22
+ fetched source.
23
+ - Reader-facing report sections use human claim text instead of internal graph ids such as `G1`; raw ids
24
+ remain in machine artifacts and the Markdown technical audit.
25
+
3
26
  ## 0.3.0 — 2026-07-15 — evidence-grounded decision council
4
27
 
5
28
  This release turns idea refinement into a bounded, evidence-linked decision workflow with explicit modes,
package/README.md CHANGED
@@ -50,9 +50,8 @@ stop copy-pasting between them by hand.
50
50
  - **Evidence-grounded decisions.** Supply local sources with `--evidence`; Aiki records their paths and
51
51
  hashes, checks freshness and citations, independently verifies selected load-bearing claims, and shows the
52
52
  remaining coverage gaps before the chair.
53
- - **Three explicit modes.** Use `quick` for one structured analyst, `council` for bounded multi-model
54
- deliberation, or `research` for current-fact work with Codex search. Aiki never hides this choice behind a
55
- learned router.
53
+ - **Two bounded behaviors.** Use `quick` for one structured analyst or `council` for the full multi-model
54
+ decision council with source investigation. `research` remains accepted as an alias for `council`.
56
55
  - **A decision dossier, not an essay.** Reports lead with the recommendation, verified evidence coverage,
57
56
  decisive facts, first action, strongest counter-case, and critical unknowns. Financial and threshold-heavy
58
57
  decisions can include graph-anchored numbers, payback, option commitments, and a go/no-go tripwire.
@@ -236,7 +235,7 @@ Plain text is never charged silently — you get a confirm step before any run s
236
235
  aiki run idea-refinement "a fridge-photo-to-recipe app for busy parents"
237
236
  aiki run idea-refinement "an early idea" --mode quick # one structured analyst; no council claim
238
237
  aiki run idea-refinement ./idea.md
239
- aiki run idea-refinement ./idea.md --mode research --evidence ./research/ # grounded, source-verifying council
238
+ aiki run idea-refinement ./idea.md --mode council --evidence ./research/ # grounded, source-verifying council
240
239
  aiki run code-review --base main # review this branch vs main
241
240
  aiki run code-review --diff ./changes.patch # review a patch file
242
241
  aiki run code-review --cheap # Gemini+Codex review, Claude judges only disputes (~⅓ the Opus)
@@ -249,6 +248,12 @@ An idea run **auto-opens** its report in your browser when it finishes.
249
248
  SHA-256 hashes in the run, gives those paths to the read-only scouts, and never copies the source files or
250
249
  reads provider credential directories.
251
250
 
251
+ Public `http(s)` links in an idea are snapshotted once before model calls and stored in
252
+ `00a-url-sources.json`; every preflight reader and analyst sees that same text. npm package links use the
253
+ registry metadata API. A snapshot records `FETCHED`, `BLOCKED`, or `FAILED` honestly. In full council mode,
254
+ an unreadable supplied link stops before paid calls and asks for pasted text or a public export instead of
255
+ letting the council guess or grill you for facts that should have come from the link.
256
+
252
257
  ## The two workflows
253
258
 
254
259
  <p align="center">
@@ -263,8 +268,9 @@ only the disputes → report.
263
268
  complementary analyst lanes → deterministic claim/evidence graph audit → only decision-critical verification
264
269
  or rebuttal → evidence-linked chair → validation planner. The report is a graph-backed **decision dossier**,
265
270
  not an essay.
266
- Choose `--mode quick` for one structured analyst, `--mode council` (default) for the full decision council,
267
- or `--mode research` for source-grounded current-fact work. Aiki never chooses a mode with a learned router:
271
+ Choose `--mode quick` for one structured analyst or `--mode council` for the full source-investigating decision
272
+ council. `--mode research` is retained as a compatibility alias and runs the same council. Without a flag, the
273
+ default is the full council:
268
274
 
269
275
  - a **reader-first decision card** — recommendation, verified evidence coverage, decisive facts, first action,
270
276
  strongest counter-case, and critical unknowns before audit detail
@@ -274,6 +280,8 @@ or `--mode research` for source-grounded current-fact work. Aiki never chooses a
274
280
  - an **evidence and coverage ledger** — source, date, freshness, verification, `NOT_APPLICABLE`, and missing evidence
275
281
  - **genuine disagreements and position changes** — explicit `CONCEDE` / `COUNTER` / `NARROW` events
276
282
  - **decision sensitivity and an executable experiment plan** — anchored tests with effort and kill signals
283
+ - **requested product deliverables** — when the prompt asks for them, the same planner call must produce a
284
+ prioritized `MUST / SHOULD / LATER / NOT IN SCOPE` feature list and timeboxed implementation milestones
277
285
  - a **verified contribution ledger** — unique provider claims count only after independent verification
278
286
  - an **orchestration receipt and technical fold** — calls, degradation, submissions, edges, and graph events
279
287
 
@@ -350,8 +358,8 @@ This is the part that makes aiki trustworthy to point at a real repo:
350
358
  ## Costs & limits
351
359
 
352
360
  - **Runs cost real model calls** against your existing CLI subscriptions/quota. Idea refinement is nominally
353
- **3 calls in quick**, **6–8 in council**, or **8–10 in research** (schema repairs can add calls within the
354
- mode-aware budget); code review is about **5**. `aiki run` shows the mode, range, budget, and reserved
361
+ **3 calls in quick** or **8–10 in the full council** (`research` is the same behavior; schema repairs can add
362
+ calls within the mode-aware budget); code review is about **5**. `aiki run` shows the mode, range, budget, and reserved
355
363
  chair/planner calls before asking to confirm (skip with `--yes`).
356
364
  - **Not a general assistant.** Questions and "explore my whole codebase" requests are redirected, not answered
357
365
  — aiki reviews a *diff* and vets a *stated idea*.
@@ -118,7 +118,7 @@ function blindId(seed) {
118
118
  return createHash('sha256').update(seed).digest('hex').slice(0, 12).toUpperCase();
119
119
  }
120
120
  /** Remove the explicit identifiers forbidden by BENCHMARK-IDEA-V3.md §4 before human rating. §4 bars
121
- * provider/model names, run ids, arm labels, AND costs — so the R7 dossier's §9 "Run details" cost
121
+ * provider/model names, run ids, arm labels, AND costs — so the dossier's "Run details" cost
122
122
  * block (mode, call counts, categories, per-provider calls, model time, degradation flags) and the
123
123
  * inline `> ⚠ DEGRADED: <flag tokens>` callouts are redacted too; the DEGRADED marker and any prose
124
124
  * note stay, since those are quality self-assessments raters legitimately read. */
package/dist/cli/index.js CHANGED
@@ -51,7 +51,7 @@ program
51
51
  .option('--head <ref>', 'code-review: head git ref to diff to (default HEAD)')
52
52
  .option('--diff <file>', 'code-review: review a patch file instead of computing a git diff')
53
53
  .option('--evidence <path>', 'idea-refinement: local source file/directory (stores paths + hashes, not copies)')
54
- .option('--mode <mode>', 'idea-refinement: quick | council | research (default council)')
54
+ .option('--mode <mode>', 'idea-refinement: quick | council (research is an alias for council)')
55
55
  .option('--cheap', 'code-review: Gemini+Codex review, Claude judges only disputes (~⅓ the Opus; experimental)')
56
56
  .option('--yes', 'skip the run-cost confirmation prompt')
57
57
  .action(async (workflow, input, opts) => {
package/dist/cli/run.js CHANGED
@@ -11,7 +11,7 @@ import { resolveRunsRoot } from '../storage/paths.js';
11
11
  import { openCouncilHtml } from '../council/open.js';
12
12
  import { buildEvidencePack } from '../orchestration/evidence-pack.js';
13
13
  import { IdeaModeSchema } from '../schemas/index.js';
14
- import { defaultBudgetFor, defaultDeadlineFor, IDEA_MODE_PLANS } from '../orchestration/modes.js';
14
+ import { defaultBudgetFor, defaultDeadlineFor, IDEA_MODE_PLANS, inferIdeaMode } from '../orchestration/modes.js';
15
15
  const WORKFLOWS = ['idea-refinement', 'code-review'];
16
16
  export function estimateRun(workflow, opts = {}) {
17
17
  if (workflow === 'code-review')
@@ -21,7 +21,7 @@ export function estimateRun(workflow, opts = {}) {
21
21
  return {
22
22
  calls: plan.maxCalls,
23
23
  opus: mode === 'quick' ? 1 : 2,
24
- minCalls: mode === 'research' ? 8 : plan.baseCalls,
24
+ minCalls: plan.maxCalls - plan.reservedCalls,
25
25
  reserved: plan.reservedCalls,
26
26
  };
27
27
  }
@@ -91,12 +91,14 @@ export async function runCommand(workflow, input, opts = {}) {
91
91
  }
92
92
  let mode;
93
93
  if (workflow === 'idea-refinement') {
94
- const parsed = IdeaModeSchema.safeParse(opts.mode ?? 'council');
95
- if (!parsed.success) {
96
- process.stderr.write(`unknown idea mode "${opts.mode}". Available: quick, council, research\n`);
97
- return 1;
94
+ if (opts.mode !== undefined) {
95
+ const parsed = IdeaModeSchema.safeParse(opts.mode);
96
+ if (!parsed.success) {
97
+ process.stderr.write(`unknown idea mode "${opts.mode}". Available: quick, council, research\n`);
98
+ return 1;
99
+ }
100
+ mode = parsed.data;
98
101
  }
99
- mode = parsed.data;
100
102
  }
101
103
  else if (opts.mode) {
102
104
  process.stderr.write('--mode only applies to idea-refinement.\n');
@@ -118,6 +120,7 @@ export async function runCommand(workflow, input, opts = {}) {
118
120
  return 1;
119
121
  }
120
122
  text = resolved;
123
+ mode ??= inferIdeaMode(text);
121
124
  }
122
125
  if (opts.evidence) {
123
126
  if (workflow !== 'idea-refinement') {
@@ -3,7 +3,7 @@ import { basename, join } from 'node:path';
3
3
  import { DISPLAY_NAME } from '../providers/types.js';
4
4
  import { RoleOutput as RoleOutputSchema } from '../schemas/index.js';
5
5
  import { deriveAudit, deriveScorecard, mergeOpenQuestions } from '../orchestration/stages/s10-render.js';
6
- import { renderDecisionDossierMarkdown } from '../orchestration/decision-dossier.js';
6
+ import { buildReaderProjection, claimLookup, readerClaimLabel, readerClaimRefs, renderDecisionDossierMarkdown, stripReaderClaimIds } from '../orchestration/decision-dossier.js';
7
7
  import { IDEA_RUBRIC } from '../workflows/idea-refinement.js';
8
8
  import { listArtifacts, readJsonArtifact } from '../storage/runs-read.js';
9
9
  import { adaptIdeaOutput, adaptLegacyDecisionGraph } from '../orchestration/legacy-idea-adapter.js';
@@ -377,31 +377,38 @@ function renderDossierIdeaBody(report) {
377
377
  const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
378
378
  const tone = dossier.recommendation.status === 'ACCEPTED' ? 'good'
379
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>`
380
+ const labelFor = claimLookup(report);
381
+ // Substitute ids then dedupe before escaping, mirroring the markdown renderer: old stored artifacts
382
+ // may carry duplicate / bare-G# condition strings.
383
+ const conditionItems = [...new Set(dossier.recommendation.conditions.map((condition) => stripReaderClaimIds(condition.text, labelFor)))];
384
+ const conditions = conditionItems.length
385
+ ? `<details class="decision-details"><summary>Conditions and decision state</summary><div><p><strong>Internal state:</strong> ${escapeHtml(dossier.recommendation.status)}</p><ul>${conditionItems.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div></details>`
382
386
  : '';
383
387
  const startHere = dossier.experiments.actions[0]?.action ?? 'No executable next step was produced.';
388
+ const recommendationLead = stripReaderClaimIds(dossier.recommendation.reason, labelFor);
389
+ const recommendationDetail = stripReaderClaimIds(dossier.recommendation.summary, labelFor);
384
390
  const factLabels = ['Decisive result', 'Consequence', 'Supporting signal'];
385
391
  const facts = keyFindings.slice(0, 3).map((finding, index) => `<article class="decision-fact"><span>${factLabels[index]}</span><p>${escapeHtml(finding)}</p></article>`).join('');
386
392
  const snapshot = report.decisionSnapshot;
387
393
  const decisiveNumbers = snapshot ? `<div class="decision-numbers">
388
394
  <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>` : ''}
395
+ <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>
396
+ ${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>` : ''}
391
397
  </div>` : '';
392
398
  const optionComparison = snapshot ? `<div class="option-comparison">
393
399
  <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>
400
+ <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>
395
401
  </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>` : '';
402
+ 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>` : '';
397
403
  const topCounterCase = dossier.counterCase.reasoning;
398
404
  const unknowns = criticalUnknowns.length
399
405
  ? `<ul class="critical-unknowns">${criticalUnknowns.map((unknown) => `<li>${escapeHtml(unknown)}</li>`).join('')}</ul>`
400
406
  : '<p class="muted">No verdict-flipping unknown was recorded.</p>';
401
407
  const recommendation = `<div class="verdict tone-${tone}">
402
- <span class="pill">Council recommendation</span>
408
+ <div class="decision-status"><span class="pill">Council recommendation</span><span>${escapeHtml(dossier.recommendation.status.replaceAll('_', ' '))}</span></div>
403
409
  ${decisiveNumbers}
404
- <p class="verdict-text">${escapeHtml(dossier.recommendation.summary)}</p>
410
+ <p class="verdict-text">${escapeHtml(recommendationLead)}</p>
411
+ <p class="verdict-detail">${escapeHtml(recommendationDetail)}</p>
405
412
  <div class="evidence-coverage ${coverageLabel.toLowerCase()}">
406
413
  <div><span class="fk">Evidence coverage</span><strong>${coverageLabel} · ${dossierPct(confidence.verificationCoverage)}</strong></div>
407
414
  <div class="coverage-track"><span style="width:${Math.round(confidence.verificationCoverage * 100)}%"></span></div>
@@ -418,17 +425,17 @@ function renderDossierIdeaBody(report) {
418
425
  ${report.verdict.criticalWarning ? `<div class="critical-warning"><span class="tag">Critical warning</span><p>${escapeHtml(report.verdict.criticalWarning)}</p></div>` : ''}
419
426
  <p class="council-read"><strong>Council read:</strong> ${escapeHtml(dossierCouncilRead(report))}</p>
420
427
  ${conditions}
421
- <div class="evidence-anchors"><span>Evidence anchors</span><code>${escapeHtml(dossierRefs(dossier.recommendation.claimIds))}</code></div>
428
+ ${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>` : ''}
422
429
  ${dossierWarning(report, ['synthesis_suspect'])}
423
430
  ${dossier.recommendation.claimIds.length ? '' : '<div class="warns"><span class="warn">⚑ DEGRADED: recommendation has no stored graph anchor</span></div>'}
424
431
  </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)]))}`;
432
+ const claimChain = dossierTable(['Claim', 'Ruling', 'Evidence status', 'Depends on'], dossier.claimChain.map((claim) => [claim.text, claim.ruling, claim.evidenceStatus, readerClaimRefs(report, claim.dependsOn)]));
433
+ 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)]))}`;
427
434
  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('')
435
+ ? 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('')
429
436
  : `<p class="muted">${report.mode === 'quick' ? 'No cross-model disagreement analysis runs in quick mode.' : 'No genuine disagreements were stored.'}</p>`;
430
437
  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]))
438
+ ? 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]))
432
439
  : '<p class="muted">No CONCEDE, COUNTER, or NARROW event was recorded.</p>';
433
440
  const minority = `<h3>Minority report</h3>${report.minority.dissent.length
434
441
  ? `<ul class="checks">${report.minority.dissent.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`
@@ -437,18 +444,29 @@ function renderDossierIdeaBody(report) {
437
444
  <p class="lede">Decision-blocking status: <strong>${escapeHtml(report.minority.blocksDecision)}</strong></p>`;
438
445
  const disagreement = `${dossierWarning(report, ['single_model', 'low_diversity'])}${disagreementCards}<h3>Position changes</h3>${changes}${minority}`;
439
446
  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>`
447
+ ? `<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>`
441
448
  : '<h3>Shared concerns</h3><p class="muted">None recorded.</p>';
442
449
  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>`
450
+ ? `<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>`
444
451
  : '<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>` : ''}`;
452
+ const coverage = dossierTable(['Dimension', 'Status', 'Related claims'], dossier.coverage.map((item) => [item.label, item.status, readerClaimRefs(report, item.claimIds)]));
453
+ 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)]));
454
+ 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>'}`;
455
+ const missingDeliverables = (dossier.missingRequestedOutputs ?? []).length
456
+ ? `<div class="warns"><span class="warn">⚑ DEGRADED: requested output missing: ${escapeHtml(dossier.missingRequestedOutputs.join(', '))}</span></div>`
457
+ : '';
458
+ const backlog = dossier.featureBacklog;
459
+ const featurePriorities = backlog ? `<h3>Feature priorities</h3><div class="feature-groups">${[
460
+ ['MUST', 'Build for the first usable release', backlog.must],
461
+ ['SHOULD', 'Add after the core path is stable', backlog.should],
462
+ ['LATER', 'Keep out of the current build', backlog.later],
463
+ ].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>` : ''}` : '';
464
+ 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>` : '';
465
+ const actionPlan = `${missingDeliverables}${featurePriorities}${implementationPlan}<h3>Validation plan</h3>${experiments}`;
448
466
  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>`
467
+ ? `<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>`
450
468
  : `<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)]))}`;
469
+ 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)]))}`;
452
470
  const receipt = `<div class="receipt">
453
471
  <span>mode ${escapeHtml(report.mode)}</span><span>${report.receipt.calls}/${report.receipt.budget} provider calls</span>
454
472
  <span>discovery ${report.receipt.categories.discovery}</span><span>verification ${report.receipt.categories.verification}</span>
@@ -456,13 +474,16 @@ function renderDossierIdeaBody(report) {
456
474
  <span>by provider ${escapeHtml(Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none')}</span>
457
475
  <span>model time ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s</span>
458
476
  </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]));
477
+ const shownRisks = report.risks.slice(0, 8);
478
+ const risks = `${dossierTable(['Risk', 'Severity'], shownRisks.map((item) => [item.risk, item.severity]))}${report.risks.length > shownRisks.length
479
+ ? `<p class="muted">${report.risks.length - shownRisks.length} lower-severity items — more in the technical audit (full list in the stored JSON).</p>`
480
+ : ''}`;
460
481
  const questions = report.openQuestions.length
461
482
  ? `<ul class="checks">${report.openQuestions.map((question) => `<li>${escapeHtml(question)}</li>`).join('')}</ul>`
462
483
  : '<p class="muted">No verdict-flipping open question was recorded.</p>';
463
484
  const runDetails = `<article class="card">
464
485
  <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>
486
+ <details class="inline-fold compact"><summary>View original request</summary><p>${escapeHtml(report.task.original)}</p></details>
466
487
  <div class="field"><span class="fk">Normalized question</span><p>${escapeHtml(report.task.normalized)}</p></div>
467
488
  <div class="field"><span class="fk">Constraints</span><p>${escapeHtml(report.task.constraints.join('; ') || 'none recorded')}</p></div>
468
489
  <div class="field"><span class="fk">Success criteria</span><p>${escapeHtml(report.task.successCriteria.join('; ') || 'none recorded')}</p></div>
@@ -475,18 +496,48 @@ function renderDossierIdeaBody(report) {
475
496
  <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
497
  <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
498
  </div></details>`;
478
- return [
499
+ const readerBody = [
479
500
  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.'),
501
+ section('02', 'Action plan', actionPlan, 100, 'Requested product priorities, implementation milestones, and the smallest decisive validation test.'),
481
502
  section('03', 'Why this decision', claimChain, 140, 'The load-bearing claim chain behind the recommendation.'),
482
503
  section('04', 'What could change the decision', `<h3>Decision-sensitive facts</h3>${sensitivity}<h3>Strongest counter-case</h3>${counterCase}`, 180),
483
504
  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),
505
+ 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),
485
506
  section('07', 'Disagreement and dissent', disagreement, 300),
486
507
  section('08', 'What the council added', `${shared}${unique}<h3>Verified unique contributions</h3>${contributions}`, 340),
487
508
  section('09', 'Run details', runDetails, 380),
488
- section('10', 'Technical audit', technical, 420),
489
509
  ].join('');
510
+ return `${stripReaderClaimIds(readerBody, labelFor)}${section('10', 'Technical audit', technical, 420)}`;
511
+ }
512
+ function renderReaderBriefIdeaBody(report) {
513
+ const dossier = report.dossier;
514
+ const projection = buildReaderProjection(report);
515
+ const tone = dossier.recommendation.status === 'ACCEPTED' ? 'good'
516
+ : dossier.recommendation.status === 'REJECTED' ? 'risk' : 'caution';
517
+ const hero = `<section class="verdict tone-${tone} reveal" style="animation-delay:60ms">
518
+ <span class="pill">Recommendation</span>
519
+ <p class="verdict-text">${escapeHtml(projection.headline)}</p>
520
+ <p class="verdict-detail">${escapeHtml(projection.bottomLine)}</p>
521
+ </section>`;
522
+ const warnings = projection.warnings.length || projection.notices.length
523
+ ? `<div class="warns">${projection.warnings.map((warning) => `<span class="warn">⚑ ${escapeHtml(warning.message)}</span>`).join('')}${projection.notices.map((notice) => `<span class="warn">ⓘ ${escapeHtml(notice.message)}</span>`).join('')}</div>`
524
+ : '';
525
+ const snapshot = projection.snapshot ? section('', 'Decision numbers', `${dossierTable(['Metric', 'Value', 'What it means'], projection.snapshot.decisiveNumbers.map((item) => [item.label, item.value, item.meaning]))}${projection.snapshot.payback ? `<div class="action-callout"><span>Payback · ${escapeHtml(projection.snapshot.payback.status.replaceAll('_', ' '))}</span><p>${escapeHtml(projection.snapshot.payback.result)} — ${escapeHtml(projection.snapshot.payback.basis)}</p></div>` : ''}<h3>Options at a glance</h3>${dossierTable(['Path', 'Commitment', 'Basis', 'Trade-off'], projection.snapshot.options.map((item) => [item.label, item.commitment, item.commitmentKind.replace('_', ' '), item.tradeoff]))}${projection.snapshot.tripwire ? `<div class="action-callout"><span>Go/no-go tripwire</span><p><strong>${escapeHtml(projection.snapshot.tripwire.metric)}: ${escapeHtml(projection.snapshot.tripwire.threshold)}</strong> — ${escapeHtml(projection.snapshot.tripwire.decisionRule)}</p></div>` : ''}`, 80) : '';
526
+ const editorial = projection.sections.map((item, index) => section(String(index + 1).padStart(2, '0'), item.heading, `<p class="lede">${escapeHtml(item.summary)}</p>${item.bullets.length
527
+ ? `<ul class="reasons">${item.bullets.map((bullet) => `<li>${escapeHtml(bullet)}</li>`).join('')}</ul>`
528
+ : ''}`, 100 + index * 40)).join('');
529
+ const backlog = projection.featureBacklog;
530
+ const features = backlog ? section('', 'Feature priorities', `<div class="feature-groups">${[
531
+ ['MUST', 'Build for the first useful release', backlog.must],
532
+ ['SHOULD', 'Add after the golden path is stable', backlog.should],
533
+ ['LATER', 'Keep outside the current build', backlog.later],
534
+ ].filter(([, , items]) => items.length).map(([priority, description, items]) => `<section class="feature-group priority-${priority.toLowerCase()}"><header><div><span>${priority}</span><h4>${description}</h4></div><strong>${items.length}</strong></header><ul>${items.map((item) => `<li><div class="feature-title"><strong>${escapeHtml(item.feature)}</strong><span>${item.effort}</span></div><p>${escapeHtml(item.user_value)}</p><small>${escapeHtml(item.rationale)}</small></li>`).join('')}</ul></section>`).join('')}</div>${backlog.wont.length ? `<h3>Not in this scope</h3><ul class="checks">${backlog.wont.map((item) => `<li><strong>${escapeHtml(item.feature)}</strong> — ${escapeHtml(item.reason)}</li>`).join('')}</ul>` : ''}`, 340) : '';
535
+ const buildPlan = projection.implementationPlan ? section('', 'Build plan', `<ol class="milestone-list">${projection.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>`, 380) : '';
536
+ const caveats = projection.caveats.length ? section('', 'Top caveats', `<ul class="checks">${projection.caveats.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`, 420) : '';
537
+ const sources = projection.sources.length ? section('', 'Sources', `<ul class="agree">${projection.sources.map((source) => `<li><p>${source.url ? `<a href="${escapeHtml(source.url)}" rel="noopener noreferrer">${escapeHtml(source.label)}</a>` : escapeHtml(source.label)}</p>${source.citedFor.length ? `<small>Cited for: ${source.citedFor.map(escapeHtml).join('; ')}</small>` : ''}</li>`).join('')}</ul>`, 460) : '';
538
+ const nextStep = section('', 'Next step', `<div class="action-callout"><span>Do this now</span><p>${escapeHtml(projection.nextStep)}</p></div>`, 500);
539
+ const audit = `<details class="fold council-audit"><summary>Council audit — reasoning, evidence, dissent, and run receipt</summary><div class="fold-body">${renderDossierIdeaBody(report)}</div></details>`;
540
+ return `${hero}${warnings}${snapshot}${editorial}${features}${buildPlan}${caveats}${sources}${nextStep}${audit}`;
490
541
  }
491
542
  function renderLegacyIdeaBody(view) {
492
543
  const risks = view.risks ?? [];
@@ -877,19 +928,22 @@ function renderTechnical(view) {
877
928
  export function renderCouncilHtml(view) {
878
929
  const isIdea = view.workflow !== 'code-review';
879
930
  const quick = isIdea && view.mode === 'quick';
931
+ const hasReaderBrief = Boolean(view.decisionReport?.dossier.readerBrief);
880
932
  const kicker = quick ? 'aiki · quick analysis' : isIdea ? 'aiki · idea refinement' : 'aiki · code review';
881
933
  const title = isIdea && view.topic ? cleanTopic(view.topic) : (isIdea ? 'Idea refinement' : 'Code review');
882
934
  const panel = view.columns.map((c) => c.title);
883
- const metaBits = [
935
+ const metaBits = hasReaderBrief ? [] : [
884
936
  panel.length ? `${quick ? 'Analyst' : 'Panel'}: ${panel.join(' · ')}` : '',
885
937
  !quick && view.moderator ? `Chair: ${view.moderator}` : '',
886
938
  view.calls,
887
939
  ].filter(Boolean);
888
- const flags = view.flags.length
940
+ const flags = !hasReaderBrief && view.flags.length
889
941
  ? `<div class="warns">${view.flags.map((f) => `<span class="warn">⚑ ${escapeHtml(f.replaceAll('_', ' '))}</span>`).join('')}</div>`
890
942
  : '';
891
943
  const body = isIdea && view.decisionReport?.dossier
892
- ? renderDossierIdeaBody(view.decisionReport)
944
+ ? view.decisionReport.dossier.readerBrief
945
+ ? renderReaderBriefIdeaBody(view.decisionReport)
946
+ : renderDossierIdeaBody(view.decisionReport)
893
947
  : quick ? renderQuickIdeaBody(view) : isIdea ? renderIdeaBody(view) : renderReviewBody(view);
894
948
  // Embed the report as Markdown for the Copy button. Escape "<" so a "</script>" in content can't break out.
895
949
  const md = JSON.stringify(councilMarkdown(view)).replace(/</g, '\\u003c');
@@ -912,16 +966,16 @@ export function renderCouncilHtml(view) {
912
966
  *{box-sizing:border-box;}
913
967
  html{-webkit-text-size-adjust:100%;}
914
968
  body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.6;}
915
- main{max-width:880px;margin:0 auto;padding:34px 24px 90px;}
969
+ main{max-width:940px;margin:0 auto;padding:34px 24px 90px;}
916
970
  a{color:var(--accent);}
917
- h1{font-family:var(--serif);font-weight:600;letter-spacing:-.025em;}
971
+ h1{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
918
972
  h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
919
973
  p{margin:0 0 .6em;}
920
974
 
921
975
  /* masthead */
922
976
  .mast{border-bottom:1px solid var(--ink);padding-bottom:22px;margin-bottom:30px;}
923
977
  .kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);}
924
- .mast h1{font-size:clamp(32px,5.4vw,50px);line-height:1.05;margin:12px 0 18px;max-width:18ch;}
978
+ .mast h1{font-size:clamp(28px,4.6vw,42px);line-height:1.12;margin:12px 0 16px;}
925
979
  .mmeta{display:flex;flex-wrap:wrap;gap:7px;}
926
980
  .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;}
927
981
  .warns{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;}
@@ -933,12 +987,15 @@ p{margin:0 0 .6em;}
933
987
  .verdict::before{content:"";position:absolute;left:0;top:0;bottom:0;width:6px;}
934
988
  .tone-good::before{background:var(--good);} .tone-caution::before{background:var(--caution);} .tone-risk::before{background:var(--risk);}
935
989
  .pill{display:inline-block;font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.02em;
936
- padding:5px 13px;border-radius:100px;margin-bottom:14px;}
990
+ padding:5px 13px;border-radius:100px;}
937
991
  .tone-good .pill{background:var(--good-bg);color:var(--good);} .tone-caution .pill{background:var(--caution-bg);color:var(--caution);}
938
992
  .tone-risk .pill{background:var(--risk-bg);color:var(--risk);}
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;}
993
+ .decision-status{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px;}
994
+ .decision-status>span:last-child{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;color:var(--soft);text-transform:uppercase;}
995
+ .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;}
996
+ .verdict-detail{font-size:14.5px;line-height:1.62;color:var(--soft);margin:9px 0 0;max-width:76ch;}
940
997
  .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;}
998
+ .decision-numbers{margin:0 0 20px;}.snapshot-table{width:100%;border-collapse:collapse;font-size:13.5px;line-height:1.4;}
942
999
  .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
1000
  .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
1001
  .snapshot-table .number-value{font-family:var(--serif);font-size:17px;font-weight:700;color:var(--ink);white-space:nowrap;}
@@ -974,8 +1031,9 @@ p{margin:0 0 .6em;}
974
1031
  .decision-details{margin-top:14px;border-top:1px solid var(--line);padding-top:12px;}
975
1032
  .decision-details summary{cursor:pointer;font-family:var(--mono);font-size:11px;color:var(--soft);text-transform:uppercase;letter-spacing:.08em;}
976
1033
  .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;}
1034
+ .decision-evidence{margin-top:12px;border-top:1px solid var(--line);padding-top:12px;}
1035
+ .decision-evidence summary{cursor:pointer;font-family:var(--mono);font-size:10.5px;color:var(--faint);letter-spacing:.07em;text-transform:uppercase;}
1036
+ .decision-evidence ul{margin:10px 0 0;padding-left:20px;}.decision-evidence li{font-size:13px;color:var(--soft);margin-bottom:5px;}
979
1037
 
980
1038
  /* top action bar + copy */
981
1039
  .bar{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;gap:12px;
@@ -985,6 +1043,7 @@ p{margin:0 0 .6em;}
985
1043
  border:0;border-radius:8px;padding:8px 15px;transition:background .15s ease,transform .1s ease;}
986
1044
  .copy-btn:hover{filter:brightness(1.05);} .copy-btn:active{transform:translateY(1px);}
987
1045
  .copy-btn.ok{background:var(--good);}
1046
+ button:focus-visible,summary:focus-visible,a:focus-visible{outline:3px solid rgba(21,95,85,.28);outline-offset:3px;}
988
1047
 
989
1048
  /* chairman's reasoning */
990
1049
  .reasons{margin:0;padding:0;list-style:none;}
@@ -1061,12 +1120,30 @@ p{margin:0 0 .6em;}
1061
1120
  .score span{font-size:13.5px;color:var(--ink);}
1062
1121
  .score strong{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
1063
1122
  .score.contested{border-left:4px solid var(--caution);} .score.examined{border-left:4px solid var(--good);} .score.unexamined{border-left:4px solid var(--risk);}
1064
- .table-wrap{overflow-x:auto;}
1123
+ .table-wrap{overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin;}
1065
1124
  .data-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:13.5px;}
1066
1125
  .data-table th,.data-table td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:9px 10px;}
1067
1126
  .data-table th{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--soft);background:var(--paper);}
1068
1127
  .data-table tr:last-child td{border-bottom:0;}
1069
1128
  .data-table code{font-family:var(--mono);font-size:12px;color:var(--accent);}
1129
+ .feature-groups{display:grid;gap:14px;margin-bottom:26px;}
1130
+ .feature-group{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden;}
1131
+ .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);}
1132
+ .feature-group>header div>span{display:block;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.13em;color:var(--accent);}
1133
+ .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);}
1134
+ .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;}
1135
+ .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);}
1136
+ .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);}
1137
+ .priority-must{border-left:4px solid var(--accent);}.priority-should{border-left:4px solid var(--caution);}.priority-later{border-left:4px solid var(--slate);}
1138
+ .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;}
1139
+ .milestone-list>li:not(:last-child)::before{content:"";position:absolute;left:23px;top:37px;bottom:0;border-left:1px solid var(--line);}
1140
+ .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;}
1141
+ .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;}
1142
+ .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;}
1143
+ .experiment-list{display:grid;gap:12px;margin-bottom:14px;}.experiment-card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 18px;}
1144
+ .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);}
1145
+ .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;}
1146
+ .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);}
1070
1147
  .debate-card p{margin-bottom:8px;}
1071
1148
  .redteam{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
1072
1149
  .redteam>div{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;}
@@ -1100,7 +1177,14 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
1100
1177
  main{padding:34px 16px 60px;}
1101
1178
  .verdict{padding:24px 20px 20px;}
1102
1179
  .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;}
1180
+ .decision-status{align-items:flex-start;flex-direction:column;gap:8px;}.decision-status>span:last-child{text-align:left;}.pill{white-space:nowrap;}
1181
+ .snapshot-table{min-width:0;}.snapshot-table thead{display:none;}.snapshot-table tbody,.snapshot-table tr,.snapshot-table td{display:block;width:100%;}
1182
+ .snapshot-table tr{padding:12px 0;border-bottom:1px solid var(--line);}.snapshot-table td{padding:3px 8px;border:0;white-space:normal;}
1183
+ .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);}
1184
+ .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";}
1185
+ .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";}
1186
+ .payback-result{grid-template-columns:1fr;}.payback-result p{grid-column:1;}
1187
+ .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;}
1104
1188
  }
1105
1189
  @media (prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none;animation:none;}}
1106
1190
  @media print{.reveal{opacity:1;transform:none;animation:none;}.fold[open]{break-inside:avoid;}}
@@ -1109,7 +1193,7 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
1109
1193
  <body>
1110
1194
  <div class="bar">
1111
1195
  <span class="bar-label">${escapeHtml(kicker)}</span>
1112
- <button class="copy-btn" onclick="copyReport(this)">Copy report (Markdown)</button>
1196
+ <button id="copy-report" class="copy-btn">Copy report (Markdown)</button>
1113
1197
  </div>
1114
1198
  <main>
1115
1199
  <header class="mast">
@@ -1122,12 +1206,28 @@ footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-fa
1122
1206
  </main>
1123
1207
  <script>
1124
1208
  const REPORT_MD = ${md};
1125
- function copyReport(btn){
1126
- navigator.clipboard.writeText(REPORT_MD).then(function(){
1127
- var o=btn.textContent; btn.textContent='✓ Copied'; btn.classList.add('ok');
1128
- setTimeout(function(){ btn.textContent=o; btn.classList.remove('ok'); }, 1600);
1129
- }).catch(function(){ btn.textContent='Copy failed — select the text manually'; });
1209
+ async function copyReport(btn){
1210
+ var original=btn.textContent;
1211
+ try {
1212
+ var copied=false;
1213
+ if(navigator.clipboard && navigator.clipboard.writeText){
1214
+ try{ await navigator.clipboard.writeText(REPORT_MD); copied=true; }catch(error){}
1215
+ }
1216
+ if(!copied){
1217
+ var field=document.createElement('textarea');
1218
+ field.value=REPORT_MD; field.setAttribute('readonly','');
1219
+ field.style.position='fixed'; field.style.opacity='0';
1220
+ document.body.appendChild(field); field.select();
1221
+ copied=document.execCommand('copy'); field.remove();
1222
+ if(!copied) throw new Error('copy unavailable');
1223
+ }
1224
+ btn.textContent='✓ Copied'; btn.classList.add('ok');
1225
+ }catch(error){
1226
+ btn.textContent='Copy failed — select the text manually';
1227
+ }
1228
+ setTimeout(function(){ btn.textContent=original; btn.classList.remove('ok'); }, 1800);
1130
1229
  }
1230
+ document.getElementById('copy-report').addEventListener('click',function(){ copyReport(this); });
1131
1231
  </script>
1132
1232
  </body>
1133
1233
  </html>`;
@@ -15,7 +15,7 @@ import { extractJson } from '../providers/adapter-core.js';
15
15
  import { detect } from '../providers/detect.js';
16
16
  import { probeFlags } from '../providers/probe.js';
17
17
  import { replayKey } from '../storage/replay.js';
18
- import { callCategory, defaultBudgetFor, IDEA_MODE_PLANS, isOptionalStage, LEGACY_DEFAULT_BUDGET } from './modes.js';
18
+ import { callCategory, defaultBudgetFor, defaultDeadlineFor, IDEA_MODE_PLANS, isOptionalStage, LEGACY_DEFAULT_BUDGET } from './modes.js';
19
19
  /** Bracket a stage call with the TUI's start/end events (no-op headless). A thrown StageError marks
20
20
  * the row `failed` and re-propagates unchanged — the engine's failure handling is untouched. */
21
21
  export async function runStage(ctx, id, fn) {
@@ -100,7 +100,7 @@ export class RunCtx {
100
100
  this.budget = { limit: opts.budget ?? defaultBudgetFor(opts.workflow, this.mode), used: 0 };
101
101
  this.handles = new Map(opts.handles.map((h) => [h.id, h]));
102
102
  this.signal = opts.signal;
103
- this.deadlineMs = opts.deadlineMs ?? DEFAULT_DEADLINE_MS;
103
+ this.deadlineMs = opts.deadlineMs ?? defaultDeadlineFor(opts.workflow, this.mode);
104
104
  this.now = opts.now ?? Date.now;
105
105
  this.deadlineAt = this.now() + this.deadlineMs;
106
106
  this.replay = opts.replay;
@@ -174,7 +174,7 @@ export class RunCtx {
174
174
  timeoutMs: req.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS,
175
175
  expectJson: req.expectJson,
176
176
  readOnly: true,
177
- research: this.mode === 'research' && stage.startsWith('S4'),
177
+ research: this.workflow === 'idea-refinement' && this.mode !== 'quick' && stage.startsWith('S4'),
178
178
  signal: this.signal, // Ctrl+C kills the in-flight child (T8); undefined headless
179
179
  }, handle.flags);
180
180
  // Only REAL calls count toward the ledger/budget; a replayed call is free and already recorded in