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
@@ -3,37 +3,26 @@
3
3
  // sensitivity, coverage, contribution, and confidence are DERIVED here rather than invented by the judge.
4
4
  // A truly missing required field is a template bug (fail loudly); degraded-but-valid
5
5
  // states (S8 skipped, items UNVERIFIED, empty consensus) render normally. User-facing → DISPLAY_NAME.
6
+ import { ReaderBrief as ReaderBriefSchema, readerBriefIssues } from '../../schemas/index.js';
6
7
  import { DISPLAY_NAME } from '../../providers/types.js';
7
8
  import { overlap, tokenize } from '../cluster.js';
8
- import { renderDecisionDossierMarkdown } from '../decision-dossier.js';
9
+ import { interpretClaimOutcome } from '../decision-graph.js';
10
+ import { evidenceOrigin } from '../evidence-origin.js';
11
+ import { buildReaderProjection, formatTokenLine, renderDecisionDossierMarkdown, sanitizeReaderText } from '../decision-dossier.js';
9
12
  import { callCategory } from '../modes.js';
10
13
  /** Pure graph-backed decision audit. */
11
14
  export function deriveAudit(graph, judgeReport) {
12
- const ruling = new Map(judgeReport.adjudications.map((a) => [a.id, a.ruling]));
15
+ const ruling = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
13
16
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
14
17
  return graph.claims.map((claim) => {
15
18
  const positions = claim.position_ids.map((id) => positionById.get(id));
16
19
  const providers = [...new Set(positions.map((position) => position.provider))];
17
- let status;
18
- let confidence;
19
- if (claim.state === 'UNCERTAIN' || claim.evidence_state !== 'SUPPORTED') {
20
- [status, confidence] = ['unverified', 'LOW'];
21
- }
22
- else if (claim.state === 'SHARED_CONCERN' || (claim.state === 'UNIQUE' && positions[0]?.stance === 'OPPOSE')) {
23
- [status, confidence] = ['failed', providers.length >= 2 ? 'HIGH' : 'MEDIUM'];
24
- }
25
- else if (claim.state === 'DISAGREEMENT') {
26
- const result = ruling.get(claim.id);
27
- if (result === 'UPHOLD')
28
- [status, confidence] = ['failed', 'MEDIUM'];
29
- else if (result === 'REJECT')
30
- [status, confidence] = ['held', 'MEDIUM'];
31
- else
32
- [status, confidence] = ['unverified', 'LOW'];
33
- }
34
- else {
35
- [status, confidence] = ['held', providers.length >= 2 ? 'HIGH' : 'MEDIUM'];
36
- }
20
+ const outcome = interpretClaimOutcome(graph, claim, ruling.get(claim.id));
21
+ const status = outcome.decisionEffect === 'HELD' ? 'held'
22
+ : outcome.decisionEffect === 'FAILED' ? 'failed' : 'unverified';
23
+ const confidence = status === 'unverified' ? 'LOW'
24
+ : claim.state === 'DISAGREEMENT' ? 'MEDIUM'
25
+ : providers.length >= 2 ? 'HIGH' : 'MEDIUM';
37
26
  return { id: claim.id, statement: claim.proposition, providers, status, confidence };
38
27
  });
39
28
  }
@@ -96,12 +85,16 @@ const DEGRADATION_FLAGS = ['low_diversity', 'synthesis_suspect', 'plan_fallback'
96
85
  * self-confidence never enters it, and consensus alone can never reach the High band. */
97
86
  export function computeConfidence(graph, flags) {
98
87
  const loadBearing = graph.claims.filter((claim) => claim.load_bearing);
99
- const verificationCoverage = loadBearing.length
100
- ? loadBearing.filter((claim) => claim.evidence_state === 'SUPPORTED').length / loadBearing.length : 0;
88
+ const factual = loadBearing.filter((claim) => claim.nature === 'FACTUAL');
89
+ const verificationClaims = factual.length ? factual : loadBearing;
90
+ const verificationCoverage = verificationClaims.length
91
+ ? verificationClaims.filter((claim) => claim.evidence_state === 'SUPPORTED').length / verificationClaims.length : 0;
101
92
  const independentConvergence = graph.claims.length
102
93
  ? graph.claims.filter((claim) => claim.state === 'CONSENSUS' || claim.state === 'SHARED_CONCERN').length / graph.claims.length : 0;
94
+ // v6: only independent EXTERNAL evidence counts as quality — the user's own material restated
95
+ // as cards inflated f740's coverage to theater (8 of 12 cards were the user's idea-brief).
103
96
  const evidenceQuality = graph.evidence.length
104
- ? graph.evidence.filter((card) => card.source_kind !== 'MODEL_KNOWLEDGE').length / graph.evidence.length : 0;
97
+ ? graph.evidence.filter((card) => evidenceOrigin(card) === 'EXTERNAL').length / graph.evidence.length : 0;
105
98
  const stability = Math.max(0, 1 - 0.25 * DEGRADATION_FLAGS.filter((flag) => flags.has(flag)).length);
106
99
  const criticalRiskPenalty = Math.min(20, 5 * loadBearing.filter((claim) => claim.if_false === 'STOP' && claim.evidence_state !== 'SUPPORTED').length);
107
100
  let score = Math.round(verificationCoverage * 40 + independentConvergence * 25 + evidenceQuality * 20 + stability * 15 - criticalRiskPenalty);
@@ -109,22 +102,93 @@ export function computeConfidence(graph, flags) {
109
102
  score = Math.min(score, 79); // consensus alone never yields High
110
103
  score = Math.max(0, Math.min(100, score));
111
104
  const label = score >= 80 ? 'High' : score >= 60 ? 'Medium' : 'Low';
112
- return { score, label, verificationCoverage, independentConvergence, evidenceQuality, stability, criticalRiskPenalty };
105
+ return {
106
+ score,
107
+ label,
108
+ verificationCoverage,
109
+ ...(factual.length ? { verificationScope: 'FACTUAL' } : {}),
110
+ independentConvergence,
111
+ evidenceQuality,
112
+ stability,
113
+ criticalRiskPenalty,
114
+ };
113
115
  }
114
116
  const STANCE_MAP = { SUPPORT: 'AGREE', OPPOSE: 'DISAGREE', MIXED: 'CONDITIONAL', UNKNOWN: 'UNKNOWN' };
115
117
  const VERIFICATION_MAP = { SUPPORTED: 'VERIFIED', CONFLICTED: 'PARTIAL', UNVERIFIED: 'UNVERIFIED' };
116
118
  const SEVERITY_MAP = { DECISIVE: 'High', MATERIAL: 'Medium', LOW: 'Low' };
117
- function claimRuling(claim, adjudication) {
118
- // UPHOLD/REJECT inverts ONLY on a disagreement (the ruling is on the objection); for every other
119
- // state the ruling is evidence-based, matching deriveAudit's semantics.
120
- if (claim.state === 'DISAGREEMENT') {
121
- if (!adjudication || adjudication.ruling === 'UNRESOLVED')
122
- return 'UNRESOLVED';
123
- return adjudication.ruling === 'UPHOLD' ? 'REJECTED' : 'ACCEPTED';
124
- }
125
- if (claim.state === 'UNCERTAIN')
126
- return 'UNRESOLVED';
127
- return claim.evidence_state === 'SUPPORTED' ? 'ACCEPTED' : 'CONDITIONAL';
119
+ function claimRuling(graph, claim, adjudication) {
120
+ const truth = interpretClaimOutcome(graph, claim, adjudication).propositionTruth;
121
+ return truth === 'HOLDS' ? 'ACCEPTED' : truth === 'FAILS' ? 'REJECTED' : 'UNRESOLVED';
122
+ }
123
+ function fallbackReaderBrief(args) {
124
+ const labelFor = (id) => args.graph.claims.find((claim) => claim.id === id)?.proposition ?? null;
125
+ const clean = (value, max) => {
126
+ const text = sanitizeReaderText(value, labelFor);
127
+ if (text.length <= max)
128
+ return text;
129
+ const clipped = text.slice(0, max - 1);
130
+ const boundary = clipped.lastIndexOf(' ');
131
+ return `${clipped.slice(0, boundary > max * 0.6 ? boundary : max - 1).trimEnd()}…`;
132
+ };
133
+ const keyPoints = args.judgeReport.key_points ?? [];
134
+ const missing = args.missingRequestedOutputs.length
135
+ ? `Requested deliverables unavailable: ${args.missingRequestedOutputs.join(', ')}.`
136
+ : 'No additional requested deliverable was synthesized.';
137
+ const firstAction = args.actionPlan && !('kind' in args.actionPlan) ? args.actionPlan.actions[0]?.action : undefined;
138
+ const nextStep = firstAction
139
+ ?? args.openQuestions[0]
140
+ ?? args.judgeReport.strongest_counter_case?.reasoning
141
+ ?? 'Review the unresolved decision evidence before committing.';
142
+ const adjudicationById = new Map(args.judgeReport.adjudications.map((item) => [item.id, item]));
143
+ const acceptedClaims = new Set(args.graph.claims
144
+ .filter((claim) => claim.evidence_state === 'SUPPORTED'
145
+ && interpretClaimOutcome(args.graph, claim, adjudicationById.get(claim.id)).propositionTruth === 'HOLDS')
146
+ .map((claim) => claim.id));
147
+ const sourceIds = args.graph.evidence
148
+ .filter((evidence) => {
149
+ if (evidence.source_kind !== 'PRIMARY' && evidence.source_kind !== 'SECONDARY')
150
+ return false;
151
+ try {
152
+ const url = new URL(evidence.url ?? '');
153
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
154
+ return false;
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ return args.graph.edges.some((edge) => edge.from === evidence.id && edge.type === 'SUPPORTS' && acceptedClaims.has(edge.to));
160
+ })
161
+ .map((evidence) => evidence.id)
162
+ .slice(0, 8);
163
+ const raw = {
164
+ headline: clean(args.judgeReport.verdict, 160),
165
+ bottom_line: clean(args.judgeReport.verdict, 1200),
166
+ sections: [
167
+ {
168
+ heading: 'Why',
169
+ summary: clean(keyPoints[0] ?? args.judgeReport.verdict, 1000),
170
+ bullets: [...keyPoints.slice(1, 3), ...(args.judgeReport.conditions ?? []).slice(0, 2)].map((item) => clean(item, 500)),
171
+ },
172
+ {
173
+ heading: 'What remains',
174
+ summary: missing,
175
+ bullets: [args.openQuestions[0], args.judgeReport.strongest_counter_case?.reasoning]
176
+ .filter((item) => Boolean(item)).map((item) => clean(item, 500)),
177
+ },
178
+ ],
179
+ next_step: clean(nextStep, 600),
180
+ caveats: [
181
+ ...(args.missingRequestedOutputs.length ? [missing] : []),
182
+ ...(args.flags.has('plan_skipped') || args.flags.has('plan_fallback')
183
+ ? ['The requested synthesis was unavailable; this concise summary uses the recorded chair decision only.'] : []),
184
+ ],
185
+ source_ids: sourceIds,
186
+ };
187
+ const brief = ReaderBriefSchema.parse(raw);
188
+ const issues = readerBriefIssues(brief, args.graph.claims.map((claim) => claim.id));
189
+ if (issues.length)
190
+ throw new Error(`fallback reader brief leaked graph ids: ${issues.join(', ')}`);
191
+ return brief;
128
192
  }
129
193
  function verificationStatusByClaim(verifications) {
130
194
  const statuses = new Map();
@@ -154,6 +218,18 @@ function buildDossier(args) {
154
218
  }
155
219
  const recommendationIds = (judgeReport.recommendation_claim_ids ?? [])
156
220
  .filter((id) => claimById.has(id));
221
+ const recommendationPositionIds = new Set(recommendationIds.flatMap((id) => claimById.get(id)?.position_ids ?? []));
222
+ const seatStats = [...graph.positions.reduce((stats, position) => {
223
+ const provider = position.id.split('/')[0].replace(/-coverage-fill$/, '');
224
+ const entry = stats.get(provider) ?? { provider, positions: 0, evidenced: 0, survivedIntoDecision: 0 };
225
+ entry.positions += 1;
226
+ if (position.evidence_ids.length)
227
+ entry.evidenced += 1;
228
+ if (recommendationPositionIds.has(position.id))
229
+ entry.survivedIntoDecision += 1;
230
+ stats.set(provider, entry);
231
+ return stats;
232
+ }, new Map()).values()];
157
233
  const anchors = recommendationIds.length
158
234
  ? recommendationIds
159
235
  : graph.claims.filter((claim) => claim.load_bearing).slice(0, 8).map((claim) => claim.id);
@@ -191,6 +267,8 @@ function buildDossier(args) {
191
267
  id: item.id,
192
268
  source: item.locator ?? (item.source_kind === 'MODEL_KNOWLEDGE' ? `${disp(item.provider)} model knowledge` : item.source_kind),
193
269
  sourceKind: item.source_kind,
270
+ ...(item.title ? { title: item.title } : {}),
271
+ ...(item.url ? { url: item.url } : {}),
194
272
  date: item.accessed_at ?? 'Not recorded',
195
273
  freshness: item.freshness,
196
274
  verificationStatus,
@@ -264,7 +342,7 @@ function buildDossier(args) {
264
342
  linkedClaimIds: [...new Set(related)],
265
343
  };
266
344
  });
267
- const experiments = actionPlan && !('kind' in actionPlan)
345
+ const experiments = actionPlan && !('kind' in actionPlan) && actionPlan.actions.length > 0
268
346
  ? {
269
347
  status: 'AVAILABLE',
270
348
  note: actionPlan.sequencing_note,
@@ -279,9 +357,11 @@ function buildDossier(args) {
279
357
  }
280
358
  : {
281
359
  status: 'DEGRADED',
282
- note: actionPlan && 'kind' in actionPlan
283
- ? `Planner unavailable: ${actionPlan.reason}. Unresolved: ${actionPlan.unresolved_questions.join('; ')}`
284
- : 'No planner artifact was recorded.',
360
+ note: actionPlan && !('kind' in actionPlan)
361
+ ? 'The answer is available, but no validation action survived anchor checks.'
362
+ : actionPlan && 'kind' in actionPlan
363
+ ? `Planner unavailable: ${actionPlan.reason}. Unresolved: ${actionPlan.unresolved_questions.join('; ')}`
364
+ : 'No planner artifact was recorded.',
285
365
  actions: [],
286
366
  };
287
367
  const featureBacklog = actionPlan && !('kind' in actionPlan) ? actionPlan.feature_backlog : undefined;
@@ -290,6 +370,15 @@ function buildDossier(args) {
290
370
  ...(args.requestedOutputs.includes('FEATURE_BACKLOG') && !featureBacklog ? ['FEATURE_BACKLOG'] : []),
291
371
  ...(args.requestedOutputs.includes('IMPLEMENTATION_PLAN') && !implementationPlan ? ['IMPLEMENTATION_PLAN'] : []),
292
372
  ];
373
+ const persistedReaderBrief = actionPlan && !('kind' in actionPlan) ? actionPlan.reader_brief : undefined;
374
+ const readerBrief = persistedReaderBrief ?? (args.newDecisionContract ? fallbackReaderBrief({
375
+ graph,
376
+ judgeReport,
377
+ actionPlan,
378
+ openQuestions: mergeOpenQuestions(seats),
379
+ missingRequestedOutputs,
380
+ flags: args.flags,
381
+ }) : undefined);
293
382
  const counter = judgeReport.strongest_counter_case;
294
383
  const contributions = args.models.map((model) => ({
295
384
  provider: model.provider,
@@ -330,11 +419,13 @@ function buildDossier(args) {
330
419
  experiments,
331
420
  ...(featureBacklog ? { featureBacklog } : {}),
332
421
  ...(implementationPlan ? { implementationPlan } : {}),
422
+ ...(readerBrief ? { readerBrief } : {}),
333
423
  missingRequestedOutputs,
334
424
  counterCase: counter
335
425
  ? { available: true, reasoning: counter.reasoning, claimIds: counter.claim_ids.filter((id) => claimById.has(id)) }
336
426
  : { available: false, reasoning: 'No graph-anchored counter-case was recorded.', claimIds: [] },
337
427
  contributions,
428
+ seatStats,
338
429
  technical: {
339
430
  submissions: seats.map((seat) => ({
340
431
  provider: seat.provider,
@@ -352,7 +443,14 @@ function buildDossier(args) {
352
443
  export function buildDecisionReport(ctx, args) {
353
444
  const { contract, seats, graph, verifications, judgeReport, actionPlan } = args;
354
445
  const mode = ctx.mode ?? 'council';
446
+ const adaptiveAuto = ctx.isAuto === true;
447
+ const autoEscalationReasons = ctx.autoEscalationReasons ?? [];
355
448
  const flags = new Set(ctx.flags);
449
+ const newDecisionContract = 'success_bar' in contract;
450
+ const hasReaderBrief = Boolean(actionPlan && !('kind' in actionPlan) && actionPlan.reader_brief);
451
+ if (newDecisionContract && !hasReaderBrief && !flags.has('plan_skipped') && !flags.has('plan_fallback')) {
452
+ flags.add('plan_fallback');
453
+ }
356
454
  const confidence = computeConfidence(graph, flags);
357
455
  const status = statusFrom(judgeReport);
358
456
  const rulingById = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
@@ -372,7 +470,7 @@ export function buildDecisionReport(ctx, args) {
372
470
  text: claim.proposition,
373
471
  stances,
374
472
  verification: VERIFICATION_MAP[claim.evidence_state],
375
- ruling: claimRuling(claim, rulingById.get(claim.id)),
473
+ ruling: claimRuling(graph, claim, rulingById.get(claim.id)),
376
474
  loadBearing: claim.load_bearing,
377
475
  sensitivity: claim.sensitivity,
378
476
  };
@@ -401,12 +499,9 @@ export function buildDecisionReport(ctx, args) {
401
499
  .filter((position) => position.stance === 'OPPOSE')
402
500
  .map((position) => ({ provider: disp(position.provider), proposition: position.proposition }));
403
501
  const unresolvedDecisive = disagreements.some((d) => d.status === 'UNRESOLVED' && claimById.get(d.id)?.sensitivity === 'DECISIVE');
404
- const consensusType = mode === 'quick' ? 'single_analyst' : disagreements.length === 0
502
+ const consensusType = adaptiveAuto || mode === 'quick' ? 'single_analyst' : disagreements.length === 0
405
503
  ? (claims.some((claim) => claim.ruling === 'UNRESOLVED') ? 'convergent_with_unresolved_claims' : 'unanimous')
406
504
  : disagreements.every((d) => d.status === 'RESOLVED') ? 'majority_with_dissent' : 'contested';
407
- const risks = graph.claims
408
- .filter((claim) => claim.load_bearing && claim.evidence_state !== 'SUPPORTED')
409
- .map((claim) => ({ risk: claim.proposition, severity: SEVERITY_MAP[claim.sensitivity] ?? 'Medium' }));
410
505
  // Frame the slot as what it is — a decisive assumption that is NOT proven. Echoing the bare
411
506
  // proposition inverts the meaning when the claim is phrased affirmatively (run 20260714-2321:
412
507
  // "a cutover can preserve compliance" read as reassurance, the opposite of the verdict).
@@ -414,6 +509,18 @@ export function buildDecisionReport(ctx, args) {
414
509
  const criticalWarning = criticalClaim
415
510
  ? `Unverified decisive assumption (if false, STOP): ${criticalClaim.proposition} — evidence ${criticalClaim.evidence_state}.`
416
511
  : null;
512
+ // Same framing problem as the critical warning above: an unsettled load-bearing claim rendered as
513
+ // its bare (often affirmatively-phrased) proposition reads as an endorsement, not a risk. Frame
514
+ // each explicitly, sort worst-first, and skip the claim already surfaced as the critical warning
515
+ // so it isn't double-counted.
516
+ const severityRank = { High: 0, Medium: 1, Low: 2 };
517
+ const risks = graph.claims
518
+ .filter((claim) => claim.load_bearing && claim.evidence_state !== 'SUPPORTED' && claim.id !== criticalClaim?.id)
519
+ .map((claim) => ({
520
+ risk: `Rests on unsettled claim: ${claim.proposition} (evidence ${claim.evidence_state}).`,
521
+ severity: SEVERITY_MAP[claim.sensitivity] ?? 'Medium',
522
+ }))
523
+ .sort((a, b) => severityRank[a.severity] - severityRank[b.severity]);
417
524
  const verificationResults = verifications.verifications.map((v) => {
418
525
  if ('claim_id' in v) {
419
526
  return {
@@ -436,16 +543,29 @@ export function buildDecisionReport(ctx, args) {
436
543
  });
437
544
  const byProvider = {};
438
545
  let modelTimeMs = 0;
546
+ let inputTokens = 0, outputTokens = 0, estimatedCalls = 0;
439
547
  for (const call of ctx.calls) {
440
548
  byProvider[call.provider] = (byProvider[call.provider] ?? 0) + 1;
441
549
  modelTimeMs += call.durationMs;
550
+ if (call.usage) {
551
+ inputTokens += call.usage.inputTokens ?? 0;
552
+ outputTokens += call.usage.outputTokens ?? 0;
553
+ if (call.usage.estimated)
554
+ estimatedCalls++;
555
+ }
442
556
  }
443
557
  const roles = ctx.roles;
444
- const reportProviders = mode === 'quick' ? [...new Set(seats.map((seat) => seat.provider))] : ctx.available();
558
+ const reportProviders = adaptiveAuto
559
+ ? [...new Set([...seats.map((seat) => seat.provider), ...ctx.calls.map((call) => call.provider)])]
560
+ : mode === 'quick' ? [...new Set(seats.map((seat) => seat.provider))] : ctx.available();
445
561
  const models = reportProviders.map((provider) => ({
446
562
  provider,
447
563
  name: disp(provider),
448
- roles: mode === 'quick' ? ['single analyst'] : [
564
+ roles: adaptiveAuto ? [
565
+ ...(seats.some((seat) => seat.provider === provider) ? ['primary analyst'] : []),
566
+ ...(ctx.calls.some((call) => call.provider === provider && call.stage.startsWith('P0-')) ? ['preflight reader'] : []),
567
+ ...(ctx.calls.some((call) => call.provider === provider && call.stage === 'S4b') ? ['delta challenger'] : []),
568
+ ] : mode === 'quick' ? ['single analyst'] : [
449
569
  ...(roles.analyst === provider ? ['analyst'] : []),
450
570
  ...(roles.judge === provider ? ['judge'] : []),
451
571
  ...(roles.verifier === provider ? ['verifier'] : []),
@@ -479,6 +599,8 @@ export function buildDecisionReport(ctx, args) {
479
599
  rebuttals: args.rebuttals,
480
600
  rubric: args.rubric ?? [],
481
601
  requestedOutputs,
602
+ newDecisionContract,
603
+ flags,
482
604
  });
483
605
  const decisionSnapshot = judgeReport.decision_snapshot ? {
484
606
  decisiveNumbers: judgeReport.decision_snapshot.decisive_numbers.map((item) => ({
@@ -515,6 +637,10 @@ export function buildDecisionReport(ctx, args) {
515
637
  reportId: ctx.runId,
516
638
  generatedAt: new Date().toISOString(),
517
639
  mode,
640
+ ...(ctx.fastPath ? { fastPath: true } : {}),
641
+ ...(adaptiveAuto ? { adaptiveAuto: true } : {}),
642
+ ...(autoEscalationReasons.length ? { autoEscalationReasons } : {}),
643
+ ...(adaptiveAuto && ctx.attemptedStages?.includes('S4b') ? { autoChallengeUsed: true } : {}),
518
644
  task: {
519
645
  original: args.original ?? contract.task,
520
646
  normalized: contract.task,
@@ -564,55 +690,41 @@ export function buildDecisionReport(ctx, args) {
564
690
  ? actionPlan.actions.map((action) => ({ order: action.order, action: action.action, why: action.why, effort: action.effort, killSignal: action.kill_signal }))
565
691
  : [],
566
692
  openQuestions,
567
- flags: [...ctx.flags],
568
- receipt: { calls: ctx.calls.length, budget: ctx.budget.limit, byProvider, modelTimeMs, categories: receiptCategories(ctx) },
693
+ flags: [...flags],
694
+ receipt: { calls: ctx.calls.length, budget: ctx.budget.limit, byProvider, modelTimeMs, categories: receiptCategories(ctx), tokens: { inputTokens, outputTokens, estimatedCalls } },
569
695
  dossier,
570
696
  };
571
697
  }
572
- const pct = (n) => `${Math.round(n * 100)}%`;
573
698
  export { renderDecisionDossierMarkdown };
574
699
  export function renderReport(ctx, args) {
575
700
  return renderDecisionDossierMarkdown(buildDecisionReport(ctx, args));
576
701
  }
577
702
  // ── Terminal summary (level 1) ───────────────────────────────────────────────
578
- const RULE = '─'.repeat(52);
703
+ const RULE = '─'.repeat(48);
704
+ function terminalLine(value) {
705
+ return value.replace(/\s+/g, ' ').trim();
706
+ }
579
707
  export function renderTerminalSummary(report, paths) {
580
- const summary = report.consensusSummary;
581
- const c = report.confidenceBreakdown;
582
- const snapshot = report.decisionSnapshot;
583
- const decisiveLines = snapshot ? [
584
- 'Decision numbers:',
585
- ...snapshot.decisiveNumbers.slice(0, 3).map((item) => `${item.label}: ${item.value} ${item.meaning}`),
586
- ...(snapshot.payback ? [`Payback (${snapshot.payback.status.replaceAll('_', ' ').toLowerCase()}): ${snapshot.payback.result}`] : []),
587
- `Options: ${snapshot.options.map((option) => `${option.label} — ${option.commitment} (${option.commitmentKind.replace('_', ' ').toLowerCase()})`).join(' · ')}`,
588
- ] : ['Decisive result:', report.verdict.primaryReason];
589
- const checks = [
590
- `[${c.verificationCoverage >= 0.5 ? 'PASS' : 'WARN'}] Verification coverage ${pct(c.verificationCoverage)} of load-bearing claims`,
591
- `[PASS] Minority opinion preserved (${report.minority.dissent.length} dissent item(s))`,
592
- `[${report.verification.refuted === 0 ? 'PASS' : 'WARN'}] Verifier refutations: ${report.verification.refuted}`,
593
- ...(report.flags.length ? [`[WARN] Degradation flags: ${report.flags.join(', ')}`] : []),
594
- `[WARN] Structural score ${c.score}/100 is heuristic — not yet benchmark-calibrated`,
595
- ];
708
+ const projection = report.dossier.readerBrief ? buildReaderProjection(report) : undefined;
709
+ const candidates = projection
710
+ ? [...projection.sections.flatMap((section) => [section.summary, ...section.bullets]), ...projection.caveats, projection.nextStep]
711
+ : [...(report.keyFindings ?? []), ...report.verdict.conditions, report.verdict.primaryReason];
712
+ const takeaways = [...new Set(candidates.map(terminalLine).filter(Boolean))].slice(0, 3);
713
+ const nextStep = projection?.nextStep ?? report.recommendedActions[0]?.action ?? 'Open the report and choose the smallest decisive next action.';
596
714
  return [
597
- report.mode === 'quick' ? 'SINGLE-MODEL DECISION REPORT' : 'MULTI-MODEL DECISION REPORT',
715
+ report.adaptiveAuto ? 'AIKI · ADAPTIVE DECISION'
716
+ : report.mode === 'quick' ? 'AIKI · SINGLE-MODEL DECISION' : 'AIKI · COUNCIL DECISION',
598
717
  RULE,
599
- `Verdict: ${report.verdict.summary}`,
600
- `Decision state: ${report.verdict.status}`,
601
- `Evidence coverage: ${pct(c.verificationCoverage)} of load-bearing claims independently verified`,
602
- 'Coverage note: unchecked inputs remain; this is not a probability of correctness.',
603
- `${report.mode === 'quick' ? 'Claims' : 'Consensus'}: ${report.verdict.consensusType.replaceAll('_', ' ')} — ${summary.accepted} accepted · ${summary.rejected} rejected · ${summary.unresolved} unresolved`,
604
- '',
605
- ...decisiveLines,
718
+ `Verdict: ${terminalLine(projection?.headline ?? report.verdict.summary)}`,
719
+ terminalLine(projection?.bottomLine ?? report.verdict.primaryReason),
720
+ ...(projection?.warnings[0] ? [`Warning: ${projection.warnings[0].message}`] : []),
606
721
  '',
607
- ...(report.verdict.criticalWarning ? ['Critical warning:', report.verdict.criticalWarning, ''] : []),
608
- ...(report.minority.dissent[0] ? ['Critical dissent:', report.minority.dissent[0], ''] : []),
609
- 'Verification:',
610
- ...checks,
722
+ 'Key takeaways:',
723
+ ...takeaways.map((item) => `- ${item}`),
611
724
  '',
612
- ...(report.recommendedActions[0] ? ['Recommended action:', report.recommendedActions[0].action, ''] : []),
613
- ...(snapshot?.tripwire ? ['Go/no-go tripwire:', `${snapshot.tripwire.metric}: ${snapshot.tripwire.threshold} — ${snapshot.tripwire.decisionRule}`, ''] : []),
614
- `Full report: ${paths.markdownPath}`,
615
- `Audit JSON: ${paths.jsonPath}`,
725
+ `Next step: ${terminalLine(nextStep)}`,
726
+ ...(report.receipt.tokens ? [`Tokens: ${formatTokenLine(report.receipt.tokens)}`] : []),
727
+ `Report: ${paths.markdownPath}`,
616
728
  ].join('\n');
617
729
  }
618
730
  export async function s10Render(ctx, args) {
@@ -16,15 +16,23 @@ import { IDEA_LANES } from '../idea-lanes.js';
16
16
  /** Run one analyst seat → validated `RoleOutput`, persisted to 04-role-outputs/<label>.json.
17
17
  * `label` doubles as the artifact filename, so a resample uses a distinct label. */
18
18
  async function runSeat(ctx, seat, label, lane, prompt) {
19
- const model = await jsonCall(ctx, ctx.handle(seat), `S4-${label}`, prompt, IdeaRoleOutputModel, { salvage: salvageIdeaRoleOutputModel });
19
+ // v6 tail guard: an S4 repair may spend only when the worst case (both seats repairing) still
20
+ // leaves the reserved tail — 2 seat calls + 2 repairs + 3 (chair, planner, one tail repair).
21
+ // When disallowed, jsonCall's salvage floor keeps the seat alive (proven on the f740 payloads).
22
+ const model = await jsonCall(ctx, ctx.handle(seat), `S4-${label}`, prompt, IdeaRoleOutputModel, {
23
+ salvage: salvageIdeaRoleOutputModel,
24
+ repair: ctx.budget.limit - ctx.budget.used >= 7,
25
+ });
20
26
  const output = { workflow: 'idea-refinement', ...model };
21
27
  await ctx.writer.writeRoleOutput(label, output);
22
28
  return { provider: seat, sample: label, lane, output };
23
29
  }
24
- export async function s4Analyze(ctx, prompts) {
30
+ export async function s4Analyze(ctx, prompts, needsSourceFallback = false) {
25
31
  const seats = ctx.roles.s4;
26
32
  const settled = await Promise.allSettled(seats.map((seat, index) => {
27
33
  const lane = IDEA_LANES[index % IDEA_LANES.length];
34
+ if (needsSourceFallback && ctx.mode !== 'quick' && seat === 'codex')
35
+ ctx.addFlag('source_fallback_search');
28
36
  return runSeat(ctx, seat, seat, lane, prompts[lane]);
29
37
  }));
30
38
  const survivors = [];
@@ -0,0 +1,97 @@
1
+ // v7 Phase D — one auto-only delta challenge over hard-gated claim packets.
2
+ import { ChallengeDeltaSet as ChallengeDeltaSetSchema, } from '../../schemas/index.js';
3
+ import { canProduceNewInformation, } from '../auto-profile.js';
4
+ import { isFatal, StageError } from '../context.js';
5
+ import { jsonCall } from '../jsonStage.js';
6
+ import { buildClaimPackets } from './s8-verify.js';
7
+ const CHALLENGE_PROMPT = `ROLE: Independent delta challenger. You see only the targeted anonymous claim
8
+ packets below, not the full task. Confirm, counter, narrow, replace, or leave each claim unresolved.
9
+
10
+ Output ONLY JSON:
11
+ {"deltas":[{"claimId":"<G-id>","response":"CONFIRM|COUNTER|NARROW|REPLACE|UNRESOLVED",
12
+ "reasoning":"<precise reason>","newEvidenceIds":["<E-id>"],
13
+ "changedDecisionImpact":"<what changes, or why the decision stays stable>"}]}
14
+
15
+ Return exactly one delta per packet. Cite only evidence IDs shown in that packet. Do not invent a
16
+ source, URL, claim ID, or evidence ID. JSON only.
17
+ TARGET CLAIM PACKETS: {{PACKETS_JSON}}`;
18
+ export function buildChallengePrompt(packets) {
19
+ return CHALLENGE_PROMPT.replace('{{PACKETS_JSON}}', JSON.stringify(packets, null, 2));
20
+ }
21
+ function validateAndTranslate(result, targetIds, evidenceRefs, allowedEvidence) {
22
+ const expected = new Set(targetIds);
23
+ const seen = new Set();
24
+ const issues = [];
25
+ for (const delta of result.deltas) {
26
+ if (!expected.has(delta.claimId))
27
+ issues.push(`unknown claim id: ${delta.claimId}`);
28
+ if (seen.has(delta.claimId))
29
+ issues.push(`duplicate claim delta: ${delta.claimId}`);
30
+ seen.add(delta.claimId);
31
+ const allowed = allowedEvidence.get(delta.claimId) ?? new Set();
32
+ for (const id of delta.newEvidenceIds)
33
+ if (!allowed.has(id))
34
+ issues.push(`invalid evidence id for ${delta.claimId}: ${id}`);
35
+ }
36
+ for (const id of expected)
37
+ if (!seen.has(id))
38
+ issues.push(`missing claim delta: ${id}`);
39
+ if (issues.length)
40
+ throw new StageError('S4b', 'BAD_OUTPUT', issues.join('; '));
41
+ return {
42
+ deltas: result.deltas.map((delta) => ({
43
+ ...delta,
44
+ newEvidenceIds: delta.newEvidenceIds.map((id) => evidenceRefs.get(id)),
45
+ })),
46
+ };
47
+ }
48
+ function challengerProvider(ctx, primaryProvider) {
49
+ return [...new Set([ctx.roles.verifier, ...ctx.roles.s4, ...ctx.available()])]
50
+ .find((provider) => provider !== primaryProvider && ctx.available().includes(provider));
51
+ }
52
+ /** At most one provider call; no repair call and no work for an information-free target. */
53
+ export async function s4bChallenge(ctx, graph, gates, primaryProvider) {
54
+ const byClaim = new Map(gates.map((gate) => [gate.claimId, gate]));
55
+ const targets = [...byClaim.values()]
56
+ .filter((gate) => graph.claims.some((claim) => claim.id === gate.claimId))
57
+ .filter((gate) => canProduceNewInformation(graph, gate.claimId))
58
+ .slice(0, 3);
59
+ const provider = challengerProvider(ctx, primaryProvider);
60
+ if (!targets.length || !provider)
61
+ return { deltas: [] };
62
+ const packets = buildClaimPackets(graph, targets.map((gate) => ({
63
+ claim_id: gate.claimId,
64
+ kind: gate.kind,
65
+ })));
66
+ try {
67
+ const result = await jsonCall(ctx, ctx.handle(provider), 'S4b', buildChallengePrompt(packets.packets), ChallengeDeltaSetSchema, { repair: false });
68
+ const translated = validateAndTranslate(result, targets.map((gate) => gate.claimId), packets.evidenceRefs, packets.allowedEvidence);
69
+ await ctx.writer.writeJson('challenge-deltas', translated);
70
+ return translated;
71
+ }
72
+ catch (error) {
73
+ if (isFatal(error))
74
+ throw error;
75
+ const empty = { deltas: [] };
76
+ await ctx.writer.writeJson('challenge-deltas', empty);
77
+ return empty;
78
+ }
79
+ }
80
+ /** Derived graph overlay; the stored S7 artifact and every non-target claim stay untouched. */
81
+ export function overlayChallengeDeltas(graph, deltas) {
82
+ if (!deltas.length)
83
+ return graph;
84
+ const stateById = new Map(deltas.map((delta) => [delta.claimId,
85
+ delta.response === 'CONFIRM' ? 'CONSENSUS'
86
+ : delta.response === 'COUNTER' ? 'DISAGREEMENT'
87
+ : 'UNCERTAIN']));
88
+ return {
89
+ ...graph,
90
+ claims: graph.claims.map((claim) => {
91
+ const state = stateById.get(claim.id);
92
+ if (!state || (state === 'CONSENSUS' && claim.state === 'DISAGREEMENT'))
93
+ return claim;
94
+ return { ...claim, state };
95
+ }),
96
+ };
97
+ }
@@ -2,17 +2,27 @@
2
2
  // contract's task? Two code checks (no model call — the §601/§532 T6 "deterministic core"; the §9
3
3
  // "verifier spot-check" is deferred): (1) the output produced ≥1 position (an analyst that
4
4
  // produced none did not engage the task), and (2) its `task_echo` is similar enough to the contract
5
- // task (overlap coefficient ≥ DRIFT_MIN_SIMILARITY). Drifted outputs are excluded from everything
5
+ // task + constraints + success_criteria (overlap coefficient ≥ DRIFT_MIN_SIMILARITY). Drifted
6
+ // outputs are excluded from everything
6
7
  // downstream and logged; if exclusion drops the survivor count below quorum (2), the run aborts.
7
8
  import { StageError } from '../context.js';
8
9
  import { overlapCoefficient, tokenize } from '../cluster.js';
9
- /** `task_echo` must share ≥ this fraction of its tokens with the contract task to count as on-task.
10
+ /** `task_echo` must share ≥ this fraction of its tokens with the contract to count as on-task.
10
11
  * Overlap coefficient (not Jaccard) so a short echo is not penalized against a longer contract
11
12
  * paragraph. Lenient by design — this catches an analyst that wandered off-task, not paraphrase
12
13
  * distance. Tunable (same footnote class as the S2 clustering threshold). */
13
14
  export const DRIFT_MIN_SIMILARITY = 0.3;
15
+ /** The reference token set is the contract the seat was told to address — task + constraints +
16
+ * success_criteria — NOT just the one-sentence task. Run 626e (2026-07-18, REAL QUORUM kill):
17
+ * the prompt says "restate the task" and both seats paraphrased accurately, but against the
18
+ * 17-token model-authored task sentence they scored 0.294/0.250 < 0.3 and the run died with 18
19
+ * good positions on disk. Against the full contract the same echoes score 0.680/0.500 while a
20
+ * genuinely off-task echo stays ≤0.08 — paraphrase passes, drift still fails, threshold untouched. */
21
+ function contractTokens(contract) {
22
+ return tokenize([contract.task, ...contract.constraints, ...contract.success_criteria].join(' '));
23
+ }
14
24
  export async function s5Drift(ctx, contract, seats, minSeats = 2) {
15
- const taskTokens = tokenize(contract.task);
25
+ const taskTokens = contractTokens(contract);
16
26
  const entries = [];
17
27
  const kept = [];
18
28
  const excluded = [];
@@ -1,5 +1,23 @@
1
1
  // S6 — preserve validated analyst submissions for graph compilation. No lexical merging: positions
2
2
  // keep their original text and are grouped only by stable references in S7.
3
+ /** A surviving scout seat with little output or evidence is weak. ponytail: thresholds are blunt on
4
+ * purpose; raise them only with benchmark evidence. */
5
+ export function detectWeakSeat(positions, mode) {
6
+ if (mode === 'quick')
7
+ return [];
8
+ const seats = new Map();
9
+ for (const position of positions) {
10
+ const provider = position.id.split('/')[0].replace(/-coverage-fill$/, '');
11
+ const seat = seats.get(provider) ?? { positions: 0, evidenced: 0 };
12
+ seat.positions += 1;
13
+ if (position.evidence_ids?.length)
14
+ seat.evidenced += 1;
15
+ seats.set(provider, seat);
16
+ }
17
+ if (seats.size < 2)
18
+ return [];
19
+ return [...seats].filter(([, seat]) => seat.positions < 3 || seat.evidenced / seat.positions < 0.5).map(([provider]) => provider);
20
+ }
3
21
  export function collectPositions(seats) {
4
22
  return seats.map((seat) => ({ provider: seat.provider, source_id: seat.sample ?? seat.provider, submission: seat.output }));
5
23
  }
@@ -4,6 +4,7 @@ import { compileDecisionGraph, coverageHoleQueue, positionId } from '../decision
4
4
  import { IdeaRoleOutputModel } from '../../schemas/index.js';
5
5
  import { isFatal } from '../context.js';
6
6
  import { jsonCall } from '../jsonStage.js';
7
+ import { detectWeakSeat } from './s6-positions.js';
7
8
  import { overlapCoefficient, tokenize } from '../cluster.js';
8
9
  const GROUP_SIMILARITY = 0.8;
9
10
  export function buildCoverageFillPrompt(task, holes) {
@@ -71,6 +72,8 @@ export async function s7DecisionGraph(ctx, submissions, rubric, task = '') {
71
72
  const completed = await fillCoverage(ctx, submissions, rubric, task);
72
73
  const groups = deterministicClaimGroups(completed);
73
74
  const graph = compileDecisionGraph(completed, rubric, groups);
75
+ if (detectWeakSeat(graph.positions, ctx.mode ?? 'council').length)
76
+ ctx.addFlag('weak_seat');
74
77
  await ctx.writer.writeJson('decision-graph', graph);
75
78
  return graph;
76
79
  }