aiki-cli 0.2.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.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/bench/arms.js +104 -0
- package/dist/bench/harness.js +251 -0
- package/dist/bench/results.js +70 -0
- package/dist/bench/scoring/seeded-bugs.js +31 -0
- package/dist/cli/bench.js +50 -0
- package/dist/cli/config.js +51 -0
- package/dist/cli/doctor.js +115 -0
- package/dist/cli/index.js +129 -0
- package/dist/cli/models.js +51 -0
- package/dist/cli/providers.js +31 -0
- package/dist/cli/resolve.js +159 -0
- package/dist/cli/resume.js +94 -0
- package/dist/cli/run.js +155 -0
- package/dist/cli/sessions.js +35 -0
- package/dist/cli/show.js +73 -0
- package/dist/config/config.js +102 -0
- package/dist/config/smoke-cache.js +65 -0
- package/dist/council/open.js +26 -0
- package/dist/council/view.js +873 -0
- package/dist/orchestration/cluster.js +83 -0
- package/dist/orchestration/context.js +277 -0
- package/dist/orchestration/engine.js +92 -0
- package/dist/orchestration/git.js +133 -0
- package/dist/orchestration/jsonStage.js +32 -0
- package/dist/orchestration/skills.js +39 -0
- package/dist/orchestration/stages/cr-ladder.js +63 -0
- package/dist/orchestration/stages/cr-map.js +62 -0
- package/dist/orchestration/stages/cr-report.js +83 -0
- package/dist/orchestration/stages/cr-s4-review.js +69 -0
- package/dist/orchestration/stages/cr-s8-crossexam.js +104 -0
- package/dist/orchestration/stages/cr-s9-judge.js +89 -0
- package/dist/orchestration/stages/s0-grill.js +79 -0
- package/dist/orchestration/stages/s1-intent.js +25 -0
- package/dist/orchestration/stages/s10-render.js +198 -0
- package/dist/orchestration/stages/s2-misread.js +76 -0
- package/dist/orchestration/stages/s3-prompts.js +55 -0
- package/dist/orchestration/stages/s4-analyze.js +50 -0
- package/dist/orchestration/stages/s5-drift.js +40 -0
- package/dist/orchestration/stages/s6-claims.js +56 -0
- package/dist/orchestration/stages/s7-disagreement.js +134 -0
- package/dist/orchestration/stages/s8-verify.js +56 -0
- package/dist/orchestration/stages/s9-judge.js +152 -0
- package/dist/orchestration/stages/s9b-plan.js +192 -0
- package/dist/providers/adapter-core.js +131 -0
- package/dist/providers/adapters.js +9 -0
- package/dist/providers/agy.js +29 -0
- package/dist/providers/claude.js +56 -0
- package/dist/providers/codex.js +35 -0
- package/dist/providers/detect.js +21 -0
- package/dist/providers/probe.js +43 -0
- package/dist/providers/profiles.js +38 -0
- package/dist/providers/profiles.json +5 -0
- package/dist/providers/smoke.js +26 -0
- package/dist/providers/spawn.js +152 -0
- package/dist/providers/types.js +17 -0
- package/dist/schemas/index.js +374 -0
- package/dist/skills/.gitkeep +0 -0
- package/dist/skills/code-review/judge.md +23 -0
- package/dist/skills/code-review/reviewer.md +38 -0
- package/dist/skills/idea-refinement/analyst.md +45 -0
- package/dist/skills/idea-refinement/planner.md +25 -0
- package/dist/storage/feedback.js +111 -0
- package/dist/storage/paths.js +20 -0
- package/dist/storage/replay.js +0 -0
- package/dist/storage/runs-read.js +95 -0
- package/dist/storage/runs.js +129 -0
- package/dist/storage/sessions.js +71 -0
- package/dist/tui/app.js +444 -0
- package/dist/tui/format.js +27 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/smart-entry.js +106 -0
- package/dist/tui/timeline.js +91 -0
- package/dist/workflows/code-review.js +76 -0
- package/dist/workflows/idea-refinement.js +105 -0
- package/package.json +64 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { DISPLAY_NAME } from '../providers/types.js';
|
|
4
|
+
import { deriveAudit, deriveScorecard, mergeOpenQuestions } from '../orchestration/stages/s10-render.js';
|
|
5
|
+
import { IDEA_RUBRIC } from '../workflows/idea-refinement.js';
|
|
6
|
+
import { listArtifacts, readJsonArtifact } from '../storage/runs-read.js';
|
|
7
|
+
function providerName(id) {
|
|
8
|
+
return id in DISPLAY_NAME ? DISPLAY_NAME[id] : id;
|
|
9
|
+
}
|
|
10
|
+
function findingLine(f) {
|
|
11
|
+
return `${f.severity}/${f.category} ${f.file}:${f.line_start}-${f.line_end} — ${f.claim}`;
|
|
12
|
+
}
|
|
13
|
+
function roleColumn(provider, role) {
|
|
14
|
+
if (role.workflow === 'code-review') {
|
|
15
|
+
return {
|
|
16
|
+
provider,
|
|
17
|
+
title: providerName(provider),
|
|
18
|
+
lines: role.findings.map(findingLine),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
provider,
|
|
23
|
+
title: providerName(provider),
|
|
24
|
+
lines: [
|
|
25
|
+
`Strongest version: ${role.strongest_version}`,
|
|
26
|
+
...role.assumptions.map((a) => `${a.load_bearing ? 'Load-bearing' : 'Assumption'}: ${a.statement}`),
|
|
27
|
+
...role.attacks.map((a) => `Attack: ${a.argument}`),
|
|
28
|
+
...role.open_questions.map((q) => `Question: ${q}`),
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function judgeRulings(judge) {
|
|
33
|
+
return new Map((judge?.adjudications ?? []).map((a) => [a.id, `${a.ruling}: ${a.reasoning}`]));
|
|
34
|
+
}
|
|
35
|
+
function codeReviewRows(map, judge) {
|
|
36
|
+
const rulings = judgeRulings(judge);
|
|
37
|
+
const row = (kind, a) => ({
|
|
38
|
+
kind,
|
|
39
|
+
title: findingLine(a.finding),
|
|
40
|
+
detail: a.refutation ?? a.finding.evidence,
|
|
41
|
+
providers: a.reviewers,
|
|
42
|
+
ruling: rulings.get(a.finding.id),
|
|
43
|
+
});
|
|
44
|
+
return [
|
|
45
|
+
...map.consensus.map((a) => row('consensus', a)),
|
|
46
|
+
...map.disputed.map((a) => row('dispute', a)),
|
|
47
|
+
...map.single_reviewer.map((a) => row('single', a)),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function ideaRows(map, judge) {
|
|
51
|
+
const rulings = judgeRulings(judge);
|
|
52
|
+
return [
|
|
53
|
+
...map.consensus.map((c) => ({ kind: 'consensus', title: c.statement, detail: c.evidence ?? '', providers: c.providers })),
|
|
54
|
+
...map.contradictions.map((d) => ({
|
|
55
|
+
kind: 'dispute',
|
|
56
|
+
title: d.id,
|
|
57
|
+
detail: d.attacks.map((a) => `${providerName(a.provider)}: ${a.argument}`).join(' · '),
|
|
58
|
+
providers: d.attacks.map((a) => a.provider),
|
|
59
|
+
ruling: rulings.get(d.id),
|
|
60
|
+
})),
|
|
61
|
+
...map.unique.map((c) => ({ kind: 'unique', title: c.statement, detail: c.evidence ?? '', providers: c.providers })),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
const SEV_ORDER = { HIGH: 0, MED: 1, MEDIUM: 1, LOW: 2 };
|
|
65
|
+
function sevRank(s) {
|
|
66
|
+
return SEV_ORDER[s.toUpperCase()] ?? 1;
|
|
67
|
+
}
|
|
68
|
+
function maxSeverity(sevs) {
|
|
69
|
+
return [...sevs].sort((a, b) => sevRank(a) - sevRank(b))[0] ?? 'MED';
|
|
70
|
+
}
|
|
71
|
+
function clip(s, n) {
|
|
72
|
+
const t = s.trim();
|
|
73
|
+
return t.length > n ? `${t.slice(0, n - 1).trimEnd()}…` : t;
|
|
74
|
+
}
|
|
75
|
+
/** The intent-contract task is a third-person restatement ("The user is asking about …"). Strip that
|
|
76
|
+
* meta-preamble and keep the first sentence so the masthead reads as the plain question. */
|
|
77
|
+
function cleanTopic(t) {
|
|
78
|
+
let s = t.trim().replace(/^the user('?s)?\s+(is\s+asking\s+(about|for)|wants\s+to\s+know(\s+(if|whether|how))?|wants|is\s+asking|question\s+is|request\s+is|idea\s+is)\s*:?\s*/i, '');
|
|
79
|
+
s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
80
|
+
const firstSentence = s.match(/^[^.?!]{25,}[.?!]/);
|
|
81
|
+
return clip(firstSentence ? firstSentence[0] : s, 200);
|
|
82
|
+
}
|
|
83
|
+
function computeSignal(riskCount, agreeCount) {
|
|
84
|
+
if (riskCount === 0)
|
|
85
|
+
return { tone: 'good', label: agreeCount ? 'Holds up well' : 'No major objections' };
|
|
86
|
+
const tone = riskCount >= 3 ? 'risk' : 'caution';
|
|
87
|
+
return { tone, label: agreeCount ? 'Feasible — with real caveats' : 'Proceed with caution' };
|
|
88
|
+
}
|
|
89
|
+
function recommendationTone(r, fallback) {
|
|
90
|
+
if (r === 'PROCEED')
|
|
91
|
+
return 'good';
|
|
92
|
+
if (r === 'STOP')
|
|
93
|
+
return 'risk';
|
|
94
|
+
if (r === 'PIVOT' || r === 'PROCEED_WITH_CONDITIONS')
|
|
95
|
+
return 'caution';
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
function recommendationLabel(r, fallback) {
|
|
99
|
+
if (!r)
|
|
100
|
+
return fallback;
|
|
101
|
+
return r === 'PROCEED_WITH_CONDITIONS' ? 'Proceed with conditions' : r.charAt(0) + r.slice(1).toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
function chairPhrase(ruling) {
|
|
104
|
+
if (ruling === 'UPHOLD')
|
|
105
|
+
return 'the objection stands';
|
|
106
|
+
if (ruling === 'REJECT')
|
|
107
|
+
return 'the idea holds here';
|
|
108
|
+
return 'left to you';
|
|
109
|
+
}
|
|
110
|
+
function ideaDebates(map, judge) {
|
|
111
|
+
const claims = new Map([...map.consensus, ...map.unique].map((c) => [c.id, c]));
|
|
112
|
+
const adj = new Map((judge?.adjudications ?? []).map((a) => [a.id, a]));
|
|
113
|
+
return map.contradictions.map((d) => {
|
|
114
|
+
const a = adj.get(d.id);
|
|
115
|
+
const claimantProviders = [...new Set(d.claim_ids.flatMap((id) => claims.get(id)?.providers ?? []))];
|
|
116
|
+
const attackerProviders = [...new Set(d.attacks.map((x) => x.provider))];
|
|
117
|
+
return {
|
|
118
|
+
claim: d.claim_ids.map((id) => claims.get(id)?.statement ?? id).join(' / '),
|
|
119
|
+
claimantProviders,
|
|
120
|
+
attackerProviders,
|
|
121
|
+
challenge: d.attacks.map((x) => `${providerName(x.provider)}: ${x.argument}`).join('\n\n'),
|
|
122
|
+
chair: chairPhrase(a?.ruling),
|
|
123
|
+
reasoning: a?.reasoning ?? '',
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function ideaSeatOutputs(roles) {
|
|
128
|
+
return roles
|
|
129
|
+
.filter((r) => r.role.workflow === 'idea-refinement' && r.provider in DISPLAY_NAME)
|
|
130
|
+
.map((r) => ({ provider: r.provider, output: r.role }));
|
|
131
|
+
}
|
|
132
|
+
function receiptLines(meta) {
|
|
133
|
+
const calls = meta.calls ?? [];
|
|
134
|
+
const byProvider = new Map();
|
|
135
|
+
let ms = 0;
|
|
136
|
+
for (const c of calls) {
|
|
137
|
+
byProvider.set(c.provider, (byProvider.get(c.provider) ?? 0) + 1);
|
|
138
|
+
ms += c.durationMs;
|
|
139
|
+
}
|
|
140
|
+
const providerCounts = [...byProvider.entries()].map(([p, n]) => `${providerName(p)} ${n}`).join(', ') || 'none';
|
|
141
|
+
const profiles = Object.values(meta.flag_profiles ?? {})
|
|
142
|
+
.map((p) => `${providerName(p.id)}: ${'model' in p && p.model ? p.model : 'default model'}`)
|
|
143
|
+
.join(' · ');
|
|
144
|
+
return [
|
|
145
|
+
`Calls: ${meta.call_count ?? calls.length}/${meta.budget?.limit ?? '?'}`,
|
|
146
|
+
`Per-provider: ${providerCounts}`,
|
|
147
|
+
`Recorded model time: ${(ms / 1000).toFixed(1)}s`,
|
|
148
|
+
profiles ? `Models: ${profiles}` : '',
|
|
149
|
+
meta.flags?.length ? `Flags: ${meta.flags.join(', ')}` : '',
|
|
150
|
+
].filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
/** Turn the idea disagreement map + judge report into the human decision story. Deterministic. */
|
|
153
|
+
function ideaNarrative(map, judge) {
|
|
154
|
+
const claims = new Map();
|
|
155
|
+
for (const c of [...map.consensus, ...map.unique])
|
|
156
|
+
claims.set(c.id, c.statement);
|
|
157
|
+
const adj = new Map((judge?.adjudications ?? []).map((a) => [a.id, a]));
|
|
158
|
+
const disputes = map.contradictions.map((d) => {
|
|
159
|
+
const a = adj.get(d.id);
|
|
160
|
+
return {
|
|
161
|
+
ruling: a?.ruling ?? '',
|
|
162
|
+
assumption: d.claim_ids.map((id) => claims.get(id) ?? id).join(' '),
|
|
163
|
+
challenge: d.attacks.map((x) => `${providerName(x.provider)}: ${x.argument}`).join('\n\n'),
|
|
164
|
+
severity: maxSeverity(d.attacks.map((x) => x.severity)),
|
|
165
|
+
reasoning: a?.reasoning ?? '',
|
|
166
|
+
providers: d.attacks.map((x) => x.provider),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const risks = disputes
|
|
170
|
+
.filter((d) => d.ruling === 'UPHOLD')
|
|
171
|
+
.sort((a, b) => sevRank(a.severity) - sevRank(b.severity))
|
|
172
|
+
.map(({ assumption, severity, challenge, reasoning, providers }) => ({ assumption, severity, challenge, reasoning, providers }));
|
|
173
|
+
const defended = disputes
|
|
174
|
+
.filter((d) => d.ruling === 'REJECT')
|
|
175
|
+
.map(({ assumption, challenge, reasoning }) => ({ assumption, challenge, reasoning }));
|
|
176
|
+
const agreements = map.consensus.map((c) => ({ statement: c.statement, providers: c.providers }));
|
|
177
|
+
const blindSpots = map.blind_spots ?? [];
|
|
178
|
+
const nextSteps = [
|
|
179
|
+
...risks.map((r) => `Pressure-test the assumption “${clip(r.assumption, 130)}” — the council found it doesn't hold as stated.`),
|
|
180
|
+
...blindSpots.map((b) => `Work out: ${b}.`),
|
|
181
|
+
];
|
|
182
|
+
return {
|
|
183
|
+
signal: computeSignal(risks.length, agreements.length),
|
|
184
|
+
agreements,
|
|
185
|
+
risks,
|
|
186
|
+
defended,
|
|
187
|
+
blindSpots,
|
|
188
|
+
nextSteps,
|
|
189
|
+
biggestRisk: risks[0]?.assumption,
|
|
190
|
+
bestNextStep: nextSteps[0],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async function loadRoleOutputs(dir) {
|
|
194
|
+
const artifacts = await listArtifacts(dir);
|
|
195
|
+
const roleFiles = artifacts.filter((f) => f.startsWith('04-role-outputs/') && f.endsWith('.json'));
|
|
196
|
+
const roles = await Promise.all(roleFiles.map(async (file) => ({
|
|
197
|
+
provider: basename(file, '.json'),
|
|
198
|
+
role: JSON.parse(await readFile(join(dir, file), 'utf8')),
|
|
199
|
+
})));
|
|
200
|
+
return roles.sort((a, b) => providerName(a.provider).localeCompare(providerName(b.provider)));
|
|
201
|
+
}
|
|
202
|
+
export async function loadCouncilView(runId, dir) {
|
|
203
|
+
const meta = await readJsonArtifact(dir, 'meta.json');
|
|
204
|
+
if (!meta)
|
|
205
|
+
return null;
|
|
206
|
+
const [judge, actionPlan, roles, intent] = await Promise.all([
|
|
207
|
+
readJsonArtifact(dir, '09-judge-report.json'),
|
|
208
|
+
readJsonArtifact(dir, '09b-action-plan.json'),
|
|
209
|
+
loadRoleOutputs(dir),
|
|
210
|
+
readJsonArtifact(dir, '01-intent-contract.json'),
|
|
211
|
+
]);
|
|
212
|
+
const columns = roles.map(({ provider, role }) => roleColumn(provider, role));
|
|
213
|
+
const verdict = judge?.verdict ?? (meta.exit_status === 'ok' ? 'Run completed without a judge verdict artifact.' : `Run ${meta.exit_status}.`);
|
|
214
|
+
let rows = [];
|
|
215
|
+
let stats = [];
|
|
216
|
+
let narrative = {};
|
|
217
|
+
if (meta.workflow === 'code-review') {
|
|
218
|
+
const map = await readJsonArtifact(dir, '07-review-map.json');
|
|
219
|
+
if (map) {
|
|
220
|
+
rows = codeReviewRows(map, judge);
|
|
221
|
+
stats = [
|
|
222
|
+
`${map.consensus.length} consensus`,
|
|
223
|
+
`${map.disputed.length} disputed`,
|
|
224
|
+
`${map.single_reviewer.length} single-reviewer`,
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const map = await readJsonArtifact(dir, '07-disagreement-map.json');
|
|
230
|
+
if (map) {
|
|
231
|
+
rows = ideaRows(map, judge);
|
|
232
|
+
stats = [
|
|
233
|
+
`${map.consensus.length} consensus`,
|
|
234
|
+
`${map.contradictions.length} disputes`,
|
|
235
|
+
`${map.unique.length} unique`,
|
|
236
|
+
`${map.blind_spots.length} blind spots`,
|
|
237
|
+
];
|
|
238
|
+
narrative = ideaNarrative(map, judge);
|
|
239
|
+
const reportV3 = Boolean(judge?.recommendation || actionPlan);
|
|
240
|
+
if (actionPlan)
|
|
241
|
+
narrative.nextSteps = actionPlan.actions.map((a) => a.action);
|
|
242
|
+
if (reportV3) {
|
|
243
|
+
const ideaSeats = ideaSeatOutputs(roles);
|
|
244
|
+
narrative = {
|
|
245
|
+
...narrative,
|
|
246
|
+
recommendation: judge?.recommendation,
|
|
247
|
+
conditions: judge?.conditions,
|
|
248
|
+
scorecard: deriveScorecard(IDEA_RUBRIC, map),
|
|
249
|
+
audit: judge ? deriveAudit(map, judge) : [],
|
|
250
|
+
debates: ideaDebates(map, judge),
|
|
251
|
+
actionPlan: actionPlan ?? undefined,
|
|
252
|
+
openQuestions: mergeOpenQuestions(ideaSeats),
|
|
253
|
+
receipt: receiptLines(meta),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const moderator = meta.roles?.judge ? providerName(meta.roles.judge) : undefined;
|
|
259
|
+
return {
|
|
260
|
+
runId,
|
|
261
|
+
workflow: meta.workflow,
|
|
262
|
+
verdict,
|
|
263
|
+
keyPoints: judge?.key_points ?? [],
|
|
264
|
+
confidence: judge?.confidence_notes ?? '',
|
|
265
|
+
dissent: judge?.dissent ?? [],
|
|
266
|
+
columns,
|
|
267
|
+
rows,
|
|
268
|
+
stats,
|
|
269
|
+
calls: `${meta.call_count}/${meta.budget.limit} provider calls`,
|
|
270
|
+
flags: meta.flags ?? [],
|
|
271
|
+
topic: intent?.task,
|
|
272
|
+
moderator,
|
|
273
|
+
...narrative,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// ── HTML rendering ─────────────────────────────────────────────────────────────
|
|
277
|
+
function escapeHtml(s) {
|
|
278
|
+
return s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
279
|
+
}
|
|
280
|
+
/** Escape then turn blank-line gaps into paragraph breaks. */
|
|
281
|
+
function paras(s) {
|
|
282
|
+
return s
|
|
283
|
+
.split(/\n\s*\n/)
|
|
284
|
+
.map((p) => `<p>${escapeHtml(p.trim()).replaceAll('\n', '<br>')}</p>`)
|
|
285
|
+
.join('');
|
|
286
|
+
}
|
|
287
|
+
function sevClass(s) {
|
|
288
|
+
const r = sevRank(s);
|
|
289
|
+
return r === 0 ? 'sev-high' : r === 2 ? 'sev-low' : 'sev-med';
|
|
290
|
+
}
|
|
291
|
+
function sevLabel(s) {
|
|
292
|
+
const r = sevRank(s);
|
|
293
|
+
return r === 0 ? 'High severity' : r === 2 ? 'Low severity' : 'Medium severity';
|
|
294
|
+
}
|
|
295
|
+
function initials(name) {
|
|
296
|
+
return name.slice(0, 2).toUpperCase();
|
|
297
|
+
}
|
|
298
|
+
function providerDots(ids) {
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
const names = ids.map(providerName).filter((n) => (seen.has(n) ? false : (seen.add(n), true)));
|
|
301
|
+
if (!names.length)
|
|
302
|
+
return '';
|
|
303
|
+
return `<span class="who">${names.map((n) => `<span class="dot" title="${escapeHtml(n)}">${escapeHtml(initials(n))}</span>`).join('')}<span class="who-names">${escapeHtml(names.join(' · '))}</span></span>`;
|
|
304
|
+
}
|
|
305
|
+
function section(index, title, inner, delay, note = '') {
|
|
306
|
+
return `
|
|
307
|
+
<section class="block reveal" style="animation-delay:${delay}ms">
|
|
308
|
+
<div class="block-head"><span class="idx">${index}</span><h2>${escapeHtml(title)}</h2></div>
|
|
309
|
+
${note ? `<p class="lede">${escapeHtml(note)}</p>` : ''}
|
|
310
|
+
${inner}
|
|
311
|
+
</section>`;
|
|
312
|
+
}
|
|
313
|
+
function renderLegacyIdeaBody(view) {
|
|
314
|
+
const risks = view.risks ?? [];
|
|
315
|
+
const agreements = view.agreements ?? [];
|
|
316
|
+
const blindSpots = view.blindSpots ?? [];
|
|
317
|
+
const defended = view.defended ?? [];
|
|
318
|
+
const nextSteps = view.nextSteps ?? [];
|
|
319
|
+
const signal = view.signal ?? { label: 'Reviewed', tone: 'caution' };
|
|
320
|
+
const glance = `
|
|
321
|
+
<div class="glance">
|
|
322
|
+
<div class="stat good"><span class="n">${agreements.length}</span><span class="k">agreed on</span></div>
|
|
323
|
+
<div class="stat risk"><span class="n">${risks.length}</span><span class="k">risks that stand</span></div>
|
|
324
|
+
<div class="stat caution"><span class="n">${blindSpots.length}</span><span class="k">not examined</span></div>
|
|
325
|
+
</div>`;
|
|
326
|
+
const hero = `
|
|
327
|
+
<section class="verdict tone-${signal.tone} reveal" style="animation-delay:60ms">
|
|
328
|
+
<span class="pill">${escapeHtml(signal.label)}</span>
|
|
329
|
+
<p class="verdict-text">${escapeHtml(view.verdict)}</p>
|
|
330
|
+
${glance}
|
|
331
|
+
</section>`;
|
|
332
|
+
const bottomLine = (view.biggestRisk || view.bestNextStep)
|
|
333
|
+
? `<section class="bottomline reveal" style="animation-delay:120ms">
|
|
334
|
+
${view.biggestRisk ? `<div><span class="tag">Biggest risk</span><p>${escapeHtml(clip(view.biggestRisk, 200))}</p></div>` : ''}
|
|
335
|
+
${view.bestNextStep ? `<div><span class="tag">Start here</span><p>${escapeHtml(clip(view.bestNextStep, 200))}</p></div>` : ''}
|
|
336
|
+
</section>`
|
|
337
|
+
: '';
|
|
338
|
+
const riskCards = risks.length
|
|
339
|
+
? risks.map((r) => `
|
|
340
|
+
<article class="card risk-card">
|
|
341
|
+
<div class="card-top"><span class="chip ${sevClass(r.severity)}">${escapeHtml(sevLabel(r.severity))}</span>${providerDots(r.providers)}</div>
|
|
342
|
+
<h3>${escapeHtml(r.assumption)}</h3>
|
|
343
|
+
<div class="field"><span class="fk">The challenge</span>${paras(r.challenge)}</div>
|
|
344
|
+
${r.reasoning ? `<div class="field"><span class="fk">Why it stands</span><p>${escapeHtml(r.reasoning)}</p></div>` : ''}
|
|
345
|
+
</article>`).join('')
|
|
346
|
+
: '<p class="muted">No assumption failed scrutiny — the council did not sustain any objection.</p>';
|
|
347
|
+
const blindGrid = blindSpots.length
|
|
348
|
+
? `<ul class="checks">${blindSpots.map((b) => `<li>${escapeHtml(b)}</li>`).join('')}</ul>`
|
|
349
|
+
: '<p class="muted">The council covered the major angles.</p>';
|
|
350
|
+
const agreeList = agreements.length
|
|
351
|
+
? `<ul class="agree">${agreements.map((a) => `<li><p>${escapeHtml(a.statement)}</p>${providerDots(a.providers)}</li>`).join('')}</ul>`
|
|
352
|
+
: '<p class="muted">No point drew agreement from more than one model.</p>';
|
|
353
|
+
const steps = nextSteps.length
|
|
354
|
+
? `<ol class="steps">${nextSteps.map((s) => `<li>${escapeHtml(s)}</li>`).join('')}</ol>`
|
|
355
|
+
: '<p class="muted">No follow-ups derived.</p>';
|
|
356
|
+
const defendedBlock = defended.length
|
|
357
|
+
? `<details class="fold reveal" style="animation-delay:0ms">
|
|
358
|
+
<summary>Objections the council dismissed (${defended.length}) — your idea held up here</summary>
|
|
359
|
+
<div class="fold-body">${defended.map((d) => `
|
|
360
|
+
<article class="card mini">
|
|
361
|
+
<h3>${escapeHtml(clip(d.assumption, 160))}</h3>
|
|
362
|
+
<div class="field"><span class="fk">Objection</span>${paras(d.challenge)}</div>
|
|
363
|
+
${d.reasoning ? `<div class="field"><span class="fk">Why it was dismissed</span><p>${escapeHtml(d.reasoning)}</p></div>` : ''}
|
|
364
|
+
</article>`).join('')}</div>
|
|
365
|
+
</details>`
|
|
366
|
+
: '';
|
|
367
|
+
const chairman = view.keyPoints?.length
|
|
368
|
+
? section('', "Chairman's reasoning", `<ul class="reasons">${view.keyPoints.map((p) => `<li>${escapeHtml(p)}</li>`).join('')}</ul>`, 100, 'How the chair weighed the debate to reach the verdict above.')
|
|
369
|
+
: '';
|
|
370
|
+
const modelCards = view.columns.length
|
|
371
|
+
? view.columns.map((c) => `
|
|
372
|
+
<article class="card model-card">
|
|
373
|
+
<h3>${escapeHtml(c.title)}</h3>
|
|
374
|
+
${c.lines.length ? `<ul class="model-lines">${c.lines.slice(0, 10).map((l) => `<li>${escapeHtml(l)}</li>`).join('')}</ul>` : '<p class="muted">No output recorded.</p>'}
|
|
375
|
+
</article>`).join('')
|
|
376
|
+
: '<p class="muted">No per-model output recorded.</p>';
|
|
377
|
+
return `
|
|
378
|
+
${hero}
|
|
379
|
+
${chairman}
|
|
380
|
+
${bottomLine}
|
|
381
|
+
${section('01', 'Risks that held up', riskCards, 180, 'Assumptions your idea depends on that the council challenged — and the challenge stuck.')}
|
|
382
|
+
${section('02', 'How each model saw it', `<div class="model-grid">${modelCards}</div>`, 220, 'Each model analysed the idea alone, then cross-examined the others. Here is where each one landed.')}
|
|
383
|
+
${section('03', 'Blind spots — answer these before you build', blindGrid, 260, 'Angles the council did not examine. These are usually where ideas actually fail.')}
|
|
384
|
+
${section('04', 'Where the models agreed', agreeList, 300, 'Points more than one model independently backed.')}
|
|
385
|
+
${section('05', 'Recommended next steps', steps, 360, 'Derived from the risks and blind spots above.')}
|
|
386
|
+
${defendedBlock}
|
|
387
|
+
${renderTechnical(view)}
|
|
388
|
+
`;
|
|
389
|
+
}
|
|
390
|
+
function renderIdeaBody(view) {
|
|
391
|
+
if (!view.recommendation && !view.actionPlan)
|
|
392
|
+
return renderLegacyIdeaBody(view);
|
|
393
|
+
const risks = view.risks ?? [];
|
|
394
|
+
const agreements = view.agreements ?? [];
|
|
395
|
+
const blindSpots = view.blindSpots ?? [];
|
|
396
|
+
const signal = view.signal ?? { label: 'Reviewed', tone: 'caution' };
|
|
397
|
+
const tone = recommendationTone(view.recommendation, signal.tone);
|
|
398
|
+
const label = recommendationLabel(view.recommendation, signal.label);
|
|
399
|
+
const conditions = view.conditions?.length
|
|
400
|
+
? `<div class="conditions"><span class="fk">Conditions</span><ul>${view.conditions.map((c) => `<li>${escapeHtml(c)}</li>`).join('')}</ul></div>`
|
|
401
|
+
: '';
|
|
402
|
+
const glance = `
|
|
403
|
+
<div class="glance">
|
|
404
|
+
<div class="stat good"><span class="n">${agreements.length}</span><span class="k">agreed on</span></div>
|
|
405
|
+
<div class="stat risk"><span class="n">${risks.length}</span><span class="k">risks that stand</span></div>
|
|
406
|
+
<div class="stat caution"><span class="n">${blindSpots.length}</span><span class="k">not examined</span></div>
|
|
407
|
+
</div>`;
|
|
408
|
+
const hero = `
|
|
409
|
+
<section class="verdict tone-${tone} reveal" style="animation-delay:60ms">
|
|
410
|
+
<span class="pill">${escapeHtml(label)}</span>
|
|
411
|
+
<p class="verdict-text">${escapeHtml(view.verdict)}</p>
|
|
412
|
+
${conditions}
|
|
413
|
+
${glance}
|
|
414
|
+
</section>`;
|
|
415
|
+
const chairman = view.keyPoints?.length
|
|
416
|
+
? section('01', "Chairman's reasoning", `<ul class="reasons">${view.keyPoints.map((p) => `<li>${escapeHtml(p)}</li>`).join('')}</ul>`, 100)
|
|
417
|
+
: '';
|
|
418
|
+
const scorecard = view.scorecard?.length
|
|
419
|
+
? `<div class="score-grid">${view.scorecard.map((s) => `<div class="score ${s.status}"><span>${escapeHtml(s.label)}</span><strong>${escapeHtml(s.status)}</strong></div>`).join('')}</div>`
|
|
420
|
+
: '';
|
|
421
|
+
const audit = view.audit?.length
|
|
422
|
+
? `<div class="table-wrap"><table class="data-table"><thead><tr><th>Statement</th><th>Status</th><th>Confidence</th><th>Analysts</th></tr></thead><tbody>
|
|
423
|
+
${view.audit.map((r) => `<tr><td>${escapeHtml(r.statement)}</td><td>${escapeHtml(r.status)}</td><td>${escapeHtml(r.confidence)}</td><td>${escapeHtml(r.providers.map(providerName).join(' · '))}</td></tr>`).join('')}
|
|
424
|
+
</tbody></table></div>`
|
|
425
|
+
: '';
|
|
426
|
+
const riskCards = risks.length
|
|
427
|
+
? risks.map((r) => `
|
|
428
|
+
<article class="card risk-card">
|
|
429
|
+
<div class="card-top"><span class="chip ${sevClass(r.severity)}">${escapeHtml(sevLabel(r.severity))}</span>${providerDots(r.providers)}</div>
|
|
430
|
+
<h3>${escapeHtml(r.assumption)}</h3>
|
|
431
|
+
<div class="field"><span class="fk">The challenge</span>${paras(r.challenge)}</div>
|
|
432
|
+
${r.reasoning ? `<div class="field"><span class="fk">Why it stands</span><p>${escapeHtml(r.reasoning)}</p></div>` : ''}
|
|
433
|
+
</article>`).join('')
|
|
434
|
+
: '<p class="muted">No assumption failed scrutiny — the council did not sustain any objection.</p>';
|
|
435
|
+
const debate = view.debates?.length
|
|
436
|
+
? view.debates.map((d) => `
|
|
437
|
+
<article class="card debate-card">
|
|
438
|
+
<p>${providerDots(d.claimantProviders)} <strong>claimed</strong> ${escapeHtml(d.claim)}.</p>
|
|
439
|
+
<div class="field"><span class="fk">Counter</span>${providerDots(d.attackerProviders)}${paras(d.challenge)}</div>
|
|
440
|
+
<div class="field"><span class="fk">Chair</span><p>${escapeHtml(d.chair)}${d.reasoning ? ` — ${escapeHtml(d.reasoning)}` : ''}</p></div>
|
|
441
|
+
</article>`).join('')
|
|
442
|
+
: '<p class="muted">No contradictions reached the chair.</p>';
|
|
443
|
+
const plan = view.actionPlan
|
|
444
|
+
? `<div class="table-wrap"><table class="data-table plan-table"><thead><tr><th>#</th><th>Action</th><th>Why</th><th>Validates</th><th>Effort</th><th>Kill signal</th></tr></thead><tbody>
|
|
445
|
+
${view.actionPlan.actions.map((a) => `<tr><td>${a.order}</td><td>${escapeHtml(a.action)}</td><td>${escapeHtml(a.why)}</td><td><code>${escapeHtml(a.validates)}</code></td><td>${escapeHtml(a.effort)}</td><td>${escapeHtml(a.kill_signal)}</td></tr>`).join('')}
|
|
446
|
+
</tbody></table><p class="lede">${escapeHtml(view.actionPlan.sequencing_note)}</p></div>`
|
|
447
|
+
: '<p class="muted">No validation plan artifact recorded.</p>';
|
|
448
|
+
const questions = view.openQuestions?.length
|
|
449
|
+
? `<ul class="checks">${view.openQuestions.map((q) => `<li>${escapeHtml(q)}</li>`).join('')}</ul>`
|
|
450
|
+
: '<p class="muted">No verdict-flipping open questions recorded.</p>';
|
|
451
|
+
const redTeam = `
|
|
452
|
+
<div class="redteam">
|
|
453
|
+
<div><span class="fk">Strongest counter-argument</span><ul>${view.dissent.map((d) => `<li>${escapeHtml(d)}</li>`).join('')}</ul></div>
|
|
454
|
+
<div><span class="fk">Confidence</span><p>${escapeHtml(view.confidence || 'None recorded.')}</p></div>
|
|
455
|
+
</div>`;
|
|
456
|
+
const receipt = view.receipt?.length
|
|
457
|
+
? `<div class="receipt">${view.receipt.map((r) => `<span>${escapeHtml(r)}</span>`).join('')}</div>`
|
|
458
|
+
: '';
|
|
459
|
+
const modelCards = view.columns.length
|
|
460
|
+
? view.columns.map((c) => `
|
|
461
|
+
<article class="card model-card">
|
|
462
|
+
<h3>${escapeHtml(c.title)}</h3>
|
|
463
|
+
${c.lines.length ? `<ul class="model-lines">${c.lines.slice(0, 10).map((l) => `<li>${escapeHtml(l)}</li>`).join('')}</ul>` : '<p class="muted">No output recorded.</p>'}
|
|
464
|
+
</article>`).join('')
|
|
465
|
+
: '<p class="muted">No per-model output recorded.</p>';
|
|
466
|
+
return `
|
|
467
|
+
${hero}
|
|
468
|
+
${chairman}
|
|
469
|
+
${scorecard ? section('02', 'Dimension scorecard', scorecard, 140, 'Best-effort keyword coverage across the 12 idea-vetting dimensions.') : ''}
|
|
470
|
+
${audit ? section('03', 'Assumption audit', audit, 180, 'Held, failed, or unverified assumptions derived from the disagreement map and chair rulings.') : ''}
|
|
471
|
+
${section('04', 'Risks that held up', riskCards, 220, 'Assumptions your idea depends on that the council challenged — and the challenge stuck.')}
|
|
472
|
+
${section('05', 'The debate', debate, 260, 'Who claimed what, who objected, and how the chair ruled.')}
|
|
473
|
+
${section('06', 'Validation plan', plan, 300, 'Ordered checks with kill signals, anchored to risks, blind spots, or open questions.')}
|
|
474
|
+
${section('07', 'Open questions that flip the verdict', questions, 340)}
|
|
475
|
+
${section('08', 'Red-team note', redTeam, 380)}
|
|
476
|
+
${receipt ? section('09', 'Receipt', receipt, 420) : ''}
|
|
477
|
+
${section('10', 'How each model saw it', `<div class="model-grid">${modelCards}</div>`, 460)}
|
|
478
|
+
${renderTechnical(view)}
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
/** Serialize a council view to clean Markdown — for the HTML "Copy" button (paste into a coding assistant). */
|
|
482
|
+
function councilMarkdown(view) {
|
|
483
|
+
const isIdea = view.workflow !== 'code-review';
|
|
484
|
+
const reportV3 = isIdea && Boolean(view.recommendation || view.actionPlan);
|
|
485
|
+
const L = [];
|
|
486
|
+
L.push(`# ${isIdea && view.topic ? cleanTopic(view.topic) : isIdea ? 'Idea refinement' : 'Code review'}`, '');
|
|
487
|
+
const panel = view.columns.map((c) => c.title).join(', ');
|
|
488
|
+
if (panel)
|
|
489
|
+
L.push(`Panel: ${panel}${view.moderator ? ` · Chair: ${view.moderator}` : ''}`, '');
|
|
490
|
+
if (view.recommendation) {
|
|
491
|
+
L.push('## Bottom line', '', `**${recommendationLabel(view.recommendation, '')}** — ${view.verdict}`, '');
|
|
492
|
+
if (view.conditions?.length) {
|
|
493
|
+
L.push('Conditions:');
|
|
494
|
+
for (const c of view.conditions)
|
|
495
|
+
L.push(`- ${c}`);
|
|
496
|
+
L.push('');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
L.push('## Verdict', '', view.verdict, '');
|
|
501
|
+
}
|
|
502
|
+
if (view.keyPoints?.length) {
|
|
503
|
+
L.push("## Chairman's reasoning", '');
|
|
504
|
+
for (const p of view.keyPoints)
|
|
505
|
+
L.push(`- ${p}`);
|
|
506
|
+
L.push('');
|
|
507
|
+
}
|
|
508
|
+
if (isIdea) {
|
|
509
|
+
if (reportV3 && view.scorecard?.length) {
|
|
510
|
+
L.push('## Dimension scorecard (best-effort)', '');
|
|
511
|
+
for (const s of view.scorecard)
|
|
512
|
+
L.push(`- ${s.label}: ${s.status}`);
|
|
513
|
+
L.push('');
|
|
514
|
+
}
|
|
515
|
+
if (reportV3 && view.audit?.length) {
|
|
516
|
+
L.push('## Assumption audit', '');
|
|
517
|
+
for (const a of view.audit)
|
|
518
|
+
L.push(`- ${a.status}/${a.confidence}: ${a.statement} (${a.providers.map(providerName).join(', ')})`);
|
|
519
|
+
L.push('');
|
|
520
|
+
}
|
|
521
|
+
if (view.risks?.length) {
|
|
522
|
+
L.push('## Risks that held up', '');
|
|
523
|
+
for (const r of view.risks) {
|
|
524
|
+
L.push(`### ${r.assumption} (${sevLabel(r.severity)})`);
|
|
525
|
+
L.push(`- Challenge: ${r.challenge.replace(/\s*\n+\s*/g, ' ')}`);
|
|
526
|
+
if (r.reasoning)
|
|
527
|
+
L.push(`- Why it stands: ${r.reasoning}`);
|
|
528
|
+
L.push('');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (reportV3 && view.debates?.length) {
|
|
532
|
+
L.push('## The debate', '');
|
|
533
|
+
for (const d of view.debates) {
|
|
534
|
+
L.push(`- ${d.claimantProviders.map(providerName).join(', ')} claimed ${d.claim}. ${d.attackerProviders.map(providerName).join(', ')} countered: ${d.challenge.replace(/\s*\n+\s*/g, ' ')}. Chair: ${d.chair}${d.reasoning ? ` — ${d.reasoning}` : ''}.`);
|
|
535
|
+
}
|
|
536
|
+
L.push('');
|
|
537
|
+
}
|
|
538
|
+
if (reportV3 && view.actionPlan) {
|
|
539
|
+
L.push('## Validation plan', '');
|
|
540
|
+
for (const a of view.actionPlan.actions) {
|
|
541
|
+
L.push(`${a.order}. ${a.action}`);
|
|
542
|
+
L.push(` - Why: ${a.why}`);
|
|
543
|
+
L.push(` - Validates: ${a.validates}`);
|
|
544
|
+
L.push(` - Effort: ${a.effort}`);
|
|
545
|
+
L.push(` - Kill signal: ${a.kill_signal}`);
|
|
546
|
+
}
|
|
547
|
+
L.push('', view.actionPlan.sequencing_note, '');
|
|
548
|
+
}
|
|
549
|
+
if (reportV3 && view.openQuestions?.length) {
|
|
550
|
+
L.push('## Open questions that flip the verdict', '');
|
|
551
|
+
for (const q of view.openQuestions)
|
|
552
|
+
L.push(`- ${q}`);
|
|
553
|
+
L.push('');
|
|
554
|
+
}
|
|
555
|
+
if (view.columns.length) {
|
|
556
|
+
L.push('## How each model saw it', '');
|
|
557
|
+
for (const c of view.columns) {
|
|
558
|
+
L.push(`### ${c.title}`);
|
|
559
|
+
for (const l of c.lines.slice(0, 10))
|
|
560
|
+
L.push(`- ${l}`);
|
|
561
|
+
L.push('');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (view.blindSpots?.length) {
|
|
565
|
+
L.push('## Blind spots to resolve', '');
|
|
566
|
+
for (const b of view.blindSpots)
|
|
567
|
+
L.push(`- ${b}`);
|
|
568
|
+
L.push('');
|
|
569
|
+
}
|
|
570
|
+
if (view.agreements?.length) {
|
|
571
|
+
L.push('## Where the models agreed', '');
|
|
572
|
+
for (const a of view.agreements)
|
|
573
|
+
L.push(`- ${a.statement}`);
|
|
574
|
+
L.push('');
|
|
575
|
+
}
|
|
576
|
+
if (!reportV3 && view.nextSteps?.length) {
|
|
577
|
+
L.push('## Recommended next steps', '');
|
|
578
|
+
view.nextSteps.forEach((s, i) => L.push(`${i + 1}. ${s}`));
|
|
579
|
+
L.push('');
|
|
580
|
+
}
|
|
581
|
+
if (reportV3 && view.receipt?.length) {
|
|
582
|
+
L.push('## Receipt', '');
|
|
583
|
+
for (const r of view.receipt)
|
|
584
|
+
L.push(`- ${r}`);
|
|
585
|
+
L.push('');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
else if (view.rows.length) {
|
|
589
|
+
L.push('## Findings', '');
|
|
590
|
+
for (const r of view.rows) {
|
|
591
|
+
L.push(`- **${r.title}**${r.ruling ? ` — judge: ${r.ruling}` : ''}`);
|
|
592
|
+
if (r.detail)
|
|
593
|
+
L.push(` - ${r.detail}`);
|
|
594
|
+
}
|
|
595
|
+
L.push('');
|
|
596
|
+
}
|
|
597
|
+
if (view.dissent.length) {
|
|
598
|
+
L.push('## Dissent (strongest counter-argument)', '');
|
|
599
|
+
for (const d of view.dissent)
|
|
600
|
+
L.push(`- ${d}`);
|
|
601
|
+
L.push('');
|
|
602
|
+
}
|
|
603
|
+
if (view.confidence)
|
|
604
|
+
L.push('## Confidence', '', view.confidence, '');
|
|
605
|
+
L.push('---', `Generated by aiki · ${view.runId} · analysis, not advice — verify before acting.`);
|
|
606
|
+
return L.join('\n');
|
|
607
|
+
}
|
|
608
|
+
function renderReviewBody(view) {
|
|
609
|
+
const badge = (k) => {
|
|
610
|
+
const label = k === 'consensus' ? 'Both agreed' : k === 'dispute' ? 'Disputed' : k === 'single' ? 'One reviewer' : k;
|
|
611
|
+
return `<span class="chip k-${k}">${escapeHtml(label)}</span>`;
|
|
612
|
+
};
|
|
613
|
+
const rows = view.rows.map((r) => `
|
|
614
|
+
<article class="card">
|
|
615
|
+
<div class="card-top">${badge(r.kind)}${providerDots(r.providers)}</div>
|
|
616
|
+
<h3>${escapeHtml(r.title)}</h3>
|
|
617
|
+
${r.detail ? `<div class="field"><span class="fk">Detail</span><p>${escapeHtml(r.detail)}</p></div>` : ''}
|
|
618
|
+
${r.ruling ? `<div class="field"><span class="fk">Judge</span><p>${escapeHtml(r.ruling)}</p></div>` : ''}
|
|
619
|
+
</article>`).join('');
|
|
620
|
+
const statsRow = view.stats.map((s) => `<span class="mchip">${escapeHtml(s)}</span>`).join('');
|
|
621
|
+
const hero = `
|
|
622
|
+
<section class="verdict tone-caution reveal" style="animation-delay:60ms">
|
|
623
|
+
<span class="pill">Review complete</span>
|
|
624
|
+
<p class="verdict-text">${escapeHtml(view.verdict)}</p>
|
|
625
|
+
<div class="mstrip">${statsRow}</div>
|
|
626
|
+
</section>`;
|
|
627
|
+
return `
|
|
628
|
+
${hero}
|
|
629
|
+
${section('01', 'Findings', rows || '<p class="muted">No findings recorded.</p>', 180, 'Every issue the reviewers raised, and where they agreed or disagreed.')}
|
|
630
|
+
${renderTechnical(view)}
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
/** Collapsed power-user block: per-model raw output, dissent, confidence notes. */
|
|
634
|
+
function renderTechnical(view) {
|
|
635
|
+
const columns = view.columns.map((c) => `
|
|
636
|
+
<div class="col">
|
|
637
|
+
<h4>${escapeHtml(c.title)}</h4>
|
|
638
|
+
${c.lines.length ? `<ul>${c.lines.slice(0, 12).map((l) => `<li>${escapeHtml(l)}</li>`).join('')}</ul>` : '<p class="muted">No output recorded.</p>'}
|
|
639
|
+
</div>`).join('');
|
|
640
|
+
const dissent = view.dissent.length ? `<ul>${view.dissent.map((d) => `<li>${escapeHtml(d)}</li>`).join('')}</ul>` : '<p class="muted">None recorded.</p>';
|
|
641
|
+
return `
|
|
642
|
+
<details class="fold reveal" style="animation-delay:0ms">
|
|
643
|
+
<summary>Full council analysis (technical)</summary>
|
|
644
|
+
<div class="fold-body">
|
|
645
|
+
<h4 class="fold-h">Each model, in its own words</h4>
|
|
646
|
+
<div class="cols">${columns || '<p class="muted">No model output recorded.</p>'}</div>
|
|
647
|
+
<h4 class="fold-h">The moderator's strongest counter-argument (dissent)</h4>
|
|
648
|
+
${dissent}
|
|
649
|
+
<h4 class="fold-h">Confidence notes</h4>
|
|
650
|
+
<p>${escapeHtml(view.confidence || 'None recorded.')}</p>
|
|
651
|
+
</div>
|
|
652
|
+
</details>`;
|
|
653
|
+
}
|
|
654
|
+
export function renderCouncilHtml(view) {
|
|
655
|
+
const isIdea = view.workflow !== 'code-review';
|
|
656
|
+
const kicker = isIdea ? 'aiki · idea refinement' : 'aiki · code review';
|
|
657
|
+
const title = isIdea && view.topic ? cleanTopic(view.topic) : (isIdea ? 'Idea refinement' : 'Code review');
|
|
658
|
+
const panel = view.columns.map((c) => c.title);
|
|
659
|
+
const metaBits = [
|
|
660
|
+
panel.length ? `Panel: ${panel.join(' · ')}` : '',
|
|
661
|
+
view.moderator ? `Chair: ${view.moderator}` : '',
|
|
662
|
+
view.calls,
|
|
663
|
+
].filter(Boolean);
|
|
664
|
+
const flags = view.flags.length
|
|
665
|
+
? `<div class="warns">${view.flags.map((f) => `<span class="warn">⚑ ${escapeHtml(f.replaceAll('_', ' '))}</span>`).join('')}</div>`
|
|
666
|
+
: '';
|
|
667
|
+
const body = isIdea ? renderIdeaBody(view) : renderReviewBody(view);
|
|
668
|
+
// Embed the report as Markdown for the Copy button. Escape "<" so a "</script>" in content can't break out.
|
|
669
|
+
const md = JSON.stringify(councilMarkdown(view)).replace(/</g, '\\u003c');
|
|
670
|
+
return `<!doctype html>
|
|
671
|
+
<html lang="en">
|
|
672
|
+
<head>
|
|
673
|
+
<meta charset="utf-8">
|
|
674
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
675
|
+
<title>${escapeHtml(isIdea ? 'Idea refinement' : 'Code review')} — aiki council</title>
|
|
676
|
+
<style>
|
|
677
|
+
:root{
|
|
678
|
+
color-scheme: light;
|
|
679
|
+
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Charter,Georgia,"Times New Roman",serif;
|
|
680
|
+
--sans:-apple-system,BlinkMacSystemFont,"Avenir Next","Segoe UI",system-ui,sans-serif;
|
|
681
|
+
--mono:"SF Mono","JetBrains Mono",ui-monospace,Menlo,Consolas,monospace;
|
|
682
|
+
--paper:#ffffff; --panel:#f7f8fa; --ink:#1a1c1f; --soft:#5b6470; --faint:#9aa1ab; --line:#e6e8ec;
|
|
683
|
+
--good:#15803d; --good-bg:#e9f7ef; --risk:#c0392b; --risk-bg:#fbeae7;
|
|
684
|
+
--caution:#b45309; --caution-bg:#fdf2e2; --slate:#475569; --accent:#2563eb;
|
|
685
|
+
}
|
|
686
|
+
*{box-sizing:border-box;}
|
|
687
|
+
html{-webkit-text-size-adjust:100%;}
|
|
688
|
+
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.6;}
|
|
689
|
+
main{max-width:800px;margin:0 auto;padding:34px 24px 90px;}
|
|
690
|
+
a{color:var(--accent);}
|
|
691
|
+
h1,h2,h3,h4{font-family:var(--sans);font-weight:650;letter-spacing:-.01em;}
|
|
692
|
+
p{margin:0 0 .6em;}
|
|
693
|
+
|
|
694
|
+
/* masthead */
|
|
695
|
+
.mast{border-bottom:2px solid var(--ink);padding-bottom:22px;margin-bottom:30px;}
|
|
696
|
+
.kicker{font-family:var(--mono);font-size:11.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);}
|
|
697
|
+
.mast h1{font-size:clamp(28px,4.6vw,42px);line-height:1.12;margin:12px 0 16px;}
|
|
698
|
+
.mmeta{display:flex;flex-wrap:wrap;gap:7px;}
|
|
699
|
+
.mmeta span{font-family:var(--mono);font-size:11.5px;color:var(--soft);border:1px solid var(--line);background:var(--panel);border-radius:100px;padding:3px 10px;}
|
|
700
|
+
.warns{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;}
|
|
701
|
+
.warn{font-family:var(--mono);font-size:11.5px;color:var(--caution);background:var(--caution-bg);border:1px solid #e6d09b;border-radius:6px;padding:3px 9px;}
|
|
702
|
+
|
|
703
|
+
/* verdict hero */
|
|
704
|
+
.verdict{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:14px;
|
|
705
|
+
padding:26px 28px 22px;margin-bottom:18px;overflow:hidden;}
|
|
706
|
+
.verdict::before{content:"";position:absolute;left:0;top:0;bottom:0;width:6px;}
|
|
707
|
+
.tone-good::before{background:var(--good);} .tone-caution::before{background:var(--caution);} .tone-risk::before{background:var(--risk);}
|
|
708
|
+
.pill{display:inline-block;font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.02em;
|
|
709
|
+
padding:5px 13px;border-radius:100px;margin-bottom:14px;}
|
|
710
|
+
.tone-good .pill{background:var(--good-bg);color:var(--good);} .tone-caution .pill{background:var(--caution-bg);color:var(--caution);}
|
|
711
|
+
.tone-risk .pill{background:var(--risk-bg);color:var(--risk);}
|
|
712
|
+
.verdict-text{font-family:var(--sans);font-weight:520;font-size:clamp(18px,2.3vw,21px);line-height:1.5;color:var(--ink);margin:0;}
|
|
713
|
+
|
|
714
|
+
/* top action bar + copy */
|
|
715
|
+
.bar{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
716
|
+
padding:10px 20px;background:var(--panel);border-bottom:1px solid var(--line);}
|
|
717
|
+
.bar-label{font-family:var(--mono);font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--soft);}
|
|
718
|
+
.copy-btn{font-family:var(--sans);font-size:13.5px;font-weight:600;cursor:pointer;color:#fff;background:var(--accent);
|
|
719
|
+
border:0;border-radius:8px;padding:8px 15px;transition:background .15s ease,transform .1s ease;}
|
|
720
|
+
.copy-btn:hover{filter:brightness(1.05);} .copy-btn:active{transform:translateY(1px);}
|
|
721
|
+
.copy-btn.ok{background:var(--good);}
|
|
722
|
+
|
|
723
|
+
/* chairman's reasoning */
|
|
724
|
+
.reasons{margin:0;padding:0;list-style:none;}
|
|
725
|
+
.reasons li{position:relative;padding:11px 0 11px 26px;border-bottom:1px solid var(--line);font-size:15.5px;color:var(--ink);}
|
|
726
|
+
.reasons li:last-child{border-bottom:0;}
|
|
727
|
+
.reasons li::before{content:"";position:absolute;left:4px;top:18px;width:7px;height:7px;border-radius:50%;background:var(--accent);}
|
|
728
|
+
|
|
729
|
+
/* per-model grid */
|
|
730
|
+
.model-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;}
|
|
731
|
+
.model-card h3{font-size:15.5px;margin:0 0 10px;padding-bottom:8px;border-bottom:1px solid var(--line);}
|
|
732
|
+
.model-lines{margin:0;padding-left:18px;} .model-lines li{font-size:13.5px;color:#3a3f47;margin-bottom:7px;}
|
|
733
|
+
.glance{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:22px;}
|
|
734
|
+
.stat{background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:14px 12px;text-align:center;}
|
|
735
|
+
.stat .n{display:block;font-family:var(--serif);font-size:30px;line-height:1;}
|
|
736
|
+
.stat .k{display:block;font-size:12.5px;color:var(--soft);margin-top:6px;}
|
|
737
|
+
.stat.good .n{color:var(--good);} .stat.risk .n{color:var(--risk);} .stat.caution .n{color:var(--caution);}
|
|
738
|
+
|
|
739
|
+
/* bottom line */
|
|
740
|
+
.bottomline{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:34px;}
|
|
741
|
+
.bottomline > div{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;}
|
|
742
|
+
.tag{display:inline-block;font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin-bottom:7px;}
|
|
743
|
+
.bottomline p{margin:0;font-size:15px;color:var(--ink);}
|
|
744
|
+
|
|
745
|
+
/* sections */
|
|
746
|
+
.block{margin:38px 0;}
|
|
747
|
+
.block-head{display:flex;align-items:baseline;gap:12px;border-bottom:1px solid var(--line);padding-bottom:8px;margin-bottom:14px;}
|
|
748
|
+
.idx{font-family:var(--mono);font-size:12px;color:var(--faint);letter-spacing:.1em;}
|
|
749
|
+
.block h2{font-size:22px;margin:0;}
|
|
750
|
+
.lede{color:var(--soft);font-size:14.5px;margin:0 0 16px;max-width:62ch;}
|
|
751
|
+
|
|
752
|
+
/* cards */
|
|
753
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px 20px;margin-bottom:14px;transition:box-shadow .18s ease,transform .18s ease;}
|
|
754
|
+
.card:hover{box-shadow:0 6px 22px rgba(34,29,22,.07);transform:translateY(-1px);}
|
|
755
|
+
.risk-card{border-left:4px solid var(--risk);}
|
|
756
|
+
.card.mini{border-left:4px solid var(--slate);}
|
|
757
|
+
.card-top{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;}
|
|
758
|
+
.card h3{font-size:18px;line-height:1.35;margin:0 0 12px;}
|
|
759
|
+
.card.mini h3{font-size:15.5px;}
|
|
760
|
+
.field{margin-top:10px;}
|
|
761
|
+
.fk{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--faint);margin-bottom:4px;}
|
|
762
|
+
.field p{margin:0 0 .5em;font-size:14.5px;color:#3d3629;}
|
|
763
|
+
.chip{font-family:var(--mono);font-size:11px;font-weight:600;padding:3px 9px;border-radius:6px;white-space:nowrap;}
|
|
764
|
+
.sev-high{background:var(--risk-bg);color:var(--risk);} .sev-med{background:var(--caution-bg);color:var(--caution);} .sev-low{background:#e7edf0;color:var(--slate);}
|
|
765
|
+
.k-consensus{background:var(--good-bg);color:var(--good);} .k-dispute{background:var(--risk-bg);color:var(--risk);} .k-single,.k-unique{background:#e7edf0;color:var(--slate);}
|
|
766
|
+
|
|
767
|
+
/* provider dots */
|
|
768
|
+
.who{display:inline-flex;align-items:center;gap:6px;}
|
|
769
|
+
.dot{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:var(--ink);color:var(--paper);font-family:var(--mono);font-size:9.5px;font-weight:700;}
|
|
770
|
+
.who-names{font-size:12px;color:var(--soft);}
|
|
771
|
+
|
|
772
|
+
/* blind spots */
|
|
773
|
+
.checks{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:10px;}
|
|
774
|
+
.checks li{position:relative;background:var(--caution-bg);border:1px solid #e6d09b;border-radius:10px;padding:12px 14px 12px 38px;font-size:14.5px;color:#5f4a15;}
|
|
775
|
+
.checks li::before{content:"?";position:absolute;left:12px;top:50%;transform:translateY(-50%);width:18px;height:18px;border-radius:50%;background:var(--caution);color:#fff;font-family:var(--mono);font-size:11px;font-weight:700;display:grid;place-items:center;}
|
|
776
|
+
|
|
777
|
+
/* agreements */
|
|
778
|
+
.agree{list-style:none;margin:0;padding:0;}
|
|
779
|
+
.agree li{border-left:4px solid var(--good);background:var(--good-bg);border-radius:0 10px 10px 0;padding:13px 16px;margin-bottom:10px;}
|
|
780
|
+
.agree li p{margin:0 0 8px;font-size:15px;}
|
|
781
|
+
|
|
782
|
+
/* steps */
|
|
783
|
+
.steps{margin:0;padding:0;list-style:none;counter-reset:s;}
|
|
784
|
+
.steps li{counter-increment:s;position:relative;padding:12px 0 12px 46px;border-bottom:1px solid var(--line);font-size:15px;}
|
|
785
|
+
.steps li:last-child{border-bottom:0;}
|
|
786
|
+
.steps li::before{content:counter(s);position:absolute;left:0;top:11px;width:28px;height:28px;border-radius:50%;border:1.5px solid var(--accent);color:var(--accent);font-family:var(--mono);font-size:13px;font-weight:600;display:grid;place-items:center;}
|
|
787
|
+
|
|
788
|
+
/* report v3 */
|
|
789
|
+
.conditions{margin-top:16px;background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:12px 14px;}
|
|
790
|
+
.conditions ul{margin:4px 0 0;padding-left:18px;}
|
|
791
|
+
.conditions li{font-size:14px;margin-bottom:4px;}
|
|
792
|
+
.score-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;}
|
|
793
|
+
.score{border:1px solid var(--line);border-radius:10px;background:var(--panel);padding:12px 14px;min-height:74px;display:flex;flex-direction:column;justify-content:space-between;gap:8px;}
|
|
794
|
+
.score span{font-size:13.5px;color:var(--ink);}
|
|
795
|
+
.score strong{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em;}
|
|
796
|
+
.score.contested{border-left:4px solid var(--caution);} .score.examined{border-left:4px solid var(--good);} .score.unexamined{border-left:4px solid var(--risk);}
|
|
797
|
+
.table-wrap{overflow-x:auto;}
|
|
798
|
+
.data-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:13.5px;}
|
|
799
|
+
.data-table th,.data-table td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:9px 10px;}
|
|
800
|
+
.data-table th{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--soft);background:var(--paper);}
|
|
801
|
+
.data-table tr:last-child td{border-bottom:0;}
|
|
802
|
+
.data-table code{font-family:var(--mono);font-size:12px;color:var(--accent);}
|
|
803
|
+
.debate-card p{margin-bottom:8px;}
|
|
804
|
+
.redteam{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
|
805
|
+
.redteam>div{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px;}
|
|
806
|
+
.redteam ul{margin:4px 0 0;padding-left:18px;}
|
|
807
|
+
.receipt{display:flex;flex-wrap:wrap;gap:8px;}
|
|
808
|
+
.receipt span{font-family:var(--mono);font-size:11.5px;border:1px solid var(--line);background:var(--panel);border-radius:999px;padding:4px 10px;color:var(--soft);}
|
|
809
|
+
|
|
810
|
+
/* folds */
|
|
811
|
+
.fold{margin:24px 0;background:var(--panel);border:1px solid var(--line);border-radius:12px;}
|
|
812
|
+
.fold > summary{cursor:pointer;list-style:none;padding:16px 20px;font-family:var(--mono);font-size:12.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--soft);display:flex;align-items:center;gap:10px;}
|
|
813
|
+
.fold > summary::-webkit-details-marker{display:none;}
|
|
814
|
+
.fold > summary::before{content:"+";font-size:16px;color:var(--accent);}
|
|
815
|
+
.fold[open] > summary::before{content:"–";}
|
|
816
|
+
.fold[open] > summary{border-bottom:1px solid var(--line);color:var(--ink);}
|
|
817
|
+
.fold-body{padding:6px 20px 20px;}
|
|
818
|
+
.fold-h{font-size:14px;color:var(--soft);margin:18px 0 8px;text-transform:none;letter-spacing:0;}
|
|
819
|
+
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;}
|
|
820
|
+
.col{background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:14px;}
|
|
821
|
+
.col h4{margin:0 0 10px;font-size:14px;}
|
|
822
|
+
.col ul,.fold-body ul{margin:0;padding-left:18px;}
|
|
823
|
+
.col li,.fold-body li{font-size:13px;color:#3d3629;margin-bottom:7px;}
|
|
824
|
+
.mstrip{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px;}
|
|
825
|
+
.mchip{font-family:var(--mono);font-size:12px;color:var(--soft);border:1px solid var(--line);background:var(--paper);border-radius:100px;padding:3px 11px;}
|
|
826
|
+
|
|
827
|
+
.muted{color:var(--faint);font-style:italic;}
|
|
828
|
+
footer{margin-top:56px;padding-top:18px;border-top:1px solid var(--line);font-family:var(--mono);font-size:11px;color:var(--faint);}
|
|
829
|
+
|
|
830
|
+
.reveal{opacity:0;transform:translateY(10px);animation:rise .5s cubic-bezier(.2,.7,.2,1) forwards;}
|
|
831
|
+
@keyframes rise{to{opacity:1;transform:none;}}
|
|
832
|
+
@media (max-width:640px){
|
|
833
|
+
main{padding:34px 16px 60px;}
|
|
834
|
+
.glance,.bottomline,.checks,.redteam{grid-template-columns:1fr;}
|
|
835
|
+
}
|
|
836
|
+
@media (prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none;animation:none;}}
|
|
837
|
+
@media print{.reveal{opacity:1;transform:none;animation:none;}.fold[open]{break-inside:avoid;}}
|
|
838
|
+
</style>
|
|
839
|
+
</head>
|
|
840
|
+
<body>
|
|
841
|
+
<div class="bar">
|
|
842
|
+
<span class="bar-label">${escapeHtml(kicker)}</span>
|
|
843
|
+
<button class="copy-btn" onclick="copyReport(this)">Copy report (Markdown)</button>
|
|
844
|
+
</div>
|
|
845
|
+
<main>
|
|
846
|
+
<header class="mast">
|
|
847
|
+
<h1>${escapeHtml(title)}</h1>
|
|
848
|
+
<div class="mmeta">${metaBits.map((b) => `<span>${escapeHtml(b)}</span>`).join('')}</div>
|
|
849
|
+
${flags}
|
|
850
|
+
</header>
|
|
851
|
+
${body}
|
|
852
|
+
<footer>Generated by aiki · ${escapeHtml(view.runId)} · a local model council. This is analysis, not advice — verify before acting.</footer>
|
|
853
|
+
</main>
|
|
854
|
+
<script>
|
|
855
|
+
const REPORT_MD = ${md};
|
|
856
|
+
function copyReport(btn){
|
|
857
|
+
navigator.clipboard.writeText(REPORT_MD).then(function(){
|
|
858
|
+
var o=btn.textContent; btn.textContent='✓ Copied'; btn.classList.add('ok');
|
|
859
|
+
setTimeout(function(){ btn.textContent=o; btn.classList.remove('ok'); }, 1600);
|
|
860
|
+
}).catch(function(){ btn.textContent='Copy failed — select the text manually'; });
|
|
861
|
+
}
|
|
862
|
+
</script>
|
|
863
|
+
</body>
|
|
864
|
+
</html>`;
|
|
865
|
+
}
|
|
866
|
+
export async function writeCouncilHtml(runId, dir) {
|
|
867
|
+
const view = await loadCouncilView(runId, dir);
|
|
868
|
+
if (!view)
|
|
869
|
+
return null;
|
|
870
|
+
const path = join(dir, 'council-view.html');
|
|
871
|
+
await writeFile(path, renderCouncilHtml(view));
|
|
872
|
+
return path;
|
|
873
|
+
}
|