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,19 +1,40 @@
1
1
  // S8 — independently verify graph-selected claims from stored evidence/calculation references.
2
2
  // Provider identity stays hidden; model output is translated back to graph evidence IDs before S9.
3
3
  import { selectEscalations } from '../decision-graph.js';
4
+ import { sanitizeClaimGroups } from '../claim-groups.js';
4
5
  import { ClaimVerificationSet } from '../../schemas/index.js';
5
6
  import { isFatal, StageError } from '../context.js';
6
7
  import { jsonCall } from '../jsonStage.js';
8
+ import { loadSkill } from '../skills.js';
7
9
  const S8_PROMPT = `ROLE: Independent verifier. Check each anonymous decision claim below against the
8
10
  provided evidence cards and deterministic calculation checks. Output ONLY JSON:
9
11
  {"verifications": [{"claim_id": "<G-id>", "status": "VERIFIED|PARTIAL|CONTRADICTED|UNVERIFIABLE",
10
12
  "reasoning": "<concise reason>", "evidence_ids": ["<E-id>"],
11
- "calculation_check": "PASS|FAIL|NOT_APPLICABLE", "missing_evidence": ["<gap>"]}]}
13
+ "calculation_check": "PASS|FAIL|NOT_APPLICABLE", "missing_evidence": ["<gap>"]}],
14
+ "claim_groups": [{"ids": ["<G-id>", "<G-id>"], "relation": "SAME|OPPOSES"}]}
12
15
  VERIFIED = the proposition is supported. CONTRADICTED = it is refuted. PARTIAL = evidence is mixed or
13
16
  incomplete. UNVERIFIABLE = the supplied sources cannot decide. Cite only supplied evidence IDs. Model
14
17
  knowledge cannot verify a current, numeric, legal, medical, financial, market, or regulatory fact.
15
- Judge each item independently; no verdict quota. JSON only.
16
- ITEMS: {{ITEMS_JSON}}`;
18
+ Judge each item independently; no verdict quota.
19
+ claim_groups: scan ALL CLAIMS below (not only ITEMS). Group ids that assert the SAME proposition in
20
+ different words (relation "SAME") and pair ids that directly contradict each other (relation
21
+ "OPPOSES"). Only group claims from different seats; use existing claim ids only; omit the field when
22
+ there are none. JSON only.{{SKILL}}
23
+ ITEMS: {{ITEMS_JSON}}
24
+ ALL CLAIMS (id, proposition, seat — for claim_groups only): {{CLAIMS_JSON}}`;
25
+ export function selectVerificationTargets(claims, max) {
26
+ return claims
27
+ .map((claim, index) => ({ claim, index }))
28
+ .sort((left, right) => Number(right.claim.nature === 'FACTUAL') - Number(left.claim.nature === 'FACTUAL') || left.index - right.index)
29
+ .slice(0, max)
30
+ .map(({ claim }) => claim);
31
+ }
32
+ export function selectVerificationEscalations(graph, max = 8) {
33
+ const candidates = selectEscalations(graph, { max: graph.claims.length });
34
+ const byId = new Map(candidates.map((candidate) => [candidate.claim_id, candidate]));
35
+ const claims = candidates.map((candidate) => graph.claims.find((claim) => claim.id === candidate.claim_id));
36
+ return selectVerificationTargets(claims, max).map((claim) => byId.get(claim.id));
37
+ }
17
38
  function evidenceIdsForClaim(graph, claimId) {
18
39
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
19
40
  const claim = graph.claims.find((item) => item.id === claimId);
@@ -25,14 +46,18 @@ function evidenceIdsForClaim(graph, claimId) {
25
46
  .flatMap((calculation) => calculation.inputs.flatMap((input) => input.evidence_ids));
26
47
  return new Set([...positionEvidence, ...calculationEvidence]);
27
48
  }
28
- function verifierInput(graph) {
49
+ /** Slim, reusable packet seam: selected claims plus only their linked evidence/calculations. */
50
+ export function buildClaimPackets(graph, targets = selectVerificationEscalations(graph)) {
29
51
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
30
52
  const evidenceRefs = new Map(graph.evidence.map((evidence, index) => [`E${index + 1}`, evidence.id]));
31
53
  const aliasByEvidence = new Map([...evidenceRefs].map(([alias, id]) => [id, alias]));
32
54
  const evidenceById = new Map(graph.evidence.map((evidence) => [evidence.id, evidence]));
33
- const items = selectEscalations(graph, { max: 8 }).map((escalation) => {
55
+ const allowedEvidence = new Map();
56
+ const packets = targets.map((escalation) => {
34
57
  const claim = graph.claims.find((item) => item.id === escalation.claim_id);
35
58
  const linkedEvidenceIds = evidenceIdsForClaim(graph, claim.id);
59
+ allowedEvidence.set(claim.id, new Set([...linkedEvidenceIds]
60
+ .map((id) => aliasByEvidence.get(id)).filter((id) => id !== undefined)));
36
61
  return {
37
62
  id: claim.id,
38
63
  kind: escalation.kind,
@@ -64,20 +89,47 @@ function verifierInput(graph) {
64
89
  evidence_hole: graph.holes.evidence.find((hole) => hole.claim_id === claim.id)?.reason,
65
90
  };
66
91
  });
67
- return { prompt: S8_PROMPT.replace('{{ITEMS_JSON}}', JSON.stringify(items, null, 2)), evidenceRefs };
92
+ return { packets, evidenceRefs, allowedEvidence };
93
+ }
94
+ function verifierInput(graph, skill = '') {
95
+ const positionById = new Map(graph.positions.map((position) => [position.id, position]));
96
+ const { packets, evidenceRefs } = buildClaimPackets(graph);
97
+ // Anonymous seat aliases (provider identity stays hidden): S1, S2, … by first appearance.
98
+ const seatAlias = new Map();
99
+ for (const position of graph.positions) {
100
+ if (!seatAlias.has(position.provider))
101
+ seatAlias.set(position.provider, `S${seatAlias.size + 1}`);
102
+ }
103
+ const claimsIndex = graph.claims.map((claim) => ({
104
+ id: claim.id,
105
+ proposition: claim.proposition,
106
+ seats: [...new Set(claim.position_ids.map((id) => seatAlias.get(positionById.get(id)?.provider ?? '') ?? '?'))],
107
+ }));
108
+ const prompt = S8_PROMPT
109
+ .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
110
+ .replace('{{ITEMS_JSON}}', JSON.stringify(packets, null, 2))
111
+ .replace('{{CLAIMS_JSON}}', JSON.stringify(claimsIndex, null, 2));
112
+ return { prompt, evidenceRefs };
68
113
  }
69
- export function buildVerifierPrompt(graph) {
70
- return verifierInput(graph).prompt;
114
+ export function buildVerifierPrompt(graph, skill = '') {
115
+ return verifierInput(graph, skill).prompt;
71
116
  }
72
117
  /** Validate every S8 claim/evidence reference before the chair can see it. */
73
- export function claimVerificationRefIssues(graph, verifications, expectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id)) {
118
+ export function claimVerificationRefIssues(graph, verifications, expectedIds = selectVerificationEscalations(graph).map((item) => item.claim_id)) {
74
119
  const expected = new Set(expectedIds);
120
+ const known = new Set(graph.claims.map((claim) => claim.id));
75
121
  const evidence = new Map(graph.evidence.map((item) => [item.id, item]));
76
122
  const seen = new Set();
77
123
  const issues = [];
78
124
  for (const verification of verifications.verifications) {
79
- if (!expected.has(verification.claim_id))
125
+ // A claim id absent from the graph is a dangling reference (fatal even when the caller's
126
+ // expected set is derived from the verifications themselves, as S9's is).
127
+ if (!known.has(verification.claim_id)) {
80
128
  issues.push(`unknown claim id: ${verification.claim_id}`);
129
+ continue;
130
+ }
131
+ if (!expected.has(verification.claim_id))
132
+ issues.push(`unexpected claim verification: ${verification.claim_id}`);
81
133
  if (seen.has(verification.claim_id))
82
134
  issues.push(`duplicate claim verification: ${verification.claim_id}`);
83
135
  seen.add(verification.claim_id);
@@ -111,7 +163,7 @@ export function claimVerificationRefIssues(graph, verifications, expectedIds = s
111
163
  return issues;
112
164
  }
113
165
  export async function s8Verify(ctx, graph) {
114
- const escalations = selectEscalations(graph, { max: 8 });
166
+ const escalations = selectVerificationEscalations(graph);
115
167
  if (escalations.length === 0) {
116
168
  const empty = { verifications: [] };
117
169
  await ctx.writer.writeJson('verifications', empty);
@@ -138,14 +190,16 @@ export async function s8Verify(ctx, graph) {
138
190
  return skipped;
139
191
  }
140
192
  try {
141
- const input = verifierInput(graph);
193
+ const input = verifierInput(graph, loadSkill('idea-refinement', 'verifier'));
142
194
  // Optional work gets no model repair: one logical optional call must remain one call.
143
195
  const result = await jsonCall(ctx, ctx.handle(ctx.roles.verifier), 'S8', input.prompt, ClaimVerificationSet, { repair: false });
196
+ const claimGroups = sanitizeClaimGroups(graph, result.claim_groups);
144
197
  const translated = {
145
198
  verifications: result.verifications.map((verification) => ({
146
199
  ...verification,
147
200
  evidence_ids: verification.evidence_ids.map((id) => input.evidenceRefs.get(id) ?? id),
148
201
  })),
202
+ ...(claimGroups.length ? { claim_groups: claimGroups } : {}),
149
203
  };
150
204
  const issues = claimVerificationRefIssues(graph, translated, escalations.map((item) => item.claim_id));
151
205
  if (issues.length)
@@ -5,6 +5,7 @@ import { selectRebuttalEscalations, } from '../decision-graph.js';
5
5
  import { isFatal, StageError } from '../context.js';
6
6
  import { jsonCall } from '../jsonStage.js';
7
7
  import { IDEA_MODE_PLANS } from '../modes.js';
8
+ import { loadSkill } from '../skills.js';
8
9
  /** Internal protocol caps only. R6 owns exposing modes and changing the surrounding topology. */
9
10
  export const REBUTTAL_LIMITS = {
10
11
  quick: { maxNodes: 0, optionalCalls: 0 },
@@ -22,7 +23,7 @@ Output ONLY JSON:
22
23
  CONCEDE = your position changes. COUNTER = retain it using a cited supplied card or a precise logical
23
24
  rebuttal. NARROW = state the smaller proposition actually supported. UNRESOLVED = evidence cannot decide.
24
25
  Return exactly one event per assigned claim. Cite only evidence IDs shown for that claim. Do not invent
25
- sources, URLs, claim IDs, or evidence IDs. JSON only.
26
+ sources, URLs, claim IDs, or evidence IDs. JSON only.{{SKILL}}
26
27
  NODES: {{NODES_JSON}}`;
27
28
  function claimAuthors(graph, claimId) {
28
29
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
@@ -70,7 +71,7 @@ export function planRebuttalCalls(graph, escalations, scouts, judge) {
70
71
  return claim_ids?.length ? [{ provider, claim_ids }] : [];
71
72
  });
72
73
  }
73
- function rebuttalInput(graph, verifications, escalationById, assignment) {
74
+ function rebuttalInput(graph, verifications, escalationById, assignment, skill = '') {
74
75
  const claimIds = new Set(assignment.claim_ids);
75
76
  const positionRefs = new Map();
76
77
  const evidenceRefs = new Map();
@@ -130,11 +131,16 @@ function rebuttalInput(graph, verifications, escalationById, assignment) {
130
131
  };
131
132
  });
132
133
  return {
133
- prompt: REBUTTAL_PROMPT.replace('{{NODES_JSON}}', JSON.stringify(nodes, null, 2)),
134
+ prompt: buildRebuttalPrompt(nodes, skill),
134
135
  evidenceRefs,
135
136
  allowedEvidence,
136
137
  };
137
138
  }
139
+ export function buildRebuttalPrompt(nodes, skill = '') {
140
+ return REBUTTAL_PROMPT
141
+ .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
142
+ .replace('{{NODES_JSON}}', JSON.stringify(nodes, null, 2));
143
+ }
138
144
  function translateEvents(graph, assignment, input, output, startIndex) {
139
145
  const expected = new Set(assignment.claim_ids);
140
146
  const seen = new Set();
@@ -189,8 +195,9 @@ export async function s8bRebuttal(ctx, graph, verifications, mode = ctx.mode) {
189
195
  const assignments = planned.slice(0, optionalCalls);
190
196
  const escalationById = new Map(escalations.map((item) => [item.claim_id, item]));
191
197
  const events = [];
198
+ const skill = loadSkill('idea-refinement', 'rebuttal');
192
199
  for (const assignment of assignments) {
193
- const input = rebuttalInput(graph, verifications, escalationById, assignment);
200
+ const input = rebuttalInput(graph, verifications, escalationById, assignment, skill);
194
201
  try {
195
202
  const output = await jsonCall(ctx, ctx.handle(assignment.provider), `S8b-${assignment.provider}`, input.prompt, RebuttalResponseSet, { repair: false });
196
203
  events.push(...translateEvents(graph, assignment, input, output, events.length));
@@ -1,9 +1,10 @@
1
1
  // S9 — chair adjudication over graph-selected escalations. Consensus and shared concerns are
2
2
  // read-only context; anonymous position text and the verifier record cross the boundary unchanged.
3
- import { selectEscalations } from '../decision-graph.js';
4
3
  import { IdeaChairReportModel } from '../../schemas/index.js';
5
4
  import { isFatal, StageError } from '../context.js';
6
5
  import { jsonCall } from '../jsonStage.js';
6
+ import { claimShortLabel } from '../decision-dossier.js';
7
+ import { loadSkill } from '../skills.js';
7
8
  import { claimVerificationRefIssues } from './s8-verify.js';
8
9
  const EMPTY_REBUTTALS = {
9
10
  round: 1,
@@ -39,7 +40,7 @@ Output ONLY JSON matching the judge schema:
39
40
  - key_points: 4-8 standalone decision-relevant bullets.
40
41
  - dissent: a JSON array of strings (an array even when there is only one) — the strongest arguments
41
42
  against your verdict.
42
- - confidence_notes: explain calibrated confidence.
43
+ - confidence_notes: explain calibrated confidence.{{SKILL}}
43
44
  ESCALATED CLAIMS + VERIFICATION: {{ESCALATIONS_JSON}}
44
45
  APPEND-ONLY REBUTTAL EVENTS: {{REBUTTALS_JSON}}
45
46
  SETTLED/UNRESOLVED CONTEXT: {{CONTEXT_JSON}}`;
@@ -147,10 +148,14 @@ export function adjudicationEvidenceViolations(report, verifications) {
147
148
  return [`${item.id}: missing evidence_ids`];
148
149
  const allowed = new Set(verification.evidence_ids);
149
150
  const bad = item.evidence_ids.filter((id) => !allowed.has(id));
150
- const unsettled = (verification.status === 'PARTIAL' || verification.status === 'UNVERIFIABLE') && item.ruling !== 'UNRESOLVED';
151
+ // v6: PARTIAL/UNVERIFIABLE no longer forces UNRESOLVED the chair may rule on judgment with
152
+ // the caveat carried in its reasoning (run f740 lost 5 nuanced HOLDS rulings and a 125s Opus
153
+ // repair to the old rule). The one indefensible combination stays fatal: ruling HOLDS on
154
+ // positively CONTRADICTED evidence.
155
+ const contradicted = verification.status === 'CONTRADICTED' && (item.ruling === 'HOLDS' || item.ruling === 'UPHOLD');
151
156
  return [
152
157
  ...(bad.length ? [`${item.id}: invalid evidence ids ${bad.join(', ')}`] : []),
153
- ...(unsettled ? [`${item.id}: ${verification.status} verification requires UNRESOLVED`] : []),
158
+ ...(contradicted ? [`${item.id}: CONTRADICTED verification cannot rule HOLDS`] : []),
154
159
  ];
155
160
  });
156
161
  }
@@ -173,19 +178,40 @@ export function adjudicableClaimIds(graph, ids, judgeProvider) {
173
178
  return [...ids].filter((id) => !claimById.get(id)?.position_ids
174
179
  .some((positionId) => positionById.get(positionId)?.provider === judgeProvider));
175
180
  }
181
+ /** One condition per distinct claim behind an evidence hole, claim-labeled instead of a generic
182
+ * placeholder reason. Deduped by claim_id and capped at 4 so the fallback conditions list doesn't
183
+ * drown in near-identical entries. */
184
+ export function evidenceHoleConditions(graph) {
185
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
186
+ const seen = new Set();
187
+ const out = [];
188
+ for (const hole of graph.holes.evidence) {
189
+ if (seen.has(hole.claim_id))
190
+ continue;
191
+ seen.add(hole.claim_id);
192
+ const claim = claimById.get(hole.claim_id);
193
+ out.push(`Obtain independent evidence for: "${claimShortLabel(claim?.proposition ?? 'a load-bearing claim')}".`);
194
+ if (out.length === 4)
195
+ break;
196
+ }
197
+ return out;
198
+ }
176
199
  function fallbackConditions(graph, adjudications) {
177
200
  const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
178
201
  const upheld = adjudications
179
202
  .filter((item) => item.ruling === 'UPHOLD')
180
203
  .map((item) => `Proceed only if you can resolve: ${claimById.get(item.id)?.proposition ?? item.id}.`);
181
204
  const holes = graph.holes.coverage.map((hole) => `Proceed only after examining the ${hole.label} gap.`);
182
- const evidenceHoles = graph.holes.evidence.map((hole) => `Proceed only after resolving evidence gap ${hole.claim_id}: ${hole.reason}.`);
205
+ const evidenceHoles = evidenceHoleConditions(graph);
183
206
  return [...upheld, ...holes, ...evidenceHoles, 'Proceed only after one cheap test confirms the core user need.'].slice(0, 6);
184
207
  }
185
- export function buildJudgePrompt(contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS, judgeProvider) {
186
- return judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider).prompt;
208
+ export function buildJudgePrompt(contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS, judgeProvider, skill = '') {
209
+ return judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider, skill).prompt;
210
+ }
211
+ export function buildChairRepairPrompt(basePrompt, corrections) {
212
+ return `${basePrompt}\n\n---\nCorrect these problems:\n${corrections}Output ONLY corrected JSON.`;
187
213
  }
188
- function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider) {
214
+ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider, skill = '') {
189
215
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
190
216
  const verificationById = new Map(verifications.verifications.map((item) => [item.claim_id, item]));
191
217
  const citedEvidence = [...new Set([
@@ -194,7 +220,10 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
194
220
  ])];
195
221
  const evidenceRefs = new Map(citedEvidence.map((id, index) => [`E${index + 1}`, id]));
196
222
  const aliasByEvidence = new Map([...evidenceRefs].map(([alias, id]) => [id, alias]));
197
- const selectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id);
223
+ // Chair scope = the claims S8 actually verified (see s9Judge) recomputing the escalation
224
+ // selection here on the overlaid graph would ask the chair to rule on claims that have no
225
+ // verifier record, which adjudicationEvidenceViolations then rejects after a paid call.
226
+ const selectedIds = verifications.verifications.map((item) => item.claim_id);
198
227
  const eligibleIds = judgeProvider ? adjudicableClaimIds(graph, selectedIds, judgeProvider) : selectedIds;
199
228
  const escalationIds = new Set(eligibleIds);
200
229
  const escalations = graph.claims.filter((claim) => escalationIds.has(claim.id)).map((claim) => {
@@ -234,6 +263,7 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
234
263
  ...(event.narrowed_proposition ? { narrowed_proposition: event.narrowed_proposition } : {}),
235
264
  }));
236
265
  const prompt = S9_PROMPT
266
+ .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
237
267
  .replace('{{RUBRIC_JSON}}', JSON.stringify(rubric.map((item) => item.label)))
238
268
  .replace('{{ESCALATIONS_JSON}}', JSON.stringify(escalations, null, 2))
239
269
  .replace('{{REBUTTALS_JSON}}', JSON.stringify(rebuttalContext, null, 2))
@@ -242,13 +272,21 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
242
272
  return { prompt, evidenceRefs };
243
273
  }
244
274
  export async function s9Judge(ctx, contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS) {
245
- const selectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id);
275
+ // The chair adjudicates what S8 ACTUALLY verified (persisted in 08, replay-safe) never a
276
+ // re-ranked selection. S8 picks targets from the pre-overlay graph and validates its own output
277
+ // against them; overlayClaimGroups (built from S8's OWN claim_groups) then shifts claim states,
278
+ // so re-running selectVerificationEscalations on the joined graph can disagree with a selection
279
+ // S8 could not have predicted. Run e1ce (2026-07-18): the re-selection dropped G2 / added G7 and
280
+ // rejected a fully-compliant verification set. The chair also structurally requires a
281
+ // verification per adjudicated claim (adjudicationEvidenceViolations), so the verified set IS
282
+ // the chair's scope; dangling/duplicate/evidence-linkage guards below still apply.
283
+ const selectedIds = verifications.verifications.map((item) => item.claim_id);
246
284
  const verificationIssues = claimVerificationRefIssues(graph, verifications, selectedIds);
247
285
  if (verificationIssues.length)
248
286
  throw new StageError('S9', 'BAD_OUTPUT', `invalid verification references: ${verificationIssues.join('; ')}`);
249
287
  const ids = adjudicableClaimIds(graph, selectedIds, ctx.roles.judge);
250
288
  const selfAuthored = new Set(selectedIds.filter((id) => !ids.includes(id)));
251
- const input = judgeInput(contract, graph, verifications, rubric, rebuttals, ctx.roles.judge);
289
+ const input = judgeInput(contract, graph, verifications, rubric, rebuttals, ctx.roles.judge, loadSkill('idea-refinement', 'chair'));
252
290
  const basePrompt = input.prompt;
253
291
  const judge = ctx.handle(ctx.roles.judge);
254
292
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
@@ -291,13 +329,13 @@ export async function s9Judge(ctx, contract, graph, verifications, rubric, rebut
291
329
  ];
292
330
  if ((violations.length || evidenceViolations.length || report.dissent.length === 0 || recIssues.length || chairIssues.length)
293
331
  && ctx.budget.limit - ctx.budget.used > 1) {
294
- const repair = `${basePrompt}\n\n---\nCorrect these problems:\n`
332
+ const corrections = ''
295
333
  + (violations.length ? `- adjudications may reference only [${ids.join(', ')}], not ${violations.join(', ')}\n` : '')
296
334
  + (evidenceViolations.length ? `- evidence reference errors: ${evidenceViolations.join('; ')}\n` : '')
297
335
  + (report.dissent.length === 0 ? '- dissent must contain at least one item\n' : '')
298
336
  + (recIssues.length ? `- ${recIssues.join('; ')}\n` : '')
299
- + (chairIssues.length ? `- chair contract errors: ${chairIssues.join('; ')}\n` : '')
300
- + 'Output ONLY corrected JSON.';
337
+ + (chairIssues.length ? `- chair contract errors: ${chairIssues.join('; ')}\n` : '');
338
+ const repair = buildChairRepairPrompt(basePrompt, corrections);
301
339
  try {
302
340
  report = translateChairReport(await jsonCall(ctx, judge, 'S9-repair', repair, IdeaChairReportModel, {
303
341
  repair: ctx.budget.limit - ctx.budget.used > 2,