aiki-cli 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +98 -7
  2. package/README.md +109 -30
  3. package/dist/bench/arms.js +10 -9
  4. package/dist/bench/harness.js +13 -10
  5. package/dist/bench/idea-lane-rotation.js +237 -0
  6. package/dist/bench/idea-v3-bench.js +506 -0
  7. package/dist/bench/idea-v3-rating.js +582 -0
  8. package/dist/bench/results.js +10 -3
  9. package/dist/bench/scoring/decision-insights.js +112 -0
  10. package/dist/bench/scoring/seeded-bugs.js +4 -0
  11. package/dist/cli/bench.js +180 -3
  12. package/dist/cli/doctor.js +56 -24
  13. package/dist/cli/index.js +31 -6
  14. package/dist/cli/resolve.js +7 -6
  15. package/dist/cli/resume.js +18 -0
  16. package/dist/cli/run.js +63 -8
  17. package/dist/council/view.js +378 -109
  18. package/dist/orchestration/calculations.js +97 -0
  19. package/dist/orchestration/context.js +37 -9
  20. package/dist/orchestration/decision-dossier.js +262 -0
  21. package/dist/orchestration/decision-graph.js +256 -0
  22. package/dist/orchestration/engine.js +5 -2
  23. package/dist/orchestration/evidence-pack.js +46 -0
  24. package/dist/orchestration/idea-lanes.js +20 -0
  25. package/dist/orchestration/jsonStage.js +72 -0
  26. package/dist/orchestration/legacy-idea-adapter.js +102 -0
  27. package/dist/orchestration/modes.js +33 -0
  28. package/dist/orchestration/preflight.js +183 -0
  29. package/dist/orchestration/quick-analysis.js +81 -0
  30. package/dist/orchestration/stages/cr-ladder.js +80 -0
  31. package/dist/orchestration/stages/s10-render.js +562 -150
  32. package/dist/orchestration/stages/s4-analyze.js +12 -7
  33. package/dist/orchestration/stages/s5-drift.js +9 -9
  34. package/dist/orchestration/stages/s6-positions.js +10 -0
  35. package/dist/orchestration/stages/s7-decision-graph.js +76 -0
  36. package/dist/orchestration/stages/s8-verify.js +153 -46
  37. package/dist/orchestration/stages/s8b-rebuttal.js +208 -0
  38. package/dist/orchestration/stages/s9-judge.js +329 -108
  39. package/dist/orchestration/stages/s9b-plan.js +85 -75
  40. package/dist/providers/codex.js +2 -1
  41. package/dist/providers/spawn.js +5 -0
  42. package/dist/schemas/index.js +572 -13
  43. package/dist/skills/idea-refinement/analyst.md +18 -14
  44. package/dist/skills/idea-refinement/economics-delivery.md +7 -0
  45. package/dist/skills/idea-refinement/market-adoption.md +7 -0
  46. package/dist/storage/runs.js +11 -4
  47. package/dist/tui/app.js +37 -13
  48. package/dist/tui/format.js +4 -5
  49. package/dist/tui/smart-entry.js +17 -1
  50. package/dist/tui/timeline.js +2 -4
  51. package/dist/workflows/code-review.js +4 -2
  52. package/dist/workflows/idea-refinement.js +110 -46
  53. package/package.json +12 -4
  54. package/dist/orchestration/stages/s0-grill.js +0 -79
  55. package/dist/orchestration/stages/s1-intent.js +0 -25
  56. package/dist/orchestration/stages/s2-misread.js +0 -76
  57. package/dist/orchestration/stages/s3-prompts.js +0 -55
  58. package/dist/orchestration/stages/s6-claims.js +0 -56
  59. package/dist/orchestration/stages/s7-disagreement.js +0 -134
@@ -0,0 +1,97 @@
1
+ const UNIT_ALIASES = {
2
+ ratios: '1', ratio: '1', percentage: '1', percent: '1', '%': '1', dimensionless: '1',
3
+ visitors: 'visitor', orders: 'order', customers: 'customer', users: 'user',
4
+ months: 'month', years: 'year', units: 'unit', items: 'item',
5
+ };
6
+ /** Keep unit meaning atomic. Model ledgers sometimes decorate currency tokens ("INR revenue") or
7
+ * use ordinary plurals; those are the same physical dimensions and must cancel algebraically. */
8
+ function canonicalUnitToken(token) {
9
+ const normalized = token.trim().toLowerCase().replace(/\s+/g, ' ');
10
+ const currency = normalized.match(/^(inr|usd|eur|gbp|jpy|aud|cad|cny|sgd|aed)(?:\b|$)/)?.[1];
11
+ if (currency)
12
+ return currency;
13
+ return UNIT_ALIASES[normalized] ?? normalized;
14
+ }
15
+ function unitPowers(unit) {
16
+ const powers = new Map();
17
+ let sign = 1;
18
+ for (const token of unit.split(/([*/])/).map((part) => part.trim()).filter(Boolean)) {
19
+ if (token === '*') {
20
+ sign = 1;
21
+ continue;
22
+ }
23
+ if (token === '/') {
24
+ sign = -1;
25
+ continue;
26
+ }
27
+ const canonical = canonicalUnitToken(token);
28
+ if (canonical !== '1')
29
+ powers.set(canonical, (powers.get(canonical) ?? 0) + sign);
30
+ }
31
+ return new Map([...powers].filter(([, power]) => power !== 0));
32
+ }
33
+ function sameUnits(left, right) {
34
+ return left.size === right.size && [...left].every(([unit, power]) => right.get(unit) === power);
35
+ }
36
+ function formatUnits(powers) {
37
+ const numerator = [];
38
+ const denominator = [];
39
+ for (const [unit, power] of [...powers].sort(([a], [b]) => a.localeCompare(b))) {
40
+ for (let i = 0; i < Math.abs(power); i++)
41
+ (power > 0 ? numerator : denominator).push(unit);
42
+ }
43
+ const top = numerator.join('*') || '1';
44
+ return denominator.length ? `${top}/${denominator.join('/')}` : top;
45
+ }
46
+ function resultUnit(operation, left, right) {
47
+ const leftPowers = unitPowers(left);
48
+ const rightPowers = unitPowers(right);
49
+ if (operation === 'ADD' || operation === 'SUBTRACT')
50
+ return sameUnits(leftPowers, rightPowers) ? formatUnits(leftPowers) : null;
51
+ const powers = new Map(leftPowers);
52
+ for (const [unit, power] of rightPowers) {
53
+ powers.set(unit, (powers.get(unit) ?? 0) + (operation === 'MULTIPLY' ? power : -power));
54
+ if (powers.get(unit) === 0)
55
+ powers.delete(unit);
56
+ }
57
+ return formatUnits(powers);
58
+ }
59
+ /** Recompute a declared ledger without eval/shell execution and report value/unit mismatches. */
60
+ export function evaluateCalculation(calculation) {
61
+ const values = new Map(calculation.inputs.map((input) => [input.id, { value: input.value, unit: input.unit }]));
62
+ const issues = [];
63
+ for (const step of calculation.steps) {
64
+ const left = values.get(step.left);
65
+ const right = values.get(step.right);
66
+ if (!left || !right) {
67
+ issues.push(`${step.id}: unknown operand reference`);
68
+ continue;
69
+ }
70
+ if (step.operation === 'DIVIDE' && right.value === 0) {
71
+ issues.push(`${step.id}: division by zero`);
72
+ continue;
73
+ }
74
+ const expected = step.operation === 'ADD' ? left.value + right.value
75
+ : step.operation === 'SUBTRACT' ? left.value - right.value
76
+ : step.operation === 'MULTIPLY' ? left.value * right.value
77
+ : left.value / right.value;
78
+ const unit = resultUnit(step.operation, left.unit, right.unit);
79
+ if (!unit)
80
+ issues.push(`${step.id}: ${step.operation} requires matching units (${left.unit} vs ${right.unit})`);
81
+ else if (!sameUnits(unitPowers(step.unit), unitPowers(unit)))
82
+ issues.push(`${step.id}: unit ${step.unit} does not match ${unit}`);
83
+ if (Math.abs(step.result - expected) > 1e-9 * Math.max(1, Math.abs(expected))) {
84
+ issues.push(`${step.id}: result ${step.result} does not match ${expected}`);
85
+ }
86
+ values.set(step.id, { value: expected, unit: unit ?? step.unit });
87
+ }
88
+ if (!calculation.steps.some((step) => step.id === calculation.result_step)) {
89
+ issues.push(`result_step: unknown step ${calculation.result_step}`);
90
+ }
91
+ return {
92
+ calculation_id: calculation.id,
93
+ claim_id: calculation.claim_id,
94
+ status: issues.length ? 'FAIL' : 'PASS',
95
+ issues,
96
+ };
97
+ }
@@ -15,6 +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
19
  /** Bracket a stage call with the TUI's start/end events (no-op headless). A thrown StageError marks
19
20
  * the row `failed` and re-propagates unchanged — the engine's failure handling is untouched. */
20
21
  export async function runStage(ctx, id, fn) {
@@ -29,17 +30,16 @@ export async function runStage(ctx, id, fn) {
29
30
  throw e;
30
31
  }
31
32
  }
32
- export const DEFAULT_BUDGET = 18; // full idea pipeline min = S0(1)+S1(1)+S2(3)+S3(1)+S4(2)+S7-group(1)+
33
- // S8(1)+S9(1)+S9b(1) = 12. But S2 runs 3 providers that EACH can repair, plus S9 + S9b anchor repairs —
34
- // a live run spent 3 repairs and died at 13 before S9b. 18 = 12 + ~6 repair headroom so the validation
35
- // plan actually renders; a true repair-storm still fails gracefully. Overridable via `--budget`/config.
33
+ /** Legacy/code-review fallback. Idea-refinement defaults are adaptive by explicit mode (R6). */
34
+ export const DEFAULT_BUDGET = LEGACY_DEFAULT_BUDGET;
36
35
  // §19 was 10 min; raised to 20 (user-authorized 2026-07-06). Evidence: a real Opus judge (S9) on a
37
36
  // 9-dispute idea ran ~360s and the whole run was ~14 min → the 10-min wall-clock aborted a legitimate run.
38
37
  export const DEFAULT_DEADLINE_MS = 20 * 60 * 1000; // wall-clock cap
39
- // §7.1 was 180s; raised to 300 (user-authorized 2026-07-06). The adapter retries a TIMEOUT once, so 180s
40
- // gave a 360s ceiling and the deep-reasoning judge blew it (observed exactly 360.1s TIMEOUT). 300s per
41
- // attempt covers the Opus judge; the wall-clock deadline above remains the outer bound.
42
- const DEFAULT_CALL_TIMEOUT_MS = 300_000;
38
+ // §7.1 was 180s; raised to 300 (user-authorized 2026-07-06) for the deep-reasoning judge; raised to 900
39
+ // (2026-07-13) after the spawn timeout became actually enforced and killed a LEGITIMATE deep call: codex's
40
+ // S4 analysis of a hard build case ran ~10 min to a valid output (run 20260713-1341, 13:44→13:54). 900s
41
+ // per attempt covers observed deep work; the wall-clock deadline above remains the outer bound.
42
+ const DEFAULT_CALL_TIMEOUT_MS = 900_000;
43
43
  export class BudgetExceeded extends Error {
44
44
  constructor(limit) {
45
45
  super(`call budget exhausted (limit ${limit})`);
@@ -68,12 +68,16 @@ export class StageError extends Error {
68
68
  export class RunCtx {
69
69
  runId;
70
70
  workflow;
71
+ mode;
71
72
  roles;
72
73
  writer;
73
74
  cwd;
74
75
  events;
76
+ evidencePack;
75
77
  budget;
76
78
  calls = [];
79
+ /** Logical provider-call stages, including resume cache hits. Used by bounded protocol caps. */
80
+ attemptedStages = [];
77
81
  /** §16 report-header flags accumulated by stages (S4 → low_diversity, S7 → low_diversity,
78
82
  * S9 → synthesis_suspect). Folded into meta.json at finalize by `buildMeta`. */
79
83
  flags = new Set();
@@ -87,11 +91,13 @@ export class RunCtx {
87
91
  constructor(opts) {
88
92
  this.runId = opts.runId;
89
93
  this.workflow = opts.workflow;
94
+ this.mode = opts.mode ?? 'council';
90
95
  this.roles = opts.roles;
91
96
  this.writer = opts.writer;
92
97
  this.cwd = opts.cwd;
93
98
  this.events = opts.events;
94
- this.budget = { limit: opts.budget ?? DEFAULT_BUDGET, used: 0 };
99
+ this.evidencePack = opts.evidencePack;
100
+ this.budget = { limit: opts.budget ?? defaultBudgetFor(opts.workflow, this.mode), used: 0 };
95
101
  this.handles = new Map(opts.handles.map((h) => [h.id, h]));
96
102
  this.signal = opts.signal;
97
103
  this.deadlineMs = opts.deadlineMs ?? DEFAULT_DEADLINE_MS;
@@ -125,12 +131,30 @@ export class RunCtx {
125
131
  if (this.now() > this.deadlineAt)
126
132
  throw new DeadlineExceeded(this.deadlineMs);
127
133
  }
134
+ /** Calls optional stages may still spend after preserving this mode's required tail calls. */
135
+ optionalCallsRemaining() {
136
+ if (this.workflow !== 'idea-refinement')
137
+ return 0;
138
+ const plan = IDEA_MODE_PLANS[this.mode];
139
+ const logicalUsed = this.attemptedStages.filter(isOptionalStage).length;
140
+ const protocolRoom = Math.max(0, plan.optionalCalls - logicalUsed);
141
+ const budgetRoom = Math.max(0, this.budget.limit - this.budget.used - plan.reservedCalls);
142
+ return Math.min(protocolRoom, budgetRoom);
143
+ }
144
+ /** Actual-call receipt. Resume replay is free and therefore intentionally absent. */
145
+ receipt() {
146
+ const receipt = { discovery: 0, verification: 0, repair: 0, planning: 0 };
147
+ for (const call of this.calls)
148
+ receipt[call.category ?? callCategory(call.stage)]++;
149
+ return receipt;
150
+ }
128
151
  /**
129
152
  * Make one budgeted provider call. Decrements budget (throws `BudgetExceeded` if it would go
130
153
  * over), records a `CallRecord`, and dumps the exact prompt + raw output under `raw/` (§15).
131
154
  */
132
155
  async call(handle, req, stage) {
133
156
  this.guard();
157
+ this.attemptedStages.push(stage);
134
158
  const seq = ++this.seq;
135
159
  await this.writer.writeRaw(`${stage}-${handle.id}-${seq}.prompt.txt`, req.prompt);
136
160
  // Resume (V6.3): if this exact (provider, prompt) already succeeded in the run we're resuming,
@@ -150,6 +174,7 @@ export class RunCtx {
150
174
  timeoutMs: req.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS,
151
175
  expectJson: req.expectJson,
152
176
  readOnly: true,
177
+ research: this.mode === 'research' && stage.startsWith('S4'),
153
178
  signal: this.signal, // Ctrl+C kills the in-flight child (T8); undefined headless
154
179
  }, handle.flags);
155
180
  // Only REAL calls count toward the ledger/budget; a replayed call is free and already recorded in
@@ -157,6 +182,7 @@ export class RunCtx {
157
182
  this.calls.push({
158
183
  provider: handle.id,
159
184
  stage,
185
+ category: callCategory(stage),
160
186
  durationMs: res.durationMs,
161
187
  ...(res.ok ? {} : { error: res.error }),
162
188
  });
@@ -188,6 +214,7 @@ export class RunCtx {
188
214
  return {
189
215
  run_id: this.runId,
190
216
  workflow: this.workflow,
217
+ ...(this.workflow === 'idea-refinement' ? { mode: this.mode } : {}),
191
218
  provider_versions,
192
219
  flag_profiles,
193
220
  roles,
@@ -195,6 +222,7 @@ export class RunCtx {
195
222
  calls: this.calls,
196
223
  call_count: this.calls.length,
197
224
  budget: { limit: this.budget.limit, used: this.budget.used },
225
+ receipt: this.receipt(),
198
226
  exit_status: exitStatus,
199
227
  aborted,
200
228
  ...(allFlags.length ? { flags: allFlags } : {}),
@@ -0,0 +1,262 @@
1
+ import { DISPLAY_NAME } from '../providers/types.js';
2
+ function cell(value) {
3
+ return value.replaceAll('\n', ' ').replaceAll('|', '\\|');
4
+ }
5
+ function refs(ids) {
6
+ return ids.length ? ids.map((id) => `\`${id}\``).join(', ') : 'none recorded';
7
+ }
8
+ function providerName(id) {
9
+ return id in DISPLAY_NAME ? DISPLAY_NAME[id] : id;
10
+ }
11
+ function degradation(report, flags) {
12
+ const active = flags.filter((flag) => report.flags.includes(flag));
13
+ return active.length ? [`> ⚠ DEGRADED: ${active.join(', ')}`, ''] : [];
14
+ }
15
+ function pct(value) {
16
+ return `${Math.round(value * 100)}%`;
17
+ }
18
+ function coverageLabel(value) {
19
+ return value >= 0.75 ? 'High' : value >= 0.5 ? 'Medium' : 'Low';
20
+ }
21
+ function councilRead(report) {
22
+ if (report.mode === 'quick')
23
+ return 'One structured analyst produced this result; no council, consensus, or independent-verification claim is being made.';
24
+ const scouts = report.models.filter((model) => model.roles.includes('scout')).length;
25
+ if (report.disagreements.length === 0) {
26
+ return `${scouts || 'The'} independent scout ${scouts === 1 ? 'analysis produced' : 'analyses produced'} no genuine opposing claim; the chair had less contested material to resolve.`;
27
+ }
28
+ const resolved = report.disagreements.filter((item) => item.status === 'RESOLVED').length;
29
+ return `${report.disagreements.length} genuine disagreement${report.disagreements.length === 1 ? '' : 's'} reached the chair; ${resolved} ${resolved === 1 ? 'was' : 'were'} resolved.`;
30
+ }
31
+ /** Canonical R7 Markdown. HTML and Copy-Markdown consume the same persisted dossier object. */
32
+ export function renderDecisionDossierMarkdown(report) {
33
+ const { dossier } = report;
34
+ const keyFindings = report.keyFindings?.length ? report.keyFindings : [dossier.recommendation.reason];
35
+ const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
36
+ const coverage = report.confidenceBreakdown.verificationCoverage;
37
+ const L = [report.mode === 'quick' ? '# Single-Model Decision Report' : '# Multi-Model Decision Report', ''];
38
+ L.push('## 1. Decision', '', ...degradation(report, ['synthesis_suspect']));
39
+ if (report.decisionSnapshot) {
40
+ L.push('### Decisive numbers', '', '| Metric | Value | What it means | Evidence |', '|---|---:|---|---|');
41
+ for (const item of report.decisionSnapshot.decisiveNumbers) {
42
+ L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} | ${refs(item.claimIds)} |`);
43
+ }
44
+ if (report.decisionSnapshot.payback) {
45
+ const payback = report.decisionSnapshot.payback;
46
+ L.push('', `**Payback — ${payback.status.replaceAll('_', ' ')}:** ${payback.result}`);
47
+ L.push(`Basis: ${payback.basis} (${refs(payback.claimIds)})`, '');
48
+ }
49
+ else
50
+ L.push('');
51
+ }
52
+ L.push(`**Recommendation:** ${dossier.recommendation.summary}`, '');
53
+ L.push(`**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of load-bearing claims independently verified.`);
54
+ L.push(coverage < 0.5
55
+ ? '> Low coverage means important inputs remain unchecked; it is not a probability that the recommendation is correct.'
56
+ : '> Evidence coverage measures independent checking; it is not a probability that the recommendation is correct.', '');
57
+ if (!report.decisionSnapshot) {
58
+ L.push('### Decisive findings', '');
59
+ for (const finding of keyFindings.slice(0, 3))
60
+ L.push(`- ${finding}`);
61
+ }
62
+ L.push('', '### Do this first', '', dossier.experiments.actions[0]?.action ?? 'No executable next step was produced.', '');
63
+ if (report.decisionSnapshot) {
64
+ L.push('### Options at a glance', '', '| Path | Commitment | Basis | Trade-off | Evidence |', '|---|---:|---|---|---|');
65
+ for (const option of report.decisionSnapshot.options) {
66
+ L.push(`| ${cell(option.label)} | ${cell(option.commitment)} | ${option.commitmentKind.replace('_', ' ')} | ${cell(option.tradeoff)} | ${refs(option.claimIds)} |`);
67
+ }
68
+ if (report.decisionSnapshot.tripwire) {
69
+ const tripwire = report.decisionSnapshot.tripwire;
70
+ L.push('', '### Go/no-go tripwire', '', `**${tripwire.metric}: ${tripwire.threshold}** — ${tripwire.decisionRule} (${refs(tripwire.claimIds)})`, '');
71
+ }
72
+ }
73
+ L.push('### What could overturn this', '', dossier.counterCase.available ? dossier.counterCase.reasoning : dossier.counterCase.reasoning, '');
74
+ L.push('### Critical unknowns', '');
75
+ if (criticalUnknowns.length)
76
+ for (const unknown of criticalUnknowns)
77
+ L.push(`- ${unknown}`);
78
+ else
79
+ L.push('- None recorded.');
80
+ L.push('', `**Critical warning:** ${report.verdict.criticalWarning ?? 'None recorded.'}`);
81
+ L.push(`**Council read:** ${councilRead(report)}`);
82
+ L.push(`Evidence anchors: ${refs(dossier.recommendation.claimIds)}`, '');
83
+ if (!dossier.recommendation.claimIds.length)
84
+ L.push('> ⚠ DEGRADED: recommendation has no stored graph anchor.', '');
85
+ L.push('<details>', '<summary>Decision conditions and technical confidence</summary>', '');
86
+ L.push(`- Decision state: ${dossier.recommendation.status}`);
87
+ L.push(`- Structural score: ${report.confidenceBreakdown.score}/100 (${report.confidenceBreakdown.label}); heuristic, not benchmark-calibrated.`);
88
+ L.push(`- Basis: verification ${pct(coverage)} · independent convergence ${pct(report.confidenceBreakdown.independentConvergence)} · evidence quality ${pct(report.confidenceBreakdown.evidenceQuality)} · stability ${pct(report.confidenceBreakdown.stability)} · critical-risk penalty −${report.confidenceBreakdown.criticalRiskPenalty}`);
89
+ if (dossier.recommendation.conditions.length) {
90
+ L.push('', 'Conditions:');
91
+ for (const condition of dossier.recommendation.conditions) {
92
+ L.push(`- ${condition.text} (${refs(condition.claimIds)})`);
93
+ }
94
+ }
95
+ L.push('', '</details>', '');
96
+ L.push('## 2. Action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
97
+ if (dossier.experiments.status === 'DEGRADED')
98
+ L.push(`> ⚠ DEGRADED: ${dossier.experiments.note}`, '');
99
+ if (dossier.experiments.actions.length) {
100
+ L.push('| # | Experiment | Why | Validates | Effort | Kill signal |', '|---|---|---|---|---|---|');
101
+ for (const action of dossier.experiments.actions) {
102
+ L.push(`| ${action.order} | ${cell(action.action)} | ${cell(action.why)} | ${action.validates} | ${action.effort} | ${cell(action.killSignal)} |`);
103
+ }
104
+ L.push('', dossier.experiments.note);
105
+ }
106
+ else
107
+ L.push('No executable experiment was produced.');
108
+ L.push('');
109
+ L.push('## 3. Why this decision', '');
110
+ if (dossier.claimChain.length) {
111
+ L.push('| Claim ID | Claim | Ruling | Evidence status | Depends on |', '|---|---|---|---|---|');
112
+ for (const claim of dossier.claimChain) {
113
+ L.push(`| ${claim.claimId} | ${cell(claim.text)} | ${claim.ruling} | ${claim.evidenceStatus} | ${refs(claim.dependsOn)} |`);
114
+ }
115
+ }
116
+ else
117
+ L.push('No graph-anchored decision chain was recorded.');
118
+ L.push('');
119
+ L.push('## 4. What could change the decision', '', '### Decision-sensitive facts', '');
120
+ if (dossier.sensitivity.length) {
121
+ L.push('| Claim ID | Fact | Sensitivity | If false | What would change it | Linked claims |', '|---|---|---|---|---|---|');
122
+ for (const item of dossier.sensitivity) {
123
+ L.push(`| ${item.claimId} | ${cell(item.fact)} | ${item.sensitivity} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} | ${refs(item.linkedClaimIds)} |`);
124
+ }
125
+ }
126
+ else
127
+ L.push('No verdict-sensitive graph node was recorded.');
128
+ L.push('', '### Strongest counter-case', '');
129
+ if (dossier.counterCase.available) {
130
+ L.push(dossier.counterCase.reasoning, '', `Evidence anchors: ${refs(dossier.counterCase.claimIds)}`);
131
+ }
132
+ else
133
+ L.push(`> ⚠ DEGRADED: ${dossier.counterCase.reasoning}`);
134
+ L.push('');
135
+ L.push('## 5. Evidence and verification', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']));
136
+ if (dossier.evidence.length) {
137
+ L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
138
+ for (const evidence of dossier.evidence) {
139
+ L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${evidence.verificationStatus} | ${refs(evidence.claimIds)} |`);
140
+ }
141
+ }
142
+ else
143
+ L.push('No evidence cards were stored.');
144
+ L.push('');
145
+ L.push('## 6. Risks, gaps, and open questions', '', '### Risks', '');
146
+ if (report.risks.length) {
147
+ L.push('| Risk | Severity |', '|---|---|');
148
+ for (const risk of report.risks)
149
+ L.push(`| ${cell(risk.risk)} | ${risk.severity} |`);
150
+ }
151
+ else
152
+ L.push('No material risk was recorded.');
153
+ L.push('', '### Coverage', '');
154
+ if (dossier.coverage.length) {
155
+ L.push('| Dimension | Status | Claim IDs |', '|---|---|---|');
156
+ for (const item of dossier.coverage)
157
+ L.push(`| ${cell(item.label)} | ${item.status} | ${refs(item.claimIds)} |`);
158
+ }
159
+ else
160
+ L.push('No rubric coverage ledger was recorded.');
161
+ L.push('', '### Open questions', '');
162
+ if (report.openQuestions.length) {
163
+ for (const question of report.openQuestions)
164
+ L.push(`- ${question}`);
165
+ }
166
+ else
167
+ L.push('No verdict-flipping open question was recorded.');
168
+ L.push('');
169
+ L.push('## 7. Disagreement and dissent', '', ...degradation(report, ['single_model', 'low_diversity']));
170
+ if (report.disagreements.length) {
171
+ for (const disagreement of report.disagreements) {
172
+ L.push(`- **${disagreement.id}: ${disagreement.topic}** — ${disagreement.status}; ${disagreement.ruling}.`);
173
+ for (const side of disagreement.sides)
174
+ L.push(` - ${side.stance} (${side.providers.map(providerName).join(', ')}): ${side.reasoning.join(' ')}`);
175
+ if (disagreement.reasoning)
176
+ L.push(` - Why: ${disagreement.reasoning}`);
177
+ }
178
+ }
179
+ else
180
+ L.push(report.mode === 'quick' ? 'No cross-model disagreement analysis runs in quick mode.' : 'No genuine disagreements were stored.');
181
+ L.push('', '### Position changes', '');
182
+ if (dossier.positionChanges.length) {
183
+ L.push('| Event | Claim | Responder | Change | Evidence | Detail |', '|---|---|---|---|---|---|');
184
+ for (const event of dossier.positionChanges) {
185
+ L.push(`| ${event.eventId} | ${event.claimId} | ${providerName(event.responder)} | ${event.response} | ${refs(event.evidenceIds)} | ${cell(event.narrowedProposition ?? event.reasoning)} |`);
186
+ }
187
+ }
188
+ else
189
+ L.push('No `CONCEDE`, `COUNTER`, or `NARROW` event was recorded.');
190
+ L.push('', '### Minority report', '');
191
+ if (report.minority.dissent.length) {
192
+ for (const item of report.minority.dissent)
193
+ L.push(`- ${item}`);
194
+ }
195
+ else
196
+ L.push('No minority dissent was recorded.');
197
+ for (const item of report.minority.uniqueOppositions)
198
+ L.push(`- ${providerName(item.provider)} uniquely opposed: ${item.proposition}`);
199
+ L.push(`- Decision-blocking status: ${report.minority.blocksDecision}`, '');
200
+ L.push('## 8. What the council added', '');
201
+ if (dossier.sharedConcerns.length) {
202
+ L.push('Shared concerns:');
203
+ for (const item of dossier.sharedConcerns)
204
+ L.push(`- **${item.claimId}** ${item.text} — ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
205
+ }
206
+ else
207
+ L.push('Shared concerns: none recorded.');
208
+ L.push('');
209
+ if (dossier.uniqueSupportedInsights.length) {
210
+ L.push('Unique supported insights:');
211
+ for (const item of dossier.uniqueSupportedInsights)
212
+ L.push(`- **${item.claimId}** ${item.text} — ${providerName(item.providerId)}; ${item.verificationStatus}.`);
213
+ }
214
+ else
215
+ L.push('Unique supported insights: none recorded.');
216
+ L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
217
+ L.push('Only unique claims that survived independent verification receive credit.', '');
218
+ L.push('| Provider | Verified unique claim IDs | Count |', '|---|---|---|');
219
+ for (const contribution of dossier.contributions) {
220
+ L.push(`| ${contribution.name} | ${refs(contribution.verifiedUniqueClaimIds)} | ${contribution.verifiedUniqueClaimIds.length} |`);
221
+ }
222
+ L.push('');
223
+ L.push('## 9. Run details', '');
224
+ L.push(`- Report ID: \`${report.reportId}\``);
225
+ L.push(`- Generated: ${report.generatedAt}`);
226
+ L.push(`- Original task: ${report.task.original}`);
227
+ L.push(`- Normalized question: ${report.task.normalized}`);
228
+ if (report.task.confirmation)
229
+ L.push(`- User confirmation: ${report.task.confirmation}`);
230
+ L.push(`- Constraints: ${report.task.constraints.join('; ') || 'none recorded'}`);
231
+ L.push(`- Success criteria: ${report.task.successCriteria.join('; ') || 'none recorded'}`);
232
+ L.push(`- Models and roles: ${report.models.map((model) => `${model.name} (${model.roles.join(', ')})`).join(' · ') || 'none recorded'}`);
233
+ L.push(`- Mode: ${report.mode}`);
234
+ L.push(`- Provider calls: ${report.receipt.calls}/${report.receipt.budget}`);
235
+ L.push(`- Categories: discovery ${report.receipt.categories.discovery} · verification ${report.receipt.categories.verification} · repair ${report.receipt.categories.repair} · planning ${report.receipt.categories.planning}`);
236
+ L.push(`- By provider: ${Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none'}`);
237
+ L.push(`- Recorded model time: ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s`);
238
+ L.push(`- Degradation flags: ${report.flags.join(', ') || 'none'}`, '');
239
+ L.push('## 10. Technical audit', '');
240
+ L.push('<details>', '<summary>Original submissions and graph events</summary>', '');
241
+ for (const submission of dossier.technical.submissions) {
242
+ L.push(`- **${submission.name}:** ${submission.strongestVersion} (${refs(submission.positionIds)})`);
243
+ }
244
+ L.push('', 'Original positions:');
245
+ for (const position of dossier.technical.positions) {
246
+ L.push(`- ${position.id} [${providerName(position.provider)} ${position.stance}] ${position.proposition}; evidence ${refs(position.evidenceIds)}`);
247
+ }
248
+ if (!dossier.technical.positions.length)
249
+ L.push('- none');
250
+ L.push('', 'Graph edges:');
251
+ for (const edge of dossier.technical.edges)
252
+ L.push(`- ${edge.from} —${edge.type}→ ${edge.to}`);
253
+ if (!dossier.technical.edges.length)
254
+ L.push('- none');
255
+ L.push('', 'Position-change events:');
256
+ for (const event of dossier.technical.events)
257
+ L.push(`- ${event.eventId}: ${providerName(event.responder)} ${event.response} ${event.claimId} — ${event.reasoning}`);
258
+ if (!dossier.technical.events.length)
259
+ L.push('- none');
260
+ L.push('', '</details>', '');
261
+ return L.join('\n');
262
+ }