aiki-cli 0.3.1 → 0.3.3

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 +80 -5
  2. package/README.md +73 -15
  3. package/dist/bench/idea-v3-bench.js +104 -5
  4. package/dist/bench/idea-v3-rating.js +159 -4
  5. package/dist/cli/bench.js +5 -5
  6. package/dist/cli/index.js +12 -2
  7. package/dist/cli/resume.js +20 -0
  8. package/dist/cli/run.js +76 -5
  9. package/dist/cli/serve.js +48 -0
  10. package/dist/config/config.js +7 -2
  11. package/dist/council/view.js +64 -15
  12. package/dist/orchestration/auto-profile.js +97 -0
  13. package/dist/orchestration/claim-groups.js +56 -0
  14. package/dist/orchestration/context.js +69 -6
  15. package/dist/orchestration/decision-dossier.js +450 -89
  16. package/dist/orchestration/decision-graph.js +33 -2
  17. package/dist/orchestration/engine.js +8 -4
  18. package/dist/orchestration/evidence-origin.js +17 -0
  19. package/dist/orchestration/jsonStage.js +47 -5
  20. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  21. package/dist/orchestration/modes.js +18 -10
  22. package/dist/orchestration/preflight.js +36 -3
  23. package/dist/orchestration/quick-analysis.js +29 -7
  24. package/dist/orchestration/sanitize-paths.js +10 -0
  25. package/dist/orchestration/stages/s10-render.js +196 -84
  26. package/dist/orchestration/stages/s4-analyze.js +10 -2
  27. package/dist/orchestration/stages/s4b-challenge.js +97 -0
  28. package/dist/orchestration/stages/s5-drift.js +13 -3
  29. package/dist/orchestration/stages/s6-positions.js +18 -0
  30. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  31. package/dist/orchestration/stages/s8-verify.js +66 -12
  32. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  33. package/dist/orchestration/stages/s9-judge.js +52 -14
  34. package/dist/orchestration/stages/s9b-plan.js +227 -48
  35. package/dist/orchestration/url-sources.js +21 -0
  36. package/dist/providers/adapter-core.js +1 -1
  37. package/dist/providers/claude.js +18 -0
  38. package/dist/schemas/index.js +112 -3
  39. package/dist/serve/flight-deck.js +830 -0
  40. package/dist/serve/followup.js +50 -0
  41. package/dist/serve/frames.js +168 -0
  42. package/dist/serve/gates.js +72 -0
  43. package/dist/serve/projections.js +283 -0
  44. package/dist/serve/server.js +219 -0
  45. package/dist/serve/threads.js +145 -0
  46. package/dist/serve-ui/Five.png +0 -0
  47. package/dist/serve-ui/app.js +820 -0
  48. package/dist/serve-ui/index.html +171 -0
  49. package/dist/serve-ui/workspace.css +662 -0
  50. package/dist/skills/idea-refinement/analyst.md +5 -5
  51. package/dist/skills/idea-refinement/chair.md +18 -0
  52. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  53. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  54. package/dist/skills/idea-refinement/planner.md +25 -19
  55. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  56. package/dist/skills/idea-refinement/verifier.md +17 -0
  57. package/dist/storage/runs.js +2 -1
  58. package/dist/workflows/idea-refinement.js +130 -24
  59. package/package.json +2 -2
@@ -1,4 +1,6 @@
1
1
  import { DISPLAY_NAME } from '../providers/types.js';
2
+ import { evidenceOrigin } from './evidence-origin.js';
3
+ import { sanitizeLocalPaths } from './sanitize-paths.js';
2
4
  function cell(value) {
3
5
  return value.replaceAll('\n', ' ').replaceAll('|', '\\|');
4
6
  }
@@ -12,33 +14,70 @@ function clipClaim(text, max = 96) {
12
14
  const boundary = clipped.lastIndexOf(' ');
13
15
  return `${clipped.slice(0, boundary > max * 0.65 ? boundary : max - 1).trimEnd()}…`;
14
16
  }
17
+ /** ≤60-char noun-ish handle for a claim: drop a leading "Verdict:", keep the first clause, clip at a word. */
18
+ export function claimShortLabel(text, max = 60) {
19
+ const base = text.replace(/^\s*Verdict:\s*/i, '').split(/(?<=\S)[;—]|(?<=\.)\s/)[0].trim();
20
+ if (base.length <= max)
21
+ return base;
22
+ const clipped = base.slice(0, max);
23
+ const boundary = clipped.lastIndexOf(' ');
24
+ return `${clipped.slice(0, boundary > max * 0.6 ? boundary : max).trimEnd()}…`;
25
+ }
26
+ /** Human token summary: "~18.4k in / ~3.2k out (2 calls estimated)". `~` iff any call estimated. */
27
+ export function formatTokenLine(t) {
28
+ const k = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n));
29
+ const tilde = t.estimatedCalls > 0 ? '~' : '';
30
+ const suffix = t.estimatedCalls > 0 ? ` (${t.estimatedCalls} calls estimated)` : '';
31
+ return `${tilde}${k(t.inputTokens)} in / ${tilde}${k(t.outputTokens)} out${suffix}`;
32
+ }
33
+ /** id → claim text lookup for stripReaderClaimIds substitution; shared by readerClaimLabel's fallback and the Markdown/HTML renderers. */
34
+ export function claimLookup(report) {
35
+ return (id) => report.claims.find((claim) => claim.id === id)?.text ?? null;
36
+ }
15
37
  /** Human-readable labels for reader-facing evidence links. Raw ids stay in the technical audit/JSON. */
16
38
  export function readerClaimLabel(report, id) {
17
39
  const claim = report.claims.find((item) => item.id === id);
18
40
  if (claim)
19
- return clipClaim(claim.text);
20
- return /^G\d+$/.test(id) ? 'Supporting claim' : clipClaim(stripReaderClaimIds(id));
41
+ return claimShortLabel(claim.text);
42
+ return /^G\d+$/.test(id) ? 'Supporting claim' : clipClaim(stripReaderClaimIds(id, claimLookup(report)));
21
43
  }
22
44
  export function readerClaimRefs(report, ids) {
23
45
  return ids.length ? ids.map((id) => readerClaimLabel(report, id)).join('; ') : 'none recorded';
24
46
  }
25
- /** Remove internal graph notation from prose without expanding a short id into a paragraph. */
26
- export function stripReaderClaimIds(text) {
27
- return text
28
- .replace(/\s*\((?:\s*`?G\d+`?\s*,?)+\s*\)/g, '')
29
- .replace(/\bG\d+\s+assumes\b/g, 'This assumes')
30
- .replace(/\bG\d+\b/g, '')
31
- .replace(/\(\s*\)/g, '')
32
- .replace(/\s+([,.;:])/g, '$1')
33
- .replace(/[ \t]{2,}/g, ' ')
34
- .trim();
47
+ /** Remove or substitute internal graph notation in reader prose. With a lookup, ids become quoted short labels
48
+ * instead of vanishing (never bare-delete); without one (no lookup available), ids are dropped — the legacy
49
+ * behavior, kept only for callers that have no claim table to look up against. `sanitizeLabel` escapes each
50
+ * injected label for the caller's output format (markdown passes `cell`; HTML passes nothing — raw). */
51
+ export function stripReaderClaimIds(text, labelFor, sanitizeLabel = (label) => label) {
52
+ const label = (id) => {
53
+ const hit = labelFor?.(id);
54
+ return hit ? `"${sanitizeLabel(claimShortLabel(hit))}"` : 'a related claim';
55
+ };
56
+ let out = text
57
+ // whole parenthetical citation groups — comma OR slash separated — vanish entirely: (G19), (G7/G13), (G1, G2)
58
+ .replace(/\s*\((?:\s*`?G\d+`?\s*[,/]?\s*)+\)/g, '')
59
+ // "G21's kill criteria" → `"the spike kill-gate"'s kill criteria` — keep the possessive on the label itself
60
+ // rather than injecting filler noun text, so it reads correctly for any noun that follows.
61
+ .replace(/\bG(\d+)'s\b/g, (_, n) => (labelFor ? `${label(`G${n}`)}'s` : ''));
62
+ out = labelFor
63
+ ? out.replace(/\bG\d+\b/g, (id) => label(id))
64
+ : out.replace(/\bG\d+\s+assumes\b/g, 'This assumes').replace(/\bG\d+\b/g, '');
65
+ return out.replace(/\s{2,}/g, ' ').replace(/\s+([.,;:])/g, '$1').trim();
35
66
  }
36
67
  function providerName(id) {
37
68
  return id in DISPLAY_NAME ? DISPLAY_NAME[id] : id;
38
69
  }
70
+ const FLAG_EXPLAIN = {
71
+ synthesis_suspect: 'the chair output needed a deterministic repair; phrasing may be less reliable than the underlying graph',
72
+ headless_intent: 'no human confirmed the interpretation; documented defaults were used',
73
+ weak_seat: 'one scout seat contributed far less evidenced material than the other; treat convergence cautiously',
74
+ source_fallback_search: 'a supplied page blocked direct reading; the source-investigation seat searched for accessible sources instead',
75
+ deliverable_gap: 'one or more surviving scouts omitted a requested feature or implementation proposal',
76
+ plan_partial: 'the answer planner omitted part of the requested deliverables; the report shows what was produced',
77
+ };
39
78
  function degradation(report, flags) {
40
79
  const active = flags.filter((flag) => report.flags.includes(flag));
41
- return active.length ? [`> ⚠ DEGRADED: ${active.join(', ')}`, ''] : [];
80
+ return active.flatMap((flag) => [`> ⚠ DEGRADED: ${flag}${FLAG_EXPLAIN[flag] ? ` — ${FLAG_EXPLAIN[flag]}.` : ''}`, '']);
42
81
  }
43
82
  function pct(value) {
44
83
  return `${Math.round(value * 100)}%`;
@@ -47,6 +86,12 @@ function coverageLabel(value) {
47
86
  return value >= 0.75 ? 'High' : value >= 0.5 ? 'Medium' : 'Low';
48
87
  }
49
88
  function councilRead(report) {
89
+ if (report.fastPath)
90
+ return 'Single-pass analysis; council escalation was not required.';
91
+ if (report.adaptiveAuto)
92
+ return report.autoEscalationReasons?.length
93
+ ? `The primary analysis tripped structural gates; two task readings checked the interpretation${report.autoChallengeUsed ? ', and one targeted challenger checked eligible claims' : '; no challenger could add information'}. No full council was convened.`
94
+ : 'Two task readings informed one primary decision analyst. No full council was convened.';
50
95
  if (report.mode === 'quick')
51
96
  return 'One structured analyst produced this result; no council, consensus, or independent-verification claim is being made.';
52
97
  const scouts = report.models.filter((model) => model.roles.includes('scout')).length;
@@ -56,12 +101,246 @@ function councilRead(report) {
56
101
  const resolved = report.disagreements.filter((item) => item.status === 'RESOLVED').length;
57
102
  return `${report.disagreements.length} genuine disagreement${report.disagreements.length === 1 ? '' : 's'} reached the chair; ${resolved} ${resolved === 1 ? 'was' : 'were'} resolved.`;
58
103
  }
59
- /** Canonical R7 Markdown. HTML and Copy-Markdown consume the same persisted dossier object. */
60
- export function renderDecisionDossierMarkdown(report) {
104
+ export function sanitizeReaderText(text, labelFor) {
105
+ return stripReaderClaimIds(text, labelFor)
106
+ .replace(/\b(?:G|C|D)\d+\b/g, '')
107
+ // v6: UPPERCASE-only — a leaked internal enum is rewritten; ordinary English ("the rules are
108
+ // unverified") is the model's own prose and must survive (f740's "confirmed-unverified" became
109
+ // "confirmed-not yet confirmed" under the old /gi).
110
+ .replace(/\bUNVERIFIABLE\b/g, 'not independently confirmed')
111
+ .replace(/\bUNVERIFIED\b/g, 'not yet confirmed')
112
+ .replace(/\bPARTIAL\b/g, 'partly supported')
113
+ .replace(/\bCONFLICTED\b/g, 'supported by conflicting evidence')
114
+ .replace(/\bstructural score\b/gi, 'confidence estimate')
115
+ .replace(/\bevidence coverage\b/gi, 'source strength')
116
+ .replace(/\bverification coverage\b/gi, 'source strength')
117
+ .replace(/\b(?:claim|graph) ids?\b/gi, 'internal references')
118
+ .replace(/\b(?:provider|model) calls?\b/gi, 'analysis steps')
119
+ .replace(/\banswer editor\b/gi, 'answer synthesis')
120
+ .replace(/\breader_brief\b/gi, 'reader summary')
121
+ .replace(/\s{2,}/g, ' ')
122
+ .replace(/\s+([.,;:])/g, '$1')
123
+ .trim();
124
+ }
125
+ export function cleanReaderText(report, text) {
126
+ return sanitizeReaderText(text, claimLookup(report));
127
+ }
128
+ const MATERIAL_WARNING_ORDER = [
129
+ 'synthesis_suspect', 'low_diversity', 'weak_seat', 'deliverable_gap', 'plan_skipped',
130
+ 'plan_fallback', 'plan_partial', 'headless_intent', 'verification_skipped', 'research_ungrounded',
131
+ ];
132
+ const READER_WARNING = {
133
+ synthesis_suspect: 'The final synthesis needed repair; treat its wording cautiously.',
134
+ low_diversity: 'Independent diversity was reduced because one scout seat had to be resampled.',
135
+ weak_seat: 'One scout contributed much less evidenced material; treat convergence cautiously.',
136
+ deliverable_gap: 'At least one scout omitted a requested feature or implementation proposal.',
137
+ plan_skipped: 'The answer-planning step was skipped because its call budget was unavailable.',
138
+ plan_fallback: 'The answer planner failed validation; this report uses a deterministic fallback.',
139
+ plan_partial: 'The answer planner omitted part of the requested deliverables; the report shows what was produced.',
140
+ headless_intent: 'No person confirmed the interpreted request; documented defaults were used.',
141
+ verification_skipped: 'Independent verification was skipped; important factual claims may remain unchecked.',
142
+ research_ungrounded: 'Source investigation did not produce a cited public source; treat current claims as ungrounded.',
143
+ };
144
+ export function isDecisionSnapshotRelevant(snapshot) {
145
+ if (!snapshot)
146
+ return false;
147
+ const allUnknown = snapshot.options.every((option) => option.commitmentKind === 'UNKNOWN');
148
+ const paybackUnavailable = !snapshot.payback || snapshot.payback.status === 'NOT_COMPUTABLE';
149
+ return !(allUnknown && paybackUnavailable);
150
+ }
151
+ function safeHttpUrl(raw) {
152
+ if (!raw)
153
+ return undefined;
154
+ try {
155
+ const url = new URL(raw);
156
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : undefined;
157
+ }
158
+ catch {
159
+ return undefined;
160
+ }
161
+ }
162
+ function looksLocalPath(value) {
163
+ if (!value)
164
+ return false;
165
+ return /^file:/i.test(value)
166
+ || /^(?:\/|\.\.?[\\/]|[A-Za-z]:[\\/])/.test(value)
167
+ || /^(?:[^:/\\]+[\\/])+[^/\\]+$/.test(value);
168
+ }
169
+ /** One cleaned reader contract consumed by Markdown, HTML, and terminal renderers. */
170
+ export function buildReaderProjection(report) {
171
+ const brief = report.dossier.readerBrief;
172
+ const clean = (text) => cleanReaderText(report, text);
173
+ const sources = [];
174
+ const sourceIndexByKey = new Map();
175
+ for (const id of brief.source_ids) {
176
+ const source = report.dossier.evidence.find((item) => item.id === id);
177
+ if (!source)
178
+ continue;
179
+ const url = safeHttpUrl(source.url) ?? safeHttpUrl(source.source);
180
+ const title = source.title && !looksLocalPath(source.title) ? clean(source.title) : undefined;
181
+ const label = title ?? (url ? url
182
+ : source.sourceKind === 'MODEL_KNOWLEDGE' ? 'Background model knowledge'
183
+ : source.sourceKind === 'USER' || looksLocalPath(source.source) ? 'User-supplied material' : 'Source material');
184
+ const key = url ? `url:${url.toLowerCase()}` : title ? `id:${source.id}` : `label:${label.toLowerCase()}`;
185
+ const citedFor = [...new Set(source.claimIds.flatMap((claimId) => {
186
+ const claim = report.claims.find((item) => item.id === claimId);
187
+ return claim ? [claimShortLabel(clean(claim.text))] : [];
188
+ }))].slice(0, 3);
189
+ const existingIndex = sourceIndexByKey.get(key);
190
+ if (existingIndex !== undefined) {
191
+ sources[existingIndex].citedFor = [...new Set([...sources[existingIndex].citedFor, ...citedFor])].slice(0, 3);
192
+ continue;
193
+ }
194
+ sourceIndexByKey.set(key, sources.length);
195
+ sources.push({ id: source.id, label, ...(url ? { url } : {}), citedFor });
196
+ }
197
+ const cleanBacklog = report.dossier.featureBacklog ? {
198
+ must: report.dossier.featureBacklog.must.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
199
+ should: report.dossier.featureBacklog.should.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
200
+ later: report.dossier.featureBacklog.later.map((item) => ({ ...item, feature: clean(item.feature), user_value: clean(item.user_value), rationale: clean(item.rationale) })),
201
+ wont: report.dossier.featureBacklog.wont.map((item) => ({ feature: clean(item.feature), reason: clean(item.reason) })),
202
+ } : undefined;
203
+ const cleanPlan = report.dossier.implementationPlan ? {
204
+ milestones: report.dossier.implementationPlan.milestones.map((item) => ({
205
+ ...item,
206
+ timebox: clean(item.timebox),
207
+ outcome: clean(item.outcome),
208
+ tasks: item.tasks.map(clean),
209
+ acceptance_test: clean(item.acceptance_test),
210
+ })),
211
+ } : undefined;
212
+ const snapshot = isDecisionSnapshotRelevant(report.decisionSnapshot) ? {
213
+ decisiveNumbers: report.decisionSnapshot.decisiveNumbers.map((item) => ({ label: clean(item.label), value: clean(item.value), meaning: clean(item.meaning) })),
214
+ ...(report.decisionSnapshot.payback ? { payback: {
215
+ status: report.decisionSnapshot.payback.status,
216
+ result: clean(report.decisionSnapshot.payback.result),
217
+ basis: clean(report.decisionSnapshot.payback.basis),
218
+ } } : {}),
219
+ options: report.decisionSnapshot.options.map((item) => ({
220
+ label: clean(item.label), commitment: clean(item.commitment), commitmentKind: item.commitmentKind, tradeoff: clean(item.tradeoff),
221
+ })),
222
+ ...(report.decisionSnapshot.tripwire ? { tripwire: {
223
+ metric: clean(report.decisionSnapshot.tripwire.metric),
224
+ threshold: clean(report.decisionSnapshot.tripwire.threshold),
225
+ decisionRule: clean(report.decisionSnapshot.tripwire.decisionRule),
226
+ } } : {}),
227
+ } : undefined;
228
+ return {
229
+ headline: clean(brief.headline),
230
+ bottomLine: clean(brief.bottom_line),
231
+ sections: brief.sections.map((section) => ({ heading: clean(section.heading), summary: clean(section.summary), bullets: section.bullets.map(clean) })),
232
+ featureBacklog: cleanBacklog,
233
+ implementationPlan: cleanPlan,
234
+ caveats: brief.caveats.map(clean),
235
+ warnings: MATERIAL_WARNING_ORDER.filter((flag) => report.flags.includes(flag)).map((flag) => ({ flag, message: READER_WARNING[flag] })),
236
+ notices: report.flags.includes('source_fallback_search')
237
+ ? [{ flag: 'source_fallback_search', message: 'Search attempted: a supplied page could not be read directly, so the source-investigation scout searched for accessible sources.' }]
238
+ : [],
239
+ snapshot,
240
+ sources,
241
+ nextStep: clean(brief.next_step),
242
+ };
243
+ }
244
+ function renderReaderBriefMarkdown(report) {
245
+ const projection = buildReaderProjection(report);
246
+ const L = [`# ${projection.headline}`, '', projection.bottomLine, ''];
247
+ for (const warning of projection.warnings)
248
+ L.push(`> ⚠ ${warning.message}`, '');
249
+ for (const notice of projection.notices)
250
+ L.push(`> ℹ ${notice.message}`, '');
251
+ if (projection.snapshot) {
252
+ L.push('## Decision numbers', '', '### Decisive numbers', '', '| Metric | Value | What it means |', '|---|---:|---|');
253
+ for (const item of projection.snapshot.decisiveNumbers)
254
+ L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} |`);
255
+ // v6: a NOT_COMPUTABLE payback is noise on a non-financial question — omit, don't announce.
256
+ if (projection.snapshot.payback && projection.snapshot.payback.status !== 'NOT_COMPUTABLE') {
257
+ L.push('', `**Payback — ${projection.snapshot.payback.status.replaceAll('_', ' ')}:** ${projection.snapshot.payback.result}`);
258
+ L.push(`Basis: ${projection.snapshot.payback.basis}`, '');
259
+ }
260
+ else
261
+ L.push('');
262
+ L.push('### Options at a glance', '', '| Path | Commitment | Basis | Trade-off |', '|---|---:|---|---|');
263
+ for (const option of projection.snapshot.options) {
264
+ L.push(`| ${cell(option.label)} | ${cell(option.commitment)} | ${option.commitmentKind.replace('_', ' ')} | ${cell(option.tradeoff)} |`);
265
+ }
266
+ if (projection.snapshot.tripwire) {
267
+ const tripwire = projection.snapshot.tripwire;
268
+ L.push('', '### Go/no-go tripwire', '', `**${tripwire.metric}: ${tripwire.threshold}** — ${tripwire.decisionRule}`, '');
269
+ }
270
+ else
271
+ L.push('');
272
+ }
273
+ for (const section of projection.sections) {
274
+ L.push(`## ${section.heading}`, '', section.summary, '');
275
+ for (const bullet of section.bullets)
276
+ L.push(`- ${bullet}`);
277
+ if (section.bullets.length)
278
+ L.push('');
279
+ }
280
+ if (projection.featureBacklog) {
281
+ L.push('## Feature priorities', '', '| Priority | Feature | User value | Why now | Effort |', '|---|---|---|---|---|');
282
+ for (const [priority, items] of [
283
+ ['MUST', projection.featureBacklog.must],
284
+ ['SHOULD', projection.featureBacklog.should],
285
+ ['LATER', projection.featureBacklog.later],
286
+ ]) {
287
+ for (const item of items) {
288
+ L.push(`| ${priority} | ${cell(item.feature)} | ${cell(item.user_value)} | ${cell(item.rationale)} | ${item.effort} |`);
289
+ }
290
+ }
291
+ if (projection.featureBacklog.wont.length) {
292
+ L.push('', '**Not in this scope**', '');
293
+ for (const item of projection.featureBacklog.wont)
294
+ L.push(`- **${item.feature}:** ${item.reason}`);
295
+ }
296
+ L.push('');
297
+ }
298
+ if (projection.implementationPlan) {
299
+ L.push('## Build plan', '', '| # | Timebox | Outcome | Work | Done when |', '|---|---|---|---|---|');
300
+ for (const milestone of projection.implementationPlan.milestones) {
301
+ L.push(`| ${milestone.order} | ${cell(milestone.timebox)} | ${cell(milestone.outcome)} | ${cell(milestone.tasks.join('; '))} | ${cell(milestone.acceptance_test)} |`);
302
+ }
303
+ L.push('');
304
+ }
305
+ if (projection.caveats.length) {
306
+ L.push('## Top caveats', '');
307
+ for (const caveat of projection.caveats)
308
+ L.push(`- ${caveat}`);
309
+ L.push('');
310
+ }
311
+ if (projection.sources.length) {
312
+ L.push('## Sources', '');
313
+ const externalCount = report.dossier.evidence.filter((item) => evidenceOrigin(item) === 'EXTERNAL').length;
314
+ L.push(`${externalCount} independent external source${externalCount === 1 ? '' : 's'}; everything else is your own material or model background.`, '');
315
+ for (const source of projection.sources) {
316
+ const citationScope = source.citedFor.length ? ` — Cited for: ${source.citedFor.join('; ')}` : '';
317
+ L.push(`${source.url ? `- [${source.label.replace(/[\[\]]/g, '')}](${source.url})` : `- ${source.label}`}${citationScope}`);
318
+ }
319
+ L.push('');
320
+ }
321
+ L.push('## Next step', '', projection.nextStep, '');
322
+ const legacy = renderLegacyDecisionDossierMarkdown(report).split('\n').slice(2).join('\n');
323
+ L.push('## Council audit', '', '<details>', '<summary>Show the council reasoning, evidence ledger, dissent, and run receipt</summary>', '', legacy, '', '</details>', '');
324
+ return L.join('\n');
325
+ }
326
+ /** Pre-v5 deterministic report, retained as the replay fallback and the collapsed v5 audit body. */
327
+ function renderLegacyDecisionDossierMarkdown(report) {
61
328
  const { dossier } = report;
329
+ const labelFor = claimLookup(report);
62
330
  const keyFindings = report.keyFindings?.length ? report.keyFindings : [dossier.recommendation.reason];
63
331
  const criticalUnknowns = report.criticalUnknowns?.length ? report.criticalUnknowns : report.openQuestions.slice(0, 3);
64
332
  const coverage = report.confidenceBreakdown.verificationCoverage;
333
+ const allConditions = [...new Set(dossier.recommendation.conditions.map((condition) => stripReaderClaimIds(condition.text, labelFor)))];
334
+ const conditions = allConditions.slice(0, 4);
335
+ const verdictClaimId = report.claims.find((claim) => /^Verdict:/i.test(claim.text))?.id;
336
+ const seenQuestions = new Set();
337
+ const openQuestions = report.openQuestions.filter((question) => {
338
+ const key = question.slice(0, 60).toLowerCase();
339
+ if (seenQuestions.has(key))
340
+ return false;
341
+ seenQuestions.add(key);
342
+ return true;
343
+ });
65
344
  const L = [report.mode === 'quick' ? '# Single-Model Decision Report' : '# Multi-Model Decision Report', ''];
66
345
  L.push('## 1. Decision', '', ...degradation(report, ['synthesis_suspect']));
67
346
  if (report.decisionSnapshot) {
@@ -69,7 +348,7 @@ export function renderDecisionDossierMarkdown(report) {
69
348
  for (const item of report.decisionSnapshot.decisiveNumbers) {
70
349
  L.push(`| ${cell(item.label)} | ${cell(item.value)} | ${cell(item.meaning)} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
71
350
  }
72
- if (report.decisionSnapshot.payback) {
351
+ if (report.decisionSnapshot.payback && report.decisionSnapshot.payback.status !== 'NOT_COMPUTABLE') {
73
352
  const payback = report.decisionSnapshot.payback;
74
353
  L.push('', `**Payback — ${payback.status.replaceAll('_', ' ')}:** ${payback.result}`);
75
354
  L.push(`Basis: ${payback.basis}`, '');
@@ -78,7 +357,9 @@ export function renderDecisionDossierMarkdown(report) {
78
357
  L.push('');
79
358
  }
80
359
  L.push(`**Recommendation:** ${dossier.recommendation.summary}`, '');
81
- L.push(`**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of load-bearing claims independently verified.`);
360
+ L.push(report.confidenceBreakdown.verificationScope === 'FACTUAL'
361
+ ? `**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of checkable factual claims independently verified; design judgments are adjudicated by the chair, not verified.`
362
+ : `**Evidence coverage:** ${coverageLabel(coverage)} — ${pct(coverage)} of load-bearing claims independently verified.`);
82
363
  L.push(coverage < 0.5
83
364
  ? '> Low coverage means important inputs remain unchecked; it is not a probability that the recommendation is correct.'
84
365
  : '> Evidence coverage measures independent checking; it is not a probability that the recommendation is correct.', '');
@@ -107,21 +388,10 @@ export function renderDecisionDossierMarkdown(report) {
107
388
  L.push('- None recorded.');
108
389
  L.push('', `**Critical warning:** ${report.verdict.criticalWarning ?? 'None recorded.'}`);
109
390
  L.push(`**Council read:** ${councilRead(report)}`);
110
- L.push(`Evidence behind the recommendation: ${readerClaimRefs(report, dossier.recommendation.claimIds)}`, '');
391
+ L.push('');
111
392
  if (!dossier.recommendation.claimIds.length)
112
393
  L.push('> ⚠ DEGRADED: recommendation has no stored graph anchor.', '');
113
- L.push('<details>', '<summary>Decision conditions and technical confidence</summary>', '');
114
- L.push(`- Decision state: ${dossier.recommendation.status}`);
115
- L.push(`- Structural score: ${report.confidenceBreakdown.score}/100 (${report.confidenceBreakdown.label}); heuristic, not benchmark-calibrated.`);
116
- 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}`);
117
- if (dossier.recommendation.conditions.length) {
118
- L.push('', 'Conditions:');
119
- for (const condition of dossier.recommendation.conditions) {
120
- L.push(`- ${condition.text}`);
121
- }
122
- }
123
- L.push('', '</details>', '');
124
- L.push('## 2. Action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
394
+ L.push('## 2. Deliverables and action plan', '', ...degradation(report, ['plan_fallback', 'plan_skipped']));
125
395
  if ((dossier.missingRequestedOutputs ?? []).length) {
126
396
  L.push(`> ⚠ DEGRADED: requested output missing: ${dossier.missingRequestedOutputs.join(', ')}`, '');
127
397
  }
@@ -163,66 +433,58 @@ export function renderDecisionDossierMarkdown(report) {
163
433
  L.push('No executable experiment was produced.');
164
434
  L.push('');
165
435
  L.push('## 3. Why this decision', '');
166
- if (dossier.claimChain.length) {
167
- L.push('| Claim | Ruling | Evidence status | Depends on |', '|---|---|---|---|');
168
- for (const claim of dossier.claimChain) {
169
- L.push(`| ${cell(claim.text)} | ${claim.ruling} | ${claim.evidenceStatus} | ${cell(readerClaimRefs(report, claim.dependsOn))} |`);
170
- }
436
+ for (const point of keyFindings)
437
+ L.push(`- ${stripReaderClaimIds(point, labelFor)}`);
438
+ if (!keyFindings.length)
439
+ L.push('No chair reasoning was recorded.');
440
+ if (conditions.length) {
441
+ L.push('', '### Conditions', '');
442
+ for (const condition of conditions)
443
+ L.push(`- ${condition}`);
444
+ if (allConditions.length > conditions.length)
445
+ L.push('', `${allConditions.length - conditions.length} additional condition${allConditions.length - conditions.length === 1 ? '' : 's'} remain in the stored audit JSON.`);
171
446
  }
172
- else
173
- L.push('No graph-anchored decision chain was recorded.');
174
447
  L.push('');
175
448
  L.push('## 4. What could change the decision', '', '### Decision-sensitive facts', '');
176
- if (dossier.sensitivity.length) {
177
- L.push('| Fact | Sensitivity | If false | What would change it | Linked claims |', '|---|---|---|---|---|');
178
- for (const item of dossier.sensitivity) {
179
- L.push(`| ${cell(item.fact)} | ${item.sensitivity} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} | ${cell(readerClaimRefs(report, item.linkedClaimIds))} |`);
449
+ const decisiveFacts = dossier.sensitivity.filter((item) => item.sensitivity === 'DECISIVE').slice(0, 5);
450
+ if (decisiveFacts.length) {
451
+ L.push('| Fact | If false | What settles it |', '|---|---|---|');
452
+ for (const item of decisiveFacts) {
453
+ L.push(`| ${cell(claimShortLabel(item.fact))} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} |`);
180
454
  }
181
455
  }
182
456
  else
183
457
  L.push('No verdict-sensitive graph node was recorded.');
184
458
  L.push('', '### Strongest counter-case', '');
185
459
  if (dossier.counterCase.available) {
186
- L.push(dossier.counterCase.reasoning, '', `Evidence behind this counter-case: ${readerClaimRefs(report, dossier.counterCase.claimIds)}`);
460
+ const counterIds = dossier.counterCase.claimIds.filter((id) => id !== verdictClaimId);
461
+ L.push(dossier.counterCase.reasoning, '', `Evidence behind this counter-case: ${readerClaimRefs(report, counterIds)}`);
187
462
  }
188
463
  else
189
464
  L.push(`> ⚠ DEGRADED: ${dossier.counterCase.reasoning}`);
190
465
  L.push('');
191
- L.push('## 5. Evidence and verification', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']));
192
- if (dossier.evidence.length) {
193
- L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
194
- for (const evidence of dossier.evidence) {
195
- L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${evidence.verificationStatus} | ${cell(readerClaimRefs(report, evidence.claimIds))} |`);
196
- }
197
- }
198
- else
199
- L.push('No evidence cards were stored.');
200
- L.push('');
201
- L.push('## 6. Risks, gaps, and open questions', '', '### Risks', '');
466
+ L.push('## 5. Risks and open questions', '', ...degradation(report, ['verification_skipped', 'research_ungrounded']), '### Risks', '');
202
467
  if (report.risks.length) {
468
+ const shownRisks = report.risks.slice(0, 8);
203
469
  L.push('| Risk | Severity |', '|---|---|');
204
- for (const risk of report.risks)
470
+ for (const risk of shownRisks)
205
471
  L.push(`| ${cell(risk.risk)} | ${risk.severity} |`);
472
+ if (report.risks.length > shownRisks.length)
473
+ L.push('', `${report.risks.length - shownRisks.length} lower-severity items — more in the technical audit (full list in the stored JSON).`);
206
474
  }
207
475
  else
208
476
  L.push('No material risk was recorded.');
209
- L.push('', '### Coverage', '');
210
- if (dossier.coverage.length) {
211
- L.push('| Dimension | Status | Related claims |', '|---|---|---|');
212
- for (const item of dossier.coverage)
213
- L.push(`| ${cell(item.label)} | ${item.status} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
214
- }
215
- else
216
- L.push('No rubric coverage ledger was recorded.');
217
477
  L.push('', '### Open questions', '');
218
- if (report.openQuestions.length) {
219
- for (const question of report.openQuestions)
478
+ if (openQuestions.length) {
479
+ for (const question of openQuestions.slice(0, 5))
220
480
  L.push(`- ${question}`);
481
+ if (openQuestions.length > 5)
482
+ L.push('', `Showing 5 of ${openQuestions.length} — the rest are in the technical audit.`);
221
483
  }
222
484
  else
223
485
  L.push('No verdict-flipping open question was recorded.');
224
486
  L.push('');
225
- L.push('## 7. Disagreement and dissent', '', ...degradation(report, ['single_model', 'low_diversity']));
487
+ L.push('## 6. Disagreement and dissent', '', ...degradation(report, ['single_model', 'low_diversity']));
226
488
  if (report.disagreements.length) {
227
489
  for (const disagreement of report.disagreements) {
228
490
  L.push(`- **${disagreement.topic}** — ${disagreement.status}; ${disagreement.ruling}.`);
@@ -253,30 +515,61 @@ export function renderDecisionDossierMarkdown(report) {
253
515
  for (const item of report.minority.uniqueOppositions)
254
516
  L.push(`- ${providerName(item.provider)} uniquely opposed: ${item.proposition}`);
255
517
  L.push(`- Decision-blocking status: ${report.minority.blocksDecision}`, '');
256
- L.push('## 8. What the council added', '');
257
- if (dossier.sharedConcerns.length) {
258
- L.push('Shared concerns:');
259
- for (const item of dossier.sharedConcerns)
260
- L.push(`- ${item.text} ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
261
- }
262
- else
263
- L.push('Shared concerns: none recorded.');
264
- L.push('');
265
- if (dossier.uniqueSupportedInsights.length) {
266
- L.push('Unique supported insights:');
267
- for (const item of dossier.uniqueSupportedInsights)
268
- L.push(`- ${item.text} ${providerName(item.providerId)}; ${item.verificationStatus}.`);
518
+ L.push('## 7. What the council added', '', ...degradation(report, ['weak_seat']));
519
+ if (dossier.seatStats) {
520
+ const weakSeat = dossier.seatStats.some((seat) => seat.positions < 3 || seat.evidenced / seat.positions < 0.5);
521
+ for (const seat of dossier.seatStats) {
522
+ const weak = seat.positions < 3 || seat.evidenced / seat.positions < 0.5;
523
+ L.push(`- ${providerName(seat.provider)}: ${seat.positions} position${seat.positions === 1 ? '' : 's'}, ${seat.evidenced} with evidence${weak ? ' — weak seat this run.' : '.'}`);
524
+ }
525
+ for (const chair of report.models.filter((model) => model.roles.includes('judge') && !dossier.seatStats.some((seat) => seat.provider === model.provider))) {
526
+ L.push(`- ${chair.name}: chaired the decision and authored no scout claims (clean adjudication).`);
527
+ }
528
+ if (dossier.sharedConcerns.length) {
529
+ L.push('', 'Shared concerns:');
530
+ for (const item of dossier.sharedConcerns)
531
+ L.push(`- ${item.text} — ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
532
+ }
533
+ L.push('', report.disagreements.length
534
+ ? councilRead(report)
535
+ : 'No genuine disagreement survived to the chair.');
536
+ if (weakSeat || coverage < 0.5)
537
+ L.push('Convergence with weak verification lowers confidence; it does not raise it.');
538
+ const verifiedContributions = dossier.contributions.filter((item) => item.verifiedUniqueClaimIds.length > 0);
539
+ if (verifiedContributions.length) {
540
+ L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
541
+ L.push('Only unique claims that survived independent verification receive credit.', '');
542
+ L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
543
+ for (const contribution of verifiedContributions) {
544
+ L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
545
+ }
546
+ }
269
547
  }
270
- else
271
- L.push('Unique supported insights: none recorded.');
272
- L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
273
- L.push('Only unique claims that survived independent verification receive credit.', '');
274
- L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
275
- for (const contribution of dossier.contributions) {
276
- L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
548
+ else {
549
+ if (dossier.sharedConcerns.length) {
550
+ L.push('Shared concerns:');
551
+ for (const item of dossier.sharedConcerns)
552
+ L.push(`- ${item.text} ${item.evidenceStatus}; ${item.providerIds.map(providerName).join(', ')}.`);
553
+ }
554
+ else
555
+ L.push('Shared concerns: none recorded.');
556
+ L.push('');
557
+ if (dossier.uniqueSupportedInsights.length) {
558
+ L.push('Unique supported insights:');
559
+ for (const item of dossier.uniqueSupportedInsights)
560
+ L.push(`- ${item.text} — ${providerName(item.providerId)}; ${item.verificationStatus}.`);
561
+ }
562
+ else
563
+ L.push('Unique supported insights: none recorded.');
564
+ L.push('', '### Verified unique contributions', '', ...degradation(report, ['verification_skipped', 'single_model', 'low_diversity']));
565
+ L.push('Only unique claims that survived independent verification receive credit.', '');
566
+ L.push('| Provider | Verified unique contributions | Count |', '|---|---|---|');
567
+ for (const contribution of dossier.contributions) {
568
+ L.push(`| ${contribution.name} | ${cell(readerClaimRefs(report, contribution.verifiedUniqueClaimIds))} | ${contribution.verifiedUniqueClaimIds.length} |`);
569
+ }
277
570
  }
278
571
  L.push('');
279
- L.push('## 9. Run details', '');
572
+ L.push('## 8. Run details', '');
280
573
  L.push(`- Report ID: \`${report.reportId}\``);
281
574
  L.push(`- Generated: ${report.generatedAt}`);
282
575
  L.push(`- Original task: ${report.task.original}`);
@@ -288,14 +581,77 @@ export function renderDecisionDossierMarkdown(report) {
288
581
  L.push(`- Models and roles: ${report.models.map((model) => `${model.name} (${model.roles.join(', ')})`).join(' · ') || 'none recorded'}`);
289
582
  L.push(`- Mode: ${report.mode}`);
290
583
  L.push(`- Provider calls: ${report.receipt.calls}/${report.receipt.budget}`);
584
+ if (report.autoEscalationReasons?.length)
585
+ L.push(`- Adaptive escalation: ${report.autoEscalationReasons.join('; ')}`);
291
586
  L.push(`- Categories: discovery ${report.receipt.categories.discovery} · verification ${report.receipt.categories.verification} · repair ${report.receipt.categories.repair} · planning ${report.receipt.categories.planning}`);
292
587
  L.push(`- By provider: ${Object.entries(report.receipt.byProvider).map(([provider, count]) => `${providerName(provider)} ${count}`).join(', ') || 'none'}`);
293
588
  L.push(`- Recorded model time: ${(report.receipt.modelTimeMs / 1000).toFixed(1)}s`);
589
+ if (report.receipt.tokens)
590
+ L.push(`- Tokens: ${formatTokenLine(report.receipt.tokens)}`);
294
591
  L.push(`- Degradation flags: ${report.flags.join(', ') || 'none'}`, '');
592
+ // This pass runs over already-built (cell-escaped) table rows, so injected labels must be cell-escaped too.
295
593
  for (let index = 0; index < L.length; index++)
296
- L[index] = stripReaderClaimIds(L[index]);
297
- L.push('## 10. Technical audit', '');
298
- L.push('<details>', '<summary>Original submissions and graph events</summary>', '');
594
+ L[index] = stripReaderClaimIds(L[index], labelFor, cell);
595
+ L.push('## 9. Technical audit', '');
596
+ L.push('<details>', '<summary>Evidence, coverage, claims, and graph events</summary>', '');
597
+ L.push('### Decision confidence', '');
598
+ L.push(`- Decision state: ${dossier.recommendation.status}`);
599
+ L.push(`- Structural score: ${report.confidenceBreakdown.score}/100 (${report.confidenceBreakdown.label}); heuristic, not benchmark-calibrated.`);
600
+ 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}`, '');
601
+ L.push('### Full claim chain', '');
602
+ if (dossier.claimChain.length) {
603
+ L.push('| Claim | Ruling | Evidence status | Depends on |', '|---|---|---|---|');
604
+ for (const claim of dossier.claimChain) {
605
+ L.push(`| ${cell(claim.text)} | ${claim.ruling} | ${claim.evidenceStatus} | ${cell(readerClaimRefs(report, claim.dependsOn))} |`);
606
+ }
607
+ }
608
+ else
609
+ L.push('No graph-anchored decision chain was recorded.');
610
+ L.push('', '### Full decision-sensitivity ledger', '');
611
+ if (dossier.sensitivity.length) {
612
+ L.push('| Fact | Sensitivity | If false | What would change it | Linked claims |', '|---|---|---|---|---|');
613
+ for (const item of dossier.sensitivity) {
614
+ L.push(`| ${cell(item.fact)} | ${item.sensitivity} | ${item.impactIfFalse} | ${cell(item.whatWouldChangeIt)} | ${cell(readerClaimRefs(report, item.linkedClaimIds))} |`);
615
+ }
616
+ }
617
+ else
618
+ L.push('No verdict-sensitive graph node was recorded.');
619
+ L.push('', '### Evidence and verification', '');
620
+ if (dossier.evidence.length) {
621
+ L.push('| Evidence ID | Source | Date | Freshness | Verification | Linked claims |', '|---|---|---|---|---|---|');
622
+ for (const evidence of dossier.evidence) {
623
+ // v6: the user's own material can corroborate what they told us — never "verify" it.
624
+ const verification = evidenceOrigin(evidence) === 'USER_MATERIAL'
625
+ ? 'consistent with your materials (not independently checked)'
626
+ : evidence.verificationStatus;
627
+ L.push(`| ${evidence.id} | ${cell(evidence.source)} (${evidence.sourceKind}) | ${evidence.date} | ${evidence.freshness} | ${verification} | ${cell(readerClaimRefs(report, evidence.claimIds))} |`);
628
+ }
629
+ }
630
+ else
631
+ L.push('No evidence cards were stored.');
632
+ L.push('', '### Coverage ledger', '');
633
+ if (dossier.coverage.length) {
634
+ L.push('| Dimension | Status | Related claims |', '|---|---|---|');
635
+ for (const item of dossier.coverage)
636
+ L.push(`| ${cell(item.label)} | ${item.status} | ${cell(readerClaimRefs(report, item.claimIds))} |`);
637
+ }
638
+ else
639
+ L.push('No rubric coverage ledger was recorded.');
640
+ L.push('', '### Full risk ledger', '');
641
+ if (report.risks.length) {
642
+ L.push('| Risk | Severity |', '|---|---|');
643
+ for (const risk of report.risks)
644
+ L.push(`| ${cell(risk.risk)} | ${risk.severity} |`);
645
+ }
646
+ else
647
+ L.push('No material risk was recorded.');
648
+ L.push('', '### Full open-question ledger', '');
649
+ if (report.openQuestions.length)
650
+ for (const question of report.openQuestions)
651
+ L.push(`- ${question}`);
652
+ else
653
+ L.push('No verdict-flipping open question was recorded.');
654
+ L.push('', '### Original submissions and graph events', '');
299
655
  for (const submission of dossier.technical.submissions) {
300
656
  L.push(`- **${submission.name}:** ${submission.strongestVersion} (${refs(submission.positionIds)})`);
301
657
  }
@@ -318,3 +674,8 @@ export function renderDecisionDossierMarkdown(report) {
318
674
  L.push('', '</details>', '');
319
675
  return L.join('\n');
320
676
  }
677
+ /** Canonical Markdown. HTML and Copy-Markdown consume the same persisted reader brief.
678
+ * v6: every rendered artifact is path-sanitized; stored run JSON keeps raw locators for audit. */
679
+ export function renderDecisionDossierMarkdown(report) {
680
+ return sanitizeLocalPaths(report.dossier.readerBrief ? renderReaderBriefMarkdown(report) : renderLegacyDecisionDossierMarkdown(report));
681
+ }