aiki-cli 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +98 -7
  2. package/README.md +109 -30
  3. package/dist/bench/arms.js +10 -9
  4. package/dist/bench/harness.js +13 -10
  5. package/dist/bench/idea-lane-rotation.js +237 -0
  6. package/dist/bench/idea-v3-bench.js +506 -0
  7. package/dist/bench/idea-v3-rating.js +582 -0
  8. package/dist/bench/results.js +10 -3
  9. package/dist/bench/scoring/decision-insights.js +112 -0
  10. package/dist/bench/scoring/seeded-bugs.js +4 -0
  11. package/dist/cli/bench.js +180 -3
  12. package/dist/cli/doctor.js +56 -24
  13. package/dist/cli/index.js +31 -6
  14. package/dist/cli/resolve.js +7 -6
  15. package/dist/cli/resume.js +18 -0
  16. package/dist/cli/run.js +63 -8
  17. package/dist/council/view.js +378 -109
  18. package/dist/orchestration/calculations.js +97 -0
  19. package/dist/orchestration/context.js +37 -9
  20. package/dist/orchestration/decision-dossier.js +262 -0
  21. package/dist/orchestration/decision-graph.js +256 -0
  22. package/dist/orchestration/engine.js +5 -2
  23. package/dist/orchestration/evidence-pack.js +46 -0
  24. package/dist/orchestration/idea-lanes.js +20 -0
  25. package/dist/orchestration/jsonStage.js +72 -0
  26. package/dist/orchestration/legacy-idea-adapter.js +102 -0
  27. package/dist/orchestration/modes.js +33 -0
  28. package/dist/orchestration/preflight.js +183 -0
  29. package/dist/orchestration/quick-analysis.js +81 -0
  30. package/dist/orchestration/stages/cr-ladder.js +80 -0
  31. package/dist/orchestration/stages/s10-render.js +562 -150
  32. package/dist/orchestration/stages/s4-analyze.js +12 -7
  33. package/dist/orchestration/stages/s5-drift.js +9 -9
  34. package/dist/orchestration/stages/s6-positions.js +10 -0
  35. package/dist/orchestration/stages/s7-decision-graph.js +76 -0
  36. package/dist/orchestration/stages/s8-verify.js +153 -46
  37. package/dist/orchestration/stages/s8b-rebuttal.js +208 -0
  38. package/dist/orchestration/stages/s9-judge.js +329 -108
  39. package/dist/orchestration/stages/s9b-plan.js +85 -75
  40. package/dist/providers/codex.js +2 -1
  41. package/dist/providers/spawn.js +5 -0
  42. package/dist/schemas/index.js +572 -13
  43. package/dist/skills/idea-refinement/analyst.md +18 -14
  44. package/dist/skills/idea-refinement/economics-delivery.md +7 -0
  45. package/dist/skills/idea-refinement/market-adoption.md +7 -0
  46. package/dist/storage/runs.js +11 -4
  47. package/dist/tui/app.js +37 -13
  48. package/dist/tui/format.js +4 -5
  49. package/dist/tui/smart-entry.js +17 -1
  50. package/dist/tui/timeline.js +2 -4
  51. package/dist/workflows/code-review.js +4 -2
  52. package/dist/workflows/idea-refinement.js +110 -46
  53. package/package.json +12 -4
  54. package/dist/orchestration/stages/s0-grill.js +0 -79
  55. package/dist/orchestration/stages/s1-intent.js +0 -25
  56. package/dist/orchestration/stages/s2-misread.js +0 -76
  57. package/dist/orchestration/stages/s3-prompts.js +0 -55
  58. package/dist/orchestration/stages/s6-claims.js +0 -56
  59. package/dist/orchestration/stages/s7-disagreement.js +0 -134
@@ -17,6 +17,15 @@ export const ProviderIdSchema = z.enum(['claude', 'codex', 'agy']);
17
17
  export const TaskTypeSchema = z.enum(['idea-refinement', 'code-review', 'other']);
18
18
  /** The two runnable v1 workflows (§12). Discriminates RoleOutput and tags RunMeta. */
19
19
  export const WorkflowIdSchema = z.enum(['idea-refinement', 'code-review']);
20
+ /** Explicit user-selected idea protocol. There is deliberately no learned mode router. */
21
+ export const IdeaModeSchema = z.enum(['quick', 'council', 'research']);
22
+ export const DomainDimension = z
23
+ .object({
24
+ id: z.string().regex(/^D[1-5]$/),
25
+ label: z.string().min(1),
26
+ rationale: z.string().min(1),
27
+ })
28
+ .strict();
20
29
  // ── S1: IntentContract (§13) ────────────────────────────────────────────────
21
30
  export const IntentContract = z
22
31
  .object({
@@ -25,8 +34,21 @@ export const IntentContract = z
25
34
  constraints: z.array(z.string()), // explicit constraints the user stated (may be empty)
26
35
  unknowns: z.array(z.string()), // things the request leaves unspecified
27
36
  success_criteria: z.array(z.string()), // what a good final output must contain
37
+ domain_dimensions: z.array(DomainDimension).min(3).max(5).optional(), // required by the idea preflight; optional for old/code-review contracts
28
38
  })
29
39
  .strict();
40
+ /** R6 decision contract: the two-view preflight's single downstream boundary. */
41
+ export const DecisionContract = IntentContract.extend({
42
+ alternatives: z.array(z.string().min(1)).max(8),
43
+ success_bar: z.string().min(1),
44
+ evidence_supplied: z.array(z.string().min(1)).max(12),
45
+ missing_evidence: z.array(z.string().min(1)).max(12),
46
+ core_rubric: z.array(z.string().min(1)).min(1),
47
+ user_confirmed: z.boolean(),
48
+ confirmation: z.enum(['user-confirmed', 'headless-defaulted']),
49
+ }).strict();
50
+ /** Code review and old idea runs keep the smaller v1 contract. */
51
+ export const DecisionContractArtifact = z.union([DecisionContract, IntentContract]);
30
52
  // ── S2: Interpretation — per provider (§13) ─────────────────────────────────
31
53
  export const Interpretation = z
32
54
  .object({
@@ -65,6 +87,7 @@ const RunBriefDraftBase = z
65
87
  claims_to_test: z.array(z.string().min(1)).max(8),
66
88
  evidence_supplied: z.array(z.string().min(1)).max(8),
67
89
  missing_axes: z.array(z.string().min(1)).max(8),
90
+ domain_dimensions: z.array(DomainDimension).min(3).max(5),
68
91
  questions: z.array(RunBriefQuestion).min(3).max(4),
69
92
  })
70
93
  .strict();
@@ -77,7 +100,19 @@ function checkQuestionIds(questions, ctx) {
77
100
  seen.add(q.id);
78
101
  }
79
102
  }
80
- export const RunBriefDraft = RunBriefDraftBase.superRefine((brief, ctx) => checkQuestionIds(brief.questions, ctx));
103
+ function checkDomainDimensionIds(dimensions, ctx) {
104
+ const seen = new Set();
105
+ for (const dimension of dimensions) {
106
+ if (seen.has(dimension.id)) {
107
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['domain_dimensions'], message: `duplicate domain dimension id: ${dimension.id}` });
108
+ }
109
+ seen.add(dimension.id);
110
+ }
111
+ }
112
+ export const RunBriefDraft = RunBriefDraftBase.superRefine((brief, ctx) => {
113
+ checkQuestionIds(brief.questions, ctx);
114
+ checkDomainDimensionIds(brief.domain_dimensions, ctx);
115
+ });
81
116
  export const GrillAnswer = z
82
117
  .object({
83
118
  question_id: z.string().min(1),
@@ -89,6 +124,7 @@ export const RunBrief = RunBriefDraftBase.extend({
89
124
  answers: z.array(GrillAnswer).min(3).max(4),
90
125
  }).superRefine((brief, ctx) => {
91
126
  checkQuestionIds(brief.questions, ctx);
127
+ checkDomainDimensionIds(brief.domain_dimensions, ctx);
92
128
  const questionIds = new Set(brief.questions.map((q) => q.id));
93
129
  const answerIds = new Set();
94
130
  for (const answer of brief.answers) {
@@ -106,6 +142,34 @@ export const RunBrief = RunBriefDraftBase.extend({
106
142
  }
107
143
  }
108
144
  });
145
+ /** One of the two independent R6 preflight readings. */
146
+ export const PreflightReading = z.object({
147
+ subject: z.string().min(1),
148
+ interpretation: z.string().min(1),
149
+ normalized_decision: z.string().min(1),
150
+ alternatives: z.array(z.string().min(1)).max(8),
151
+ target_user: z.string().min(1).nullable(),
152
+ constraints: z.array(z.string().min(1)).max(10),
153
+ success_bar: z.string().min(1),
154
+ success_criteria: z.array(z.string().min(1)).max(8),
155
+ claims_to_test: z.array(z.string().min(1)).max(8),
156
+ evidence_supplied: z.array(z.string().min(1)).max(8),
157
+ missing_evidence: z.array(z.string().min(1)).max(8),
158
+ domain_dimensions: z.array(DomainDimension).min(3).max(5),
159
+ questions: z.array(RunBriefQuestion).min(3).max(4),
160
+ }).strict().superRefine((reading, ctx) => {
161
+ checkQuestionIds(reading.questions, ctx);
162
+ checkDomainDimensionIds(reading.domain_dimensions, ctx);
163
+ });
164
+ export const PreflightArtifact = z.object({
165
+ readings: z.array(z.object({ provider: ProviderIdSchema, reading: PreflightReading }).strict()).min(1).max(2),
166
+ clusters: z.array(z.object({ members: z.array(z.string()), representative: z.string().min(1) }).strict()),
167
+ chosen: z.object({
168
+ interpretation: z.string().min(1),
169
+ how: z.enum(['single-cluster', 'majority-cluster', 'user-selected', 'user-combined', 'user-typed']),
170
+ }).strict(),
171
+ dropped: z.array(z.object({ provider: ProviderIdSchema, error: z.string().min(1) }).strict()),
172
+ }).strict();
109
173
  // ── S4: RoleOutput — workflow-discriminated union (§12, §13) ─────────────────
110
174
  //
111
175
  // The model output (§13) does NOT carry a `workflow` field; the engine injects the discriminator
@@ -126,7 +190,7 @@ const Attack = z
126
190
  severity: z.enum(['HIGH', 'MED', 'LOW']),
127
191
  })
128
192
  .strict();
129
- export const IdeaRoleOutput = z
193
+ export const LegacyIdeaRoleOutput = z
130
194
  .object({
131
195
  workflow: z.literal('idea-refinement'),
132
196
  task_echo: z.string().min(1), // ≤2 sentence restatement (drift check, S5)
@@ -136,6 +200,174 @@ export const IdeaRoleOutput = z
136
200
  open_questions: z.array(z.string()).max(5),
137
201
  })
138
202
  .strict();
203
+ export const ClaimPosition = z
204
+ .object({
205
+ local_id: z.string().min(1),
206
+ proposition: z.string().min(1),
207
+ dimension_id: z.string().min(1),
208
+ stance: z.enum(['SUPPORT', 'OPPOSE', 'MIXED', 'UNKNOWN']),
209
+ basis: z.enum(['EVIDENCE', 'INFERENCE', 'ASSUMPTION']),
210
+ load_bearing: z.boolean(),
211
+ if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
212
+ reasoning: z.string().min(1),
213
+ evidence_ids: z.array(z.string().min(1)),
214
+ depends_on: z.array(z.string().min(1)),
215
+ })
216
+ .strict();
217
+ const EvidenceCardBase = z
218
+ .object({
219
+ id: z.string().min(1),
220
+ claim_supported: z.string().min(1),
221
+ source_kind: z.enum(['USER', 'PRIMARY', 'SECONDARY', 'MODEL_KNOWLEDGE']),
222
+ title: z.string().min(1).optional(),
223
+ url: z.string().url().optional(),
224
+ published_at: z.string().min(1).optional(),
225
+ accessed_at: z.string().min(1).optional(),
226
+ locator: z.string().min(1).optional(),
227
+ support: z.enum(['SUPPORTS', 'CONTRADICTS', 'CONTEXT_ONLY']),
228
+ freshness: z.enum(['CURRENT', 'DATED', 'UNKNOWN']),
229
+ })
230
+ .strict();
231
+ function checkEvidenceCard(card, ctx) {
232
+ const external = card.source_kind === 'PRIMARY' || card.source_kind === 'SECONDARY';
233
+ if (external && !card.url && !card.locator) {
234
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['locator'], message: `${card.source_kind} evidence requires a URL or locator` });
235
+ }
236
+ if (external && card.freshness === 'CURRENT' && !card.accessed_at) {
237
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['accessed_at'], message: 'current external evidence requires accessed_at' });
238
+ }
239
+ if (card.source_kind === 'MODEL_KNOWLEDGE' && card.freshness === 'CURRENT') {
240
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['freshness'], message: 'model knowledge cannot claim current freshness' });
241
+ }
242
+ }
243
+ export const EvidenceCard = EvidenceCardBase.superRefine(checkEvidenceCard);
244
+ export const CalculationInput = z.object({
245
+ id: z.string().min(1),
246
+ name: z.string().min(1),
247
+ value: z.number().finite(),
248
+ unit: z.string().min(1),
249
+ evidence_ids: z.array(z.string().min(1)).min(1),
250
+ }).strict();
251
+ export const CalculationStep = z.object({
252
+ id: z.string().min(1),
253
+ operation: z.enum(['ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']),
254
+ left: z.string().min(1),
255
+ right: z.string().min(1),
256
+ result: z.number().finite(),
257
+ unit: z.string().min(1),
258
+ }).strict();
259
+ export const CalculationLedger = z.object({
260
+ id: z.string().min(1),
261
+ claim_id: z.string().min(1),
262
+ inputs: z.array(CalculationInput).min(1).max(12),
263
+ steps: z.array(CalculationStep).min(1).max(12),
264
+ result_step: z.string().min(1),
265
+ }).strict();
266
+ // Live 20260714-2142 killed both seats on shape ceremony the prompt never promised: rationale on
267
+ // COVERED entries and a question id have no consumer, so the schema now matches the documented
268
+ // either/or shape (NOT_APPLICABLE still requires its rationale).
269
+ const CoverageEntryBase = z
270
+ .object({
271
+ dimension_id: z.string().min(1),
272
+ status: z.enum(['COVERED', 'NOT_APPLICABLE']),
273
+ position_ids: z.array(z.string().min(1)).default([]),
274
+ rationale: z.string().min(1).optional(),
275
+ })
276
+ .strict();
277
+ function checkCoverageEntry(entry, ctx) {
278
+ if (entry.status === 'NOT_APPLICABLE' && !entry.rationale) {
279
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['rationale'], message: 'NOT_APPLICABLE coverage requires a rationale' });
280
+ }
281
+ }
282
+ export const CoverageEntry = CoverageEntryBase.superRefine(checkCoverageEntry);
283
+ export const DecisionQuestion = z
284
+ .object({
285
+ id: z.string().min(1).optional(),
286
+ question: z.string().min(1),
287
+ claim_ids: z.array(z.string().min(1)),
288
+ })
289
+ .strict();
290
+ const IdeaRoleOutputBase = z
291
+ .object({
292
+ workflow: z.literal('idea-refinement'),
293
+ task_echo: z.string().min(1),
294
+ strongest_version: z.string().min(1),
295
+ positions: z.array(ClaimPosition).max(12),
296
+ evidence: z.array(EvidenceCard).max(20),
297
+ calculations: z.array(CalculationLedger).max(8).default([]),
298
+ coverage: z.array(CoverageEntry).max(18), // 13 core + up to 5 preflight domain dimensions
299
+ decision_questions: z.array(DecisionQuestion).max(8),
300
+ })
301
+ .strict();
302
+ function checkSubmissionRefs(submission, ctx) {
303
+ const positionIds = new Set();
304
+ for (const [index, position] of submission.positions.entries()) {
305
+ if (positionIds.has(position.local_id))
306
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'local_id'], message: `duplicate position id: ${position.local_id}` });
307
+ positionIds.add(position.local_id);
308
+ }
309
+ const evidenceIds = new Set();
310
+ for (const [index, evidence] of submission.evidence.entries()) {
311
+ if (evidenceIds.has(evidence.id))
312
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['evidence', index, 'id'], message: `duplicate evidence id: ${evidence.id}` });
313
+ evidenceIds.add(evidence.id);
314
+ }
315
+ const calculationIds = new Set();
316
+ for (const [index, calculation] of submission.calculations.entries()) {
317
+ if (calculationIds.has(calculation.id))
318
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'id'], message: `duplicate calculation id: ${calculation.id}` });
319
+ calculationIds.add(calculation.id);
320
+ if (!positionIds.has(calculation.claim_id))
321
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'claim_id'], message: `unknown position id: ${calculation.claim_id}` });
322
+ const refs = new Set(calculation.inputs.map((input) => input.id));
323
+ const inputIds = new Set();
324
+ const stepIds = new Set();
325
+ for (const [inputIndex, input] of calculation.inputs.entries()) {
326
+ if (inputIds.has(input.id))
327
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'inputs', inputIndex, 'id'], message: `duplicate calculation input: ${input.id}` });
328
+ inputIds.add(input.id);
329
+ for (const evidenceId of input.evidence_ids) {
330
+ if (!evidenceIds.has(evidenceId))
331
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'inputs', inputIndex, 'evidence_ids'], message: `unknown evidence id: ${evidenceId}` });
332
+ }
333
+ }
334
+ for (const [stepIndex, step] of calculation.steps.entries()) {
335
+ if (!refs.has(step.left))
336
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'left'], message: `unknown or forward calculation reference: ${step.left}` });
337
+ if (!refs.has(step.right))
338
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'right'], message: `unknown or forward calculation reference: ${step.right}` });
339
+ if (refs.has(step.id))
340
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'id'], message: `duplicate calculation reference: ${step.id}` });
341
+ refs.add(step.id);
342
+ stepIds.add(step.id);
343
+ }
344
+ if (!stepIds.has(calculation.result_step))
345
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'result_step'], message: `unknown result step: ${calculation.result_step}` });
346
+ }
347
+ for (const [index, position] of submission.positions.entries()) {
348
+ for (const id of position.evidence_ids) {
349
+ if (!evidenceIds.has(id))
350
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'evidence_ids'], message: `unknown evidence id: ${id}` });
351
+ }
352
+ for (const id of position.depends_on) {
353
+ if (!positionIds.has(id))
354
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'depends_on'], message: `unknown position id: ${id}` });
355
+ }
356
+ }
357
+ for (const [index, entry] of submission.coverage.entries()) {
358
+ for (const id of entry.position_ids) {
359
+ if (!positionIds.has(id))
360
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['coverage', index, 'position_ids'], message: `unknown position id: ${id}` });
361
+ }
362
+ }
363
+ for (const [index, question] of submission.decision_questions.entries()) {
364
+ for (const id of question.claim_ids) {
365
+ if (!positionIds.has(id))
366
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['decision_questions', index, 'claim_ids'], message: `unknown position id: ${id}` });
367
+ }
368
+ }
369
+ }
370
+ export const IdeaRoleOutput = IdeaRoleOutputBase.superRefine(checkSubmissionRefs);
139
371
  /** Defect categories a finding (and a seeded bug, T11) can carry. BENCHMARK.md's "defect class" match
140
372
  * is equality on this enum (off-by-one→CORRECTNESS, race→CONCURRENCY, unhandled-rejection→ERROR_HANDLING,
141
373
  * auth-gap→SECURITY, N+1→PERF). */
@@ -161,16 +393,123 @@ export const CodeReviewRoleOutput = z
161
393
  findings: z.array(Finding).max(12),
162
394
  })
163
395
  .strict();
164
- export const RoleOutput = z.discriminatedUnion('workflow', [IdeaRoleOutput, CodeReviewRoleOutput]);
396
+ export const RoleOutput = z.union([IdeaRoleOutput, CodeReviewRoleOutput]);
165
397
  /** The exact JSON a code-review S4 reviewer returns: `CodeReviewRoleOutput` WITHOUT the `workflow`
166
398
  * discriminator (§13 — model output carries no `workflow`). Mirrors `IdeaRoleOutputModel` (T6); S4
167
399
  * validates the raw call against this, injects `workflow`, then persists as `RoleOutput` (T10). */
168
400
  export const CodeReviewRoleOutputModel = CodeReviewRoleOutput.omit({ workflow: true });
169
- /** The exact JSON the model returns for an idea-refinement S4 seat: `IdeaRoleOutput` WITHOUT the
170
- * `workflow` discriminator (§13 model output carries no `workflow`). S4 validates the raw call
171
- * against this, then injects `workflow` and re-validates as `RoleOutput` before persisting.
172
- * `.omit` preserves the object's strict mode, so extra keys still trigger the §14 repair retry. */
173
- export const IdeaRoleOutputModel = IdeaRoleOutput.omit({ workflow: true });
401
+ const StrictIdeaRoleOutputModel = IdeaRoleOutputBase.omit({ workflow: true }).superRefine((submission, ctx) => checkSubmissionRefs({ workflow: 'idea-refinement', ...submission }, ctx));
402
+ /** Case-insensitive match to an exact canonical enum word (plus known aliases); prose never matches. */
403
+ function canonicalEnum(value, canon, aliases = {}) {
404
+ if (typeof value !== 'string')
405
+ return value;
406
+ const upper = value.toUpperCase();
407
+ const mapped = aliases[upper] ?? upper;
408
+ return canon.includes(mapped) ? mapped : value;
409
+ }
410
+ /** Match a leading canonical/alias token with trailing prose ("OPPOSES: <reason>"), the observed
411
+ * 20260714-2142 codex vocabulary. The FIRST word must itself be the token; free prose whose first
412
+ * word is not a known token still never matches. */
413
+ function canonicalEnumLeadingToken(value, canon, aliases = {}) {
414
+ if (typeof value !== 'string')
415
+ return value;
416
+ const token = value.trim().match(/^[A-Za-z_]+/)?.[0]?.toUpperCase();
417
+ if (!token)
418
+ return value;
419
+ const mapped = aliases[token] ?? token;
420
+ return canon.includes(mapped) ? mapped : value;
421
+ }
422
+ /** Canonicalize enum spellings observed in live provider repairs (SUPPORT, current, Current);
423
+ * anything that is not the exact word in some casing stays invalid. */
424
+ function canonicalizeIdeaRoleOutputModel(input) {
425
+ if (!input || typeof input !== 'object' || Array.isArray(input))
426
+ return input;
427
+ const output = input;
428
+ if (!Array.isArray(output.evidence))
429
+ return input;
430
+ return {
431
+ ...output,
432
+ evidence: output.evidence.map((item) => {
433
+ if (!item || typeof item !== 'object' || Array.isArray(item))
434
+ return item;
435
+ const evidence = item;
436
+ return {
437
+ ...evidence,
438
+ support: canonicalEnumLeadingToken(evidence.support, ['SUPPORTS', 'CONTRADICTS', 'CONTEXT_ONLY'], { SUPPORT: 'SUPPORTS', OPPOSES: 'CONTRADICTS', OPPOSE: 'CONTRADICTS' }),
439
+ freshness: canonicalEnum(evidence.freshness, ['CURRENT', 'DATED', 'UNKNOWN']),
440
+ };
441
+ }),
442
+ };
443
+ }
444
+ /** Deterministic last resort after a failed §14 repair. Three live failures shaped it:
445
+ * run 20260712-0011 wrote evidence prose into `support`; run 20260713-1503 added an unknown `content`
446
+ * key to every card and leaked `MODEL_KNOWLEDGE` into a position's `basis`; run 20260714-2142 killed
447
+ * BOTH seats via an over-cap calculation ledger and coverage/question entries the old salvage never
448
+ * touched. Policy, strictly deterministic (never invents a value):
449
+ * - unknown extra keys are stripped (zod `.strip()` re-parse);
450
+ * - an evidence card that still fails is dropped, and its id scrubbed from positions;
451
+ * - a position that still fails is dropped, and its id scrubbed from depends_on / coverage /
452
+ * decision_questions — one bad position costs one position, not the whole seat;
453
+ * - a calculation that still fails, or whose position/evidence anchors were dropped, is dropped;
454
+ * the survivors are truncated to the schema cap in order (one bad ledger costs one ledger);
455
+ * - a coverage entry or decision question that still fails is dropped (a dropped coverage entry
456
+ * becomes a visible structural hole downstream, never a dead seat);
457
+ * - a seat with NO surviving position stays a hard failure (nothing to analyze). */
458
+ export function salvageIdeaRoleOutputModel(input) {
459
+ const canonical = canonicalizeIdeaRoleOutputModel(input);
460
+ if (!canonical || typeof canonical !== 'object' || Array.isArray(canonical))
461
+ return canonical;
462
+ const output = canonical;
463
+ if (!Array.isArray(output.evidence) || !Array.isArray(output.positions))
464
+ return canonical;
465
+ const evidence = output.evidence
466
+ .map((item) => EvidenceCardBase.strip().superRefine(checkEvidenceCard).safeParse(item))
467
+ .flatMap((result) => (result.success ? [result.data] : []));
468
+ const keptEvidence = new Set(evidence.map((card) => card.id));
469
+ const parsedPositions = output.positions
470
+ .map((item) => ClaimPosition.strip().safeParse(item))
471
+ .flatMap((result) => (result.success ? [result.data] : []));
472
+ if (parsedPositions.length === 0)
473
+ return canonical; // empty claim set → let strict validation fail it
474
+ const keptPositions = new Set(parsedPositions.map((position) => position.local_id));
475
+ const positions = parsedPositions.map((position) => ({
476
+ ...position,
477
+ evidence_ids: position.evidence_ids.filter((id) => keptEvidence.has(id)),
478
+ depends_on: position.depends_on.filter((id) => keptPositions.has(id)),
479
+ }));
480
+ const scrubIds = (value, key) => {
481
+ if (!Array.isArray(value))
482
+ return value;
483
+ return value.map((item) => {
484
+ if (!item || typeof item !== 'object' || Array.isArray(item))
485
+ return item;
486
+ const entry = item;
487
+ if (!Array.isArray(entry[key]))
488
+ return item;
489
+ return { ...entry, [key]: entry[key].filter((id) => keptPositions.has(id)) };
490
+ });
491
+ };
492
+ const parseOrDrop = (value, parse) => Array.isArray(value)
493
+ ? value.map((item) => parse(item)).flatMap((result) => (result.success ? [result.data] : []))
494
+ : value;
495
+ const calculations = Array.isArray(output.calculations)
496
+ ? parseOrDrop(output.calculations, (item) => CalculationLedger.strip().safeParse(item))
497
+ .filter((calc) => keptPositions.has(calc.claim_id)
498
+ && calc.inputs.every((input) => input.evidence_ids.every((id) => keptEvidence.has(id))))
499
+ .slice(0, 8)
500
+ : output.calculations;
501
+ return {
502
+ ...output,
503
+ evidence,
504
+ positions,
505
+ calculations,
506
+ coverage: parseOrDrop(scrubIds(output.coverage, 'position_ids'), (item) => CoverageEntryBase.strip().superRefine(checkCoverageEntry).safeParse(item)),
507
+ decision_questions: parseOrDrop(scrubIds(output.decision_questions, 'claim_ids'), (item) => DecisionQuestion.strip().safeParse(item)),
508
+ };
509
+ }
510
+ /** The model-facing S4 shape. Exact known enum aliases are canonicalized, then the strict schema validates
511
+ * the full output; persisted `IdeaRoleOutput` remains canonical-only. */
512
+ export const IdeaRoleOutputModel = z.preprocess(canonicalizeIdeaRoleOutputModel, StrictIdeaRoleOutputModel);
174
513
  // ── S3: StagePrompts (§9, §13) ──────────────────────────────────────────────
175
514
  //
176
515
  // S3 output: the role-specific S4 prompts with every {{SLOT}} filled. Deterministic validator
@@ -191,10 +530,59 @@ export const ClaimGroups = z
191
530
  groups: z.array(z.array(z.string().min(1)).min(2)),
192
531
  })
193
532
  .strict();
533
+ // ── R2: typed decision graph ────────────────────────────────────────────────
534
+ const GraphPosition = ClaimPosition.extend({
535
+ id: z.string().min(1),
536
+ provider: ProviderIdSchema,
537
+ source_id: z.string().min(1),
538
+ });
539
+ const GraphEvidence = EvidenceCardBase.extend({
540
+ id: z.string().min(1),
541
+ provider: ProviderIdSchema,
542
+ source_id: z.string().min(1),
543
+ }).superRefine(checkEvidenceCard);
544
+ const GraphCalculation = CalculationLedger.extend({
545
+ id: z.string().min(1),
546
+ claim_id: z.string().min(1),
547
+ provider: ProviderIdSchema,
548
+ source_id: z.string().min(1),
549
+ });
550
+ export const CalculationCheck = z.object({
551
+ calculation_id: z.string().min(1),
552
+ claim_id: z.string().min(1),
553
+ status: z.enum(['PASS', 'FAIL']),
554
+ issues: z.array(z.string()),
555
+ }).strict();
556
+ export const DecisionClaim = z.object({
557
+ id: z.string().min(1),
558
+ proposition: z.string().min(1),
559
+ position_ids: z.array(z.string().min(1)).min(1),
560
+ state: z.enum(['CONSENSUS', 'SHARED_CONCERN', 'DISAGREEMENT', 'UNIQUE', 'UNCERTAIN']),
561
+ evidence_state: z.enum(['SUPPORTED', 'CONFLICTED', 'UNVERIFIED']),
562
+ load_bearing: z.boolean(),
563
+ if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
564
+ sensitivity: z.enum(['DECISIVE', 'MATERIAL', 'LOW']),
565
+ });
566
+ export const DecisionGraph = z.object({
567
+ positions: z.array(GraphPosition),
568
+ evidence: z.array(GraphEvidence),
569
+ calculations: z.array(GraphCalculation).default([]),
570
+ calculation_checks: z.array(CalculationCheck).default([]),
571
+ claims: z.array(DecisionClaim),
572
+ edges: z.array(z.object({
573
+ from: z.string().min(1),
574
+ to: z.string().min(1),
575
+ type: z.enum(['DEPENDS_ON', 'SUPPORTS', 'ATTACKS', 'CONTRADICTS']),
576
+ })),
577
+ holes: z.object({
578
+ coverage: z.array(z.object({ dimension_id: z.string().min(1), label: z.string().min(1) })),
579
+ evidence: z.array(z.object({ claim_id: z.string().min(1), reason: z.string().min(1) })),
580
+ }),
581
+ });
194
582
  // ── S8: Verification (§13) ──────────────────────────────────────────────────
195
583
  //
196
584
  // `Verification` is the per-item verdict (§9 "per-item Verification"). `VerificationSet` is the
197
- // actual S8 stage output: the array plus the mandatory justification when zero REFUTEs (§13).
585
+ // actual S8 stage output. Each item is judged independently; no verdict distribution is required.
198
586
  export const Verification = z
199
587
  .object({
200
588
  target_id: z.string().min(1),
@@ -206,10 +594,65 @@ export const Verification = z
206
594
  export const VerificationSet = z
207
595
  .object({
208
596
  verifications: z.array(Verification),
209
- // Required by the S8 prompt only when the verifier issued zero REFUTEs (§13).
597
+ // Accepted for backward compatibility with pre-R1 artifacts; the R1 prompt no longer requests it.
210
598
  all_confirmed_justification: z.string().optional(),
211
599
  })
212
600
  .strict();
601
+ export const ClaimVerification = z.object({
602
+ claim_id: z.string().min(1),
603
+ status: z.enum(['VERIFIED', 'PARTIAL', 'CONTRADICTED', 'UNVERIFIABLE']),
604
+ reasoning: z.string().min(1),
605
+ evidence_ids: z.array(z.string().min(1)),
606
+ calculation_check: z.enum(['PASS', 'FAIL', 'NOT_APPLICABLE']).optional(),
607
+ missing_evidence: z.array(z.string().min(1)),
608
+ }).strict();
609
+ export const ClaimVerificationSet = z.object({
610
+ verifications: z.array(ClaimVerification),
611
+ }).strict();
612
+ // ── R5: bounded, append-only rebuttal events ───────────────────────────────
613
+ const RebuttalResponseBase = z.object({
614
+ claim_id: z.string().min(1),
615
+ response: z.enum(['CONCEDE', 'COUNTER', 'NARROW', 'UNRESOLVED']),
616
+ reasoning: z.string().min(1),
617
+ evidence_ids: z.array(z.string().min(1)),
618
+ narrowed_proposition: z.string().min(1).optional(),
619
+ }).strict();
620
+ const checkNarrowedRebuttal = (event, ctx) => {
621
+ if (event.response === 'NARROW' && !event.narrowed_proposition) {
622
+ ctx.addIssue({
623
+ code: z.ZodIssueCode.custom,
624
+ path: ['narrowed_proposition'],
625
+ message: 'NARROW requires narrowed_proposition',
626
+ });
627
+ }
628
+ };
629
+ export const RebuttalResponse = RebuttalResponseBase.superRefine(checkNarrowedRebuttal);
630
+ /** Exact model output for one scout's single rebuttal round. */
631
+ export const RebuttalResponseSet = z.object({
632
+ events: z.array(RebuttalResponse).max(3),
633
+ }).strict();
634
+ export const RebuttalEvent = RebuttalResponseBase.extend({
635
+ id: z.string().min(1),
636
+ round: z.literal(1),
637
+ responder: ProviderIdSchema,
638
+ target_position_ids: z.array(z.string().min(1)),
639
+ }).superRefine(checkNarrowedRebuttal);
640
+ /** Persisted separately from DecisionGraph so original claims/evidence remain immutable. */
641
+ export const RebuttalEventSet = z.object({
642
+ round: z.literal(1),
643
+ selected_claim_ids: z.array(z.string().min(1)).max(3),
644
+ events: z.array(RebuttalEvent).max(6),
645
+ stop_reason: z.enum([
646
+ 'NO_ESCALATIONS',
647
+ 'NO_ELIGIBLE_SCOUT',
648
+ 'BUDGET_RESERVED',
649
+ 'ROUND_COMPLETE',
650
+ 'NO_NEW_EVIDENCE',
651
+ 'CALL_CAP_REACHED',
652
+ ]),
653
+ }).strict();
654
+ /** Shared artifact slot: code review keeps the v1 cross-exam shape; idea refinement uses R4 claims. */
655
+ export const VerificationArtifact = z.union([VerificationSet, ClaimVerificationSet]);
213
656
  // ── S7: DisagreementMap (§7, §9) ────────────────────────────────────────────
214
657
  //
215
658
  // The plan (§7/§9) names only the four arrays. `Claim` shape is from §6. NOTE two under-specified
@@ -280,16 +723,69 @@ const Adjudication = z
280
723
  id: z.string().min(1), // disputed item id
281
724
  ruling: z.enum(['UPHOLD', 'REJECT', 'UNRESOLVED']),
282
725
  reasoning: z.string().min(1), // ≤3 sentences
283
- evidence_cited: z.string().min(1),
726
+ evidence_cited: z.string().min(1).optional(), // code-review / legacy idea artifacts
727
+ evidence_ids: z.array(z.string().min(1)).optional(), // R4 idea chair citations, validated by reference
728
+ effect_on_decision: z.string().min(1).optional(), // R5 idea chair; optional for legacy/code-review artifacts
729
+ what_would_change_it: z.string().min(1).optional(), // required by S9 when an idea ruling is UNRESOLVED
284
730
  })
285
- .strict();
731
+ .strict()
732
+ .refine((item) => item.evidence_cited || item.evidence_ids?.length, { message: 'adjudication requires evidence' });
286
733
  export const Recommendation = z.enum(['PROCEED', 'PROCEED_WITH_CONDITIONS', 'PIVOT', 'STOP']);
734
+ const DecisionSnapshotItem = z.object({
735
+ label: z.string().min(1),
736
+ value: z.string().min(1),
737
+ meaning: z.string().min(1),
738
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
739
+ }).strict();
740
+ const DecisionOption = z.object({
741
+ label: z.string().min(1),
742
+ commitment: z.string().min(1),
743
+ commitment_kind: z.enum(['KNOWN', 'TARGET_CAP', 'UNKNOWN']),
744
+ tradeoff: z.string().min(1),
745
+ claim_ids: z.array(z.string().min(1)).max(4),
746
+ }).strict().superRefine((option, ctx) => {
747
+ if (option.commitment_kind !== 'UNKNOWN' && option.claim_ids.length === 0) {
748
+ ctx.addIssue({
749
+ code: z.ZodIssueCode.custom,
750
+ path: ['claim_ids'],
751
+ message: 'known commitments and target caps require a graph claim anchor',
752
+ });
753
+ }
754
+ });
755
+ /** Reader-first, graph-anchored numeric comparison for decision tasks where amounts or thresholds matter. */
756
+ export const DecisionSnapshot = z.object({
757
+ decisive_numbers: z.array(DecisionSnapshotItem).min(1).max(5),
758
+ payback: z.object({
759
+ status: z.enum(['ACHIEVED', 'NOT_ACHIEVED', 'NOT_COMPUTABLE']),
760
+ result: z.string().min(1),
761
+ basis: z.string().min(1),
762
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
763
+ }).strict().optional(),
764
+ options: z.array(DecisionOption).min(2).max(4),
765
+ tripwire: z.object({
766
+ metric: z.string().min(1),
767
+ threshold: z.string().min(1),
768
+ decision_rule: z.string().min(1),
769
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
770
+ }).strict().optional(),
771
+ }).strict();
287
772
  const JudgeReportBase = z
288
773
  .object({
289
774
  adjudications: z.array(Adjudication),
290
775
  verdict: z.string().min(1), // the recommendation + core reason (idea: 2-5 sentences; grounded in adjudicated + consensus claims)
291
776
  recommendation: Recommendation.optional(), // idea workflow; code-review omits it
292
777
  conditions: z.array(z.string().min(1)).max(6).optional(), // present only for PROCEED_WITH_CONDITIONS
778
+ recommendation_claim_ids: z.array(z.string().min(1)).min(1).max(8).optional(),
779
+ condition_claim_ids: z.array(z.string().min(1)).min(1).max(8).optional(),
780
+ pivot: z.object({
781
+ changed_claim_id: z.string().min(1),
782
+ new_risk_claim_id: z.string().min(1),
783
+ }).strict().optional(),
784
+ strongest_counter_case: z.object({
785
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
786
+ reasoning: z.string().min(1),
787
+ }).strict().optional(),
788
+ decision_snapshot: DecisionSnapshot.optional(),
293
789
  key_points: z.array(z.string()).max(10).optional(), // chairman's bulleted reasoning (idea workflow); code-review omits it
294
790
  dissent: z.array(z.string()).min(1), // ≥1 — empty dissent is invalid (§9); strongest counter-argument
295
791
  confidence_notes: z.string().min(1), // which conclusions are HIGH/MEDIUM/LOW and why
@@ -319,6 +815,27 @@ export const JudgeReport = JudgeReportBase.superRefine((r, ctx) => {
319
815
  export const JudgeReportModel = JudgeReportBase.extend({
320
816
  dissent: z.array(z.string()),
321
817
  });
818
+ /** R5 idea-chair boundary. Persisted JudgeReport keeps its legacy adjudication names so code-review
819
+ * and old run readers stay compatible; S9 translates only after this exact model shape validates. */
820
+ export const IdeaChairRuling = z.object({
821
+ claim_id: z.string().min(1),
822
+ ruling: z.enum(['HOLDS', 'FAILS', 'UNRESOLVED']),
823
+ reasoning: z.string().min(1),
824
+ evidence_ids: z.array(z.string().min(1)).min(1),
825
+ effect_on_decision: z.string().min(1),
826
+ what_would_change_it: z.string().min(1).optional(),
827
+ }).strict().superRefine((ruling, ctx) => {
828
+ if (ruling.ruling === 'UNRESOLVED' && !ruling.what_would_change_it) {
829
+ ctx.addIssue({
830
+ code: z.ZodIssueCode.custom,
831
+ path: ['what_would_change_it'],
832
+ message: 'UNRESOLVED requires what_would_change_it',
833
+ });
834
+ }
835
+ });
836
+ export const IdeaChairReportModel = JudgeReportModel.omit({ adjudications: true }).extend({
837
+ adjudications: z.array(IdeaChairRuling),
838
+ });
322
839
  // ── S9b: ActionPlan (idea-refinement report v3) ─────────────────────────────
323
840
  export const ActionPlan = z
324
841
  .object({
@@ -338,6 +855,31 @@ export const ActionPlan = z
338
855
  sequencing_note: z.string().min(1),
339
856
  })
340
857
  .strict();
858
+ export const PlannerUnavailable = z.object({
859
+ kind: z.literal('PlannerUnavailable'),
860
+ reason: z.enum(['budget_exhausted', 'planner_failed']),
861
+ unresolved_questions: z.array(z.string().min(1)).min(1).max(10),
862
+ }).strict();
863
+ export const ActionPlanArtifact = z.union([ActionPlan, PlannerUnavailable]);
864
+ /** R6 quick mode: one strong analyst produces the analysis, recommendation, and plan in one call. */
865
+ export const QuickDecisionModel = z.object({
866
+ analysis: IdeaRoleOutputModel,
867
+ verdict: z.string().min(1),
868
+ recommendation: Recommendation,
869
+ conditions: z.array(z.string().min(1)).max(6),
870
+ key_points: z.array(z.string().min(1)).min(2).max(8),
871
+ dissent: z.array(z.string().min(1)).min(1).max(4),
872
+ confidence_notes: z.string().min(1),
873
+ action_plan: ActionPlan,
874
+ }).strict().superRefine((decision, ctx) => {
875
+ const hasConditions = decision.conditions.length > 0;
876
+ if (decision.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
877
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['conditions'], message: 'conditions are required for PROCEED_WITH_CONDITIONS' });
878
+ }
879
+ if (decision.recommendation !== 'PROCEED_WITH_CONDITIONS' && hasConditions) {
880
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['conditions'], message: 'conditions are only valid for PROCEED_WITH_CONDITIONS' });
881
+ }
882
+ });
341
883
  // ── RunMeta (§15, §16) ──────────────────────────────────────────────────────
342
884
  //
343
885
  // Written by the artifact writer; assembled by the engine's RunCtx (T5). Internal → not strict.
@@ -345,6 +887,7 @@ export const ActionPlan = z
345
887
  export const CallRecord = z.object({
346
888
  provider: ProviderIdSchema,
347
889
  stage: z.string(), // e.g. "S4", "S1"
890
+ category: z.enum(['discovery', 'verification', 'repair', 'planning']).optional(),
348
891
  durationMs: z.number().nonnegative(),
349
892
  error: z.enum(['NOT_FOUND', 'AUTH', 'QUOTA', 'TIMEOUT', 'BAD_OUTPUT', 'CRASH']).optional(),
350
893
  });
@@ -360,6 +903,7 @@ const FlagProfileSchema = z.object({
360
903
  export const RunMeta = z.object({
361
904
  run_id: z.string().min(1), // encodes the timestamp (e.g. 20260702-1412-idea-refinement-a3f9)
362
905
  workflow: WorkflowIdSchema,
906
+ mode: IdeaModeSchema.optional(),
363
907
  provider_versions: z.record(ProviderIdSchema, z.string()), // detected `--version` strings
364
908
  flag_profiles: z.record(ProviderIdSchema, FlagProfileSchema),
365
909
  roles: z.record(z.string(), ProviderIdSchema), // role name → assigned provider
@@ -367,8 +911,23 @@ export const RunMeta = z.object({
367
911
  calls: z.array(CallRecord),
368
912
  call_count: z.number().int().nonnegative(),
369
913
  budget: z.object({ limit: z.number().int().positive(), used: z.number().int().nonnegative() }),
914
+ receipt: z.object({
915
+ discovery: z.number().int().nonnegative(),
916
+ verification: z.number().int().nonnegative(),
917
+ repair: z.number().int().nonnegative(),
918
+ planning: z.number().int().nonnegative(),
919
+ }).optional(),
370
920
  exit_status: z.enum(['ok', 'failed', 'aborted', 'partial']),
371
921
  aborted: z.boolean(), // §16: Ctrl+C finalizes meta with aborted:true
372
922
  // §16 report-header flags; absent = none.
373
- flags: z.array(z.enum(['synthesis_suspect', 'low_diversity', 'plan_skipped', 'plan_fallback'])).optional(),
923
+ flags: z.array(z.enum([
924
+ 'synthesis_suspect',
925
+ 'low_diversity',
926
+ 'plan_skipped',
927
+ 'plan_fallback',
928
+ 'headless_intent',
929
+ 'verification_skipped',
930
+ 'research_ungrounded',
931
+ 'single_model',
932
+ ])).optional(),
374
933
  });