aiki-cli 0.2.2 → 0.3.1

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 (60) hide show
  1. package/CHANGELOG.md +113 -6
  2. package/README.md +119 -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 +66 -8
  17. package/dist/council/view.js +446 -117
  18. package/dist/orchestration/calculations.js +97 -0
  19. package/dist/orchestration/context.js +37 -9
  20. package/dist/orchestration/decision-dossier.js +320 -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 +39 -0
  28. package/dist/orchestration/preflight.js +203 -0
  29. package/dist/orchestration/quick-analysis.js +93 -0
  30. package/dist/orchestration/stages/cr-ladder.js +80 -0
  31. package/dist/orchestration/stages/s10-render.js +574 -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 +118 -85
  40. package/dist/orchestration/url-sources.js +200 -0
  41. package/dist/providers/codex.js +2 -1
  42. package/dist/providers/spawn.js +5 -0
  43. package/dist/schemas/index.js +638 -15
  44. package/dist/skills/idea-refinement/analyst.md +18 -14
  45. package/dist/skills/idea-refinement/economics-delivery.md +7 -0
  46. package/dist/skills/idea-refinement/market-adoption.md +7 -0
  47. package/dist/storage/runs.js +12 -4
  48. package/dist/tui/app.js +53 -14
  49. package/dist/tui/format.js +4 -5
  50. package/dist/tui/smart-entry.js +17 -1
  51. package/dist/tui/timeline.js +2 -4
  52. package/dist/workflows/code-review.js +4 -2
  53. package/dist/workflows/idea-refinement.js +118 -46
  54. package/package.json +12 -4
  55. package/dist/orchestration/stages/s0-grill.js +0 -79
  56. package/dist/orchestration/stages/s1-intent.js +0 -25
  57. package/dist/orchestration/stages/s2-misread.js +0 -76
  58. package/dist/orchestration/stages/s3-prompts.js +0 -55
  59. package/dist/orchestration/stages/s6-claims.js +0 -56
  60. package/dist/orchestration/stages/s7-disagreement.js +0 -134
@@ -17,6 +17,43 @@ 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 RequestedOutputSchema = z.enum([
23
+ 'DECISION',
24
+ 'FEATURE_BACKLOG',
25
+ 'IMPLEMENTATION_PLAN',
26
+ ]);
27
+ export const UrlSourceStatusSchema = z.enum(['FETCHED', 'BLOCKED', 'FAILED']);
28
+ export const UrlSourceSnapshot = z.object({
29
+ id: z.string().regex(/^U[1-5]$/),
30
+ url: z.string().url(),
31
+ final_url: z.string().url().optional(),
32
+ status: UrlSourceStatusSchema,
33
+ title: z.string().min(1).optional(),
34
+ content_type: z.string().min(1).optional(),
35
+ accessed_at: z.string().datetime(),
36
+ sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
37
+ content: z.string().min(1).optional(),
38
+ error: z.string().min(1).optional(),
39
+ }).strict().superRefine((source, ctx) => {
40
+ if (source.status === 'FETCHED' && (!source.content || !source.sha256)) {
41
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'FETCHED source requires content and sha256' });
42
+ }
43
+ if (source.status !== 'FETCHED' && !source.error) {
44
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `${source.status} source requires an error` });
45
+ }
46
+ });
47
+ export const UrlSourceSet = z.object({
48
+ sources: z.array(UrlSourceSnapshot).max(5),
49
+ }).strict();
50
+ export const DomainDimension = z
51
+ .object({
52
+ id: z.string().regex(/^D[1-5]$/),
53
+ label: z.string().min(1),
54
+ rationale: z.string().min(1),
55
+ })
56
+ .strict();
20
57
  // ── S1: IntentContract (§13) ────────────────────────────────────────────────
21
58
  export const IntentContract = z
22
59
  .object({
@@ -25,8 +62,22 @@ export const IntentContract = z
25
62
  constraints: z.array(z.string()), // explicit constraints the user stated (may be empty)
26
63
  unknowns: z.array(z.string()), // things the request leaves unspecified
27
64
  success_criteria: z.array(z.string()), // what a good final output must contain
65
+ domain_dimensions: z.array(DomainDimension).min(3).max(5).optional(), // required by the idea preflight; optional for old/code-review contracts
28
66
  })
29
67
  .strict();
68
+ /** R6 decision contract: the two-view preflight's single downstream boundary. */
69
+ export const DecisionContract = IntentContract.extend({
70
+ alternatives: z.array(z.string().min(1)).max(8),
71
+ success_bar: z.string().min(1),
72
+ evidence_supplied: z.array(z.string().min(1)).max(12),
73
+ missing_evidence: z.array(z.string().min(1)).max(12),
74
+ core_rubric: z.array(z.string().min(1)).min(1),
75
+ user_confirmed: z.boolean(),
76
+ confirmation: z.enum(['user-confirmed', 'headless-defaulted']),
77
+ requested_outputs: z.array(RequestedOutputSchema).min(1).max(4).optional(),
78
+ }).strict();
79
+ /** Code review and old idea runs keep the smaller v1 contract. */
80
+ export const DecisionContractArtifact = z.union([DecisionContract, IntentContract]);
30
81
  // ── S2: Interpretation — per provider (§13) ─────────────────────────────────
31
82
  export const Interpretation = z
32
83
  .object({
@@ -65,7 +116,8 @@ const RunBriefDraftBase = z
65
116
  claims_to_test: z.array(z.string().min(1)).max(8),
66
117
  evidence_supplied: z.array(z.string().min(1)).max(8),
67
118
  missing_axes: z.array(z.string().min(1)).max(8),
68
- questions: z.array(RunBriefQuestion).min(3).max(4),
119
+ domain_dimensions: z.array(DomainDimension).min(3).max(5),
120
+ questions: z.array(RunBriefQuestion).max(4),
69
121
  })
70
122
  .strict();
71
123
  function checkQuestionIds(questions, ctx) {
@@ -77,7 +129,19 @@ function checkQuestionIds(questions, ctx) {
77
129
  seen.add(q.id);
78
130
  }
79
131
  }
80
- export const RunBriefDraft = RunBriefDraftBase.superRefine((brief, ctx) => checkQuestionIds(brief.questions, ctx));
132
+ function checkDomainDimensionIds(dimensions, ctx) {
133
+ const seen = new Set();
134
+ for (const dimension of dimensions) {
135
+ if (seen.has(dimension.id)) {
136
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['domain_dimensions'], message: `duplicate domain dimension id: ${dimension.id}` });
137
+ }
138
+ seen.add(dimension.id);
139
+ }
140
+ }
141
+ export const RunBriefDraft = RunBriefDraftBase.superRefine((brief, ctx) => {
142
+ checkQuestionIds(brief.questions, ctx);
143
+ checkDomainDimensionIds(brief.domain_dimensions, ctx);
144
+ });
81
145
  export const GrillAnswer = z
82
146
  .object({
83
147
  question_id: z.string().min(1),
@@ -86,9 +150,10 @@ export const GrillAnswer = z
86
150
  })
87
151
  .strict();
88
152
  export const RunBrief = RunBriefDraftBase.extend({
89
- answers: z.array(GrillAnswer).min(3).max(4),
153
+ answers: z.array(GrillAnswer).max(4),
90
154
  }).superRefine((brief, ctx) => {
91
155
  checkQuestionIds(brief.questions, ctx);
156
+ checkDomainDimensionIds(brief.domain_dimensions, ctx);
92
157
  const questionIds = new Set(brief.questions.map((q) => q.id));
93
158
  const answerIds = new Set();
94
159
  for (const answer of brief.answers) {
@@ -106,6 +171,34 @@ export const RunBrief = RunBriefDraftBase.extend({
106
171
  }
107
172
  }
108
173
  });
174
+ /** One of the two independent R6 preflight readings. */
175
+ export const PreflightReading = z.object({
176
+ subject: z.string().min(1),
177
+ interpretation: z.string().min(1),
178
+ normalized_decision: z.string().min(1),
179
+ alternatives: z.array(z.string().min(1)).max(8),
180
+ target_user: z.string().min(1).nullable(),
181
+ constraints: z.array(z.string().min(1)).max(10),
182
+ success_bar: z.string().min(1),
183
+ success_criteria: z.array(z.string().min(1)).max(8),
184
+ claims_to_test: z.array(z.string().min(1)).max(8),
185
+ evidence_supplied: z.array(z.string().min(1)).max(8),
186
+ missing_evidence: z.array(z.string().min(1)).max(8),
187
+ domain_dimensions: z.array(DomainDimension).min(3).max(5),
188
+ questions: z.array(RunBriefQuestion).max(4),
189
+ }).strict().superRefine((reading, ctx) => {
190
+ checkQuestionIds(reading.questions, ctx);
191
+ checkDomainDimensionIds(reading.domain_dimensions, ctx);
192
+ });
193
+ export const PreflightArtifact = z.object({
194
+ readings: z.array(z.object({ provider: ProviderIdSchema, reading: PreflightReading }).strict()).min(1).max(2),
195
+ clusters: z.array(z.object({ members: z.array(z.string()), representative: z.string().min(1) }).strict()),
196
+ chosen: z.object({
197
+ interpretation: z.string().min(1),
198
+ how: z.enum(['single-cluster', 'majority-cluster', 'user-selected', 'user-combined', 'user-typed']),
199
+ }).strict(),
200
+ dropped: z.array(z.object({ provider: ProviderIdSchema, error: z.string().min(1) }).strict()),
201
+ }).strict();
109
202
  // ── S4: RoleOutput — workflow-discriminated union (§12, §13) ─────────────────
110
203
  //
111
204
  // The model output (§13) does NOT carry a `workflow` field; the engine injects the discriminator
@@ -126,7 +219,7 @@ const Attack = z
126
219
  severity: z.enum(['HIGH', 'MED', 'LOW']),
127
220
  })
128
221
  .strict();
129
- export const IdeaRoleOutput = z
222
+ export const LegacyIdeaRoleOutput = z
130
223
  .object({
131
224
  workflow: z.literal('idea-refinement'),
132
225
  task_echo: z.string().min(1), // ≤2 sentence restatement (drift check, S5)
@@ -136,6 +229,174 @@ export const IdeaRoleOutput = z
136
229
  open_questions: z.array(z.string()).max(5),
137
230
  })
138
231
  .strict();
232
+ export const ClaimPosition = z
233
+ .object({
234
+ local_id: z.string().min(1),
235
+ proposition: z.string().min(1),
236
+ dimension_id: z.string().min(1),
237
+ stance: z.enum(['SUPPORT', 'OPPOSE', 'MIXED', 'UNKNOWN']),
238
+ basis: z.enum(['EVIDENCE', 'INFERENCE', 'ASSUMPTION']),
239
+ load_bearing: z.boolean(),
240
+ if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
241
+ reasoning: z.string().min(1),
242
+ evidence_ids: z.array(z.string().min(1)),
243
+ depends_on: z.array(z.string().min(1)),
244
+ })
245
+ .strict();
246
+ const EvidenceCardBase = z
247
+ .object({
248
+ id: z.string().min(1),
249
+ claim_supported: z.string().min(1),
250
+ source_kind: z.enum(['USER', 'PRIMARY', 'SECONDARY', 'MODEL_KNOWLEDGE']),
251
+ title: z.string().min(1).optional(),
252
+ url: z.string().url().optional(),
253
+ published_at: z.string().min(1).optional(),
254
+ accessed_at: z.string().min(1).optional(),
255
+ locator: z.string().min(1).optional(),
256
+ support: z.enum(['SUPPORTS', 'CONTRADICTS', 'CONTEXT_ONLY']),
257
+ freshness: z.enum(['CURRENT', 'DATED', 'UNKNOWN']),
258
+ })
259
+ .strict();
260
+ function checkEvidenceCard(card, ctx) {
261
+ const external = card.source_kind === 'PRIMARY' || card.source_kind === 'SECONDARY';
262
+ if (external && !card.url && !card.locator) {
263
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['locator'], message: `${card.source_kind} evidence requires a URL or locator` });
264
+ }
265
+ if (external && card.freshness === 'CURRENT' && !card.accessed_at) {
266
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['accessed_at'], message: 'current external evidence requires accessed_at' });
267
+ }
268
+ if (card.source_kind === 'MODEL_KNOWLEDGE' && card.freshness === 'CURRENT') {
269
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['freshness'], message: 'model knowledge cannot claim current freshness' });
270
+ }
271
+ }
272
+ export const EvidenceCard = EvidenceCardBase.superRefine(checkEvidenceCard);
273
+ export const CalculationInput = z.object({
274
+ id: z.string().min(1),
275
+ name: z.string().min(1),
276
+ value: z.number().finite(),
277
+ unit: z.string().min(1),
278
+ evidence_ids: z.array(z.string().min(1)).min(1),
279
+ }).strict();
280
+ export const CalculationStep = z.object({
281
+ id: z.string().min(1),
282
+ operation: z.enum(['ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']),
283
+ left: z.string().min(1),
284
+ right: z.string().min(1),
285
+ result: z.number().finite(),
286
+ unit: z.string().min(1),
287
+ }).strict();
288
+ export const CalculationLedger = z.object({
289
+ id: z.string().min(1),
290
+ claim_id: z.string().min(1),
291
+ inputs: z.array(CalculationInput).min(1).max(12),
292
+ steps: z.array(CalculationStep).min(1).max(12),
293
+ result_step: z.string().min(1),
294
+ }).strict();
295
+ // Live 20260714-2142 killed both seats on shape ceremony the prompt never promised: rationale on
296
+ // COVERED entries and a question id have no consumer, so the schema now matches the documented
297
+ // either/or shape (NOT_APPLICABLE still requires its rationale).
298
+ const CoverageEntryBase = z
299
+ .object({
300
+ dimension_id: z.string().min(1),
301
+ status: z.enum(['COVERED', 'NOT_APPLICABLE']),
302
+ position_ids: z.array(z.string().min(1)).default([]),
303
+ rationale: z.string().min(1).optional(),
304
+ })
305
+ .strict();
306
+ function checkCoverageEntry(entry, ctx) {
307
+ if (entry.status === 'NOT_APPLICABLE' && !entry.rationale) {
308
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['rationale'], message: 'NOT_APPLICABLE coverage requires a rationale' });
309
+ }
310
+ }
311
+ export const CoverageEntry = CoverageEntryBase.superRefine(checkCoverageEntry);
312
+ export const DecisionQuestion = z
313
+ .object({
314
+ id: z.string().min(1).optional(),
315
+ question: z.string().min(1),
316
+ claim_ids: z.array(z.string().min(1)),
317
+ })
318
+ .strict();
319
+ const IdeaRoleOutputBase = z
320
+ .object({
321
+ workflow: z.literal('idea-refinement'),
322
+ task_echo: z.string().min(1),
323
+ strongest_version: z.string().min(1),
324
+ positions: z.array(ClaimPosition).max(12),
325
+ evidence: z.array(EvidenceCard).max(20),
326
+ calculations: z.array(CalculationLedger).max(8).default([]),
327
+ coverage: z.array(CoverageEntry).max(18), // 13 core + up to 5 preflight domain dimensions
328
+ decision_questions: z.array(DecisionQuestion).max(8),
329
+ })
330
+ .strict();
331
+ function checkSubmissionRefs(submission, ctx) {
332
+ const positionIds = new Set();
333
+ for (const [index, position] of submission.positions.entries()) {
334
+ if (positionIds.has(position.local_id))
335
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'local_id'], message: `duplicate position id: ${position.local_id}` });
336
+ positionIds.add(position.local_id);
337
+ }
338
+ const evidenceIds = new Set();
339
+ for (const [index, evidence] of submission.evidence.entries()) {
340
+ if (evidenceIds.has(evidence.id))
341
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['evidence', index, 'id'], message: `duplicate evidence id: ${evidence.id}` });
342
+ evidenceIds.add(evidence.id);
343
+ }
344
+ const calculationIds = new Set();
345
+ for (const [index, calculation] of submission.calculations.entries()) {
346
+ if (calculationIds.has(calculation.id))
347
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'id'], message: `duplicate calculation id: ${calculation.id}` });
348
+ calculationIds.add(calculation.id);
349
+ if (!positionIds.has(calculation.claim_id))
350
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'claim_id'], message: `unknown position id: ${calculation.claim_id}` });
351
+ const refs = new Set(calculation.inputs.map((input) => input.id));
352
+ const inputIds = new Set();
353
+ const stepIds = new Set();
354
+ for (const [inputIndex, input] of calculation.inputs.entries()) {
355
+ if (inputIds.has(input.id))
356
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'inputs', inputIndex, 'id'], message: `duplicate calculation input: ${input.id}` });
357
+ inputIds.add(input.id);
358
+ for (const evidenceId of input.evidence_ids) {
359
+ if (!evidenceIds.has(evidenceId))
360
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'inputs', inputIndex, 'evidence_ids'], message: `unknown evidence id: ${evidenceId}` });
361
+ }
362
+ }
363
+ for (const [stepIndex, step] of calculation.steps.entries()) {
364
+ if (!refs.has(step.left))
365
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'left'], message: `unknown or forward calculation reference: ${step.left}` });
366
+ if (!refs.has(step.right))
367
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'right'], message: `unknown or forward calculation reference: ${step.right}` });
368
+ if (refs.has(step.id))
369
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'steps', stepIndex, 'id'], message: `duplicate calculation reference: ${step.id}` });
370
+ refs.add(step.id);
371
+ stepIds.add(step.id);
372
+ }
373
+ if (!stepIds.has(calculation.result_step))
374
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['calculations', index, 'result_step'], message: `unknown result step: ${calculation.result_step}` });
375
+ }
376
+ for (const [index, position] of submission.positions.entries()) {
377
+ for (const id of position.evidence_ids) {
378
+ if (!evidenceIds.has(id))
379
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'evidence_ids'], message: `unknown evidence id: ${id}` });
380
+ }
381
+ for (const id of position.depends_on) {
382
+ if (!positionIds.has(id))
383
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['positions', index, 'depends_on'], message: `unknown position id: ${id}` });
384
+ }
385
+ }
386
+ for (const [index, entry] of submission.coverage.entries()) {
387
+ for (const id of entry.position_ids) {
388
+ if (!positionIds.has(id))
389
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['coverage', index, 'position_ids'], message: `unknown position id: ${id}` });
390
+ }
391
+ }
392
+ for (const [index, question] of submission.decision_questions.entries()) {
393
+ for (const id of question.claim_ids) {
394
+ if (!positionIds.has(id))
395
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['decision_questions', index, 'claim_ids'], message: `unknown position id: ${id}` });
396
+ }
397
+ }
398
+ }
399
+ export const IdeaRoleOutput = IdeaRoleOutputBase.superRefine(checkSubmissionRefs);
139
400
  /** Defect categories a finding (and a seeded bug, T11) can carry. BENCHMARK.md's "defect class" match
140
401
  * is equality on this enum (off-by-one→CORRECTNESS, race→CONCURRENCY, unhandled-rejection→ERROR_HANDLING,
141
402
  * auth-gap→SECURITY, N+1→PERF). */
@@ -161,16 +422,123 @@ export const CodeReviewRoleOutput = z
161
422
  findings: z.array(Finding).max(12),
162
423
  })
163
424
  .strict();
164
- export const RoleOutput = z.discriminatedUnion('workflow', [IdeaRoleOutput, CodeReviewRoleOutput]);
425
+ export const RoleOutput = z.union([IdeaRoleOutput, CodeReviewRoleOutput]);
165
426
  /** The exact JSON a code-review S4 reviewer returns: `CodeReviewRoleOutput` WITHOUT the `workflow`
166
427
  * discriminator (§13 — model output carries no `workflow`). Mirrors `IdeaRoleOutputModel` (T6); S4
167
428
  * validates the raw call against this, injects `workflow`, then persists as `RoleOutput` (T10). */
168
429
  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 });
430
+ const StrictIdeaRoleOutputModel = IdeaRoleOutputBase.omit({ workflow: true }).superRefine((submission, ctx) => checkSubmissionRefs({ workflow: 'idea-refinement', ...submission }, ctx));
431
+ /** Case-insensitive match to an exact canonical enum word (plus known aliases); prose never matches. */
432
+ function canonicalEnum(value, canon, aliases = {}) {
433
+ if (typeof value !== 'string')
434
+ return value;
435
+ const upper = value.toUpperCase();
436
+ const mapped = aliases[upper] ?? upper;
437
+ return canon.includes(mapped) ? mapped : value;
438
+ }
439
+ /** Match a leading canonical/alias token with trailing prose ("OPPOSES: <reason>"), the observed
440
+ * 20260714-2142 codex vocabulary. The FIRST word must itself be the token; free prose whose first
441
+ * word is not a known token still never matches. */
442
+ function canonicalEnumLeadingToken(value, canon, aliases = {}) {
443
+ if (typeof value !== 'string')
444
+ return value;
445
+ const token = value.trim().match(/^[A-Za-z_]+/)?.[0]?.toUpperCase();
446
+ if (!token)
447
+ return value;
448
+ const mapped = aliases[token] ?? token;
449
+ return canon.includes(mapped) ? mapped : value;
450
+ }
451
+ /** Canonicalize enum spellings observed in live provider repairs (SUPPORT, current, Current);
452
+ * anything that is not the exact word in some casing stays invalid. */
453
+ function canonicalizeIdeaRoleOutputModel(input) {
454
+ if (!input || typeof input !== 'object' || Array.isArray(input))
455
+ return input;
456
+ const output = input;
457
+ if (!Array.isArray(output.evidence))
458
+ return input;
459
+ return {
460
+ ...output,
461
+ evidence: output.evidence.map((item) => {
462
+ if (!item || typeof item !== 'object' || Array.isArray(item))
463
+ return item;
464
+ const evidence = item;
465
+ return {
466
+ ...evidence,
467
+ support: canonicalEnumLeadingToken(evidence.support, ['SUPPORTS', 'CONTRADICTS', 'CONTEXT_ONLY'], { SUPPORT: 'SUPPORTS', OPPOSES: 'CONTRADICTS', OPPOSE: 'CONTRADICTS' }),
468
+ freshness: canonicalEnum(evidence.freshness, ['CURRENT', 'DATED', 'UNKNOWN']),
469
+ };
470
+ }),
471
+ };
472
+ }
473
+ /** Deterministic last resort after a failed §14 repair. Three live failures shaped it:
474
+ * run 20260712-0011 wrote evidence prose into `support`; run 20260713-1503 added an unknown `content`
475
+ * key to every card and leaked `MODEL_KNOWLEDGE` into a position's `basis`; run 20260714-2142 killed
476
+ * BOTH seats via an over-cap calculation ledger and coverage/question entries the old salvage never
477
+ * touched. Policy, strictly deterministic (never invents a value):
478
+ * - unknown extra keys are stripped (zod `.strip()` re-parse);
479
+ * - an evidence card that still fails is dropped, and its id scrubbed from positions;
480
+ * - a position that still fails is dropped, and its id scrubbed from depends_on / coverage /
481
+ * decision_questions — one bad position costs one position, not the whole seat;
482
+ * - a calculation that still fails, or whose position/evidence anchors were dropped, is dropped;
483
+ * the survivors are truncated to the schema cap in order (one bad ledger costs one ledger);
484
+ * - a coverage entry or decision question that still fails is dropped (a dropped coverage entry
485
+ * becomes a visible structural hole downstream, never a dead seat);
486
+ * - a seat with NO surviving position stays a hard failure (nothing to analyze). */
487
+ export function salvageIdeaRoleOutputModel(input) {
488
+ const canonical = canonicalizeIdeaRoleOutputModel(input);
489
+ if (!canonical || typeof canonical !== 'object' || Array.isArray(canonical))
490
+ return canonical;
491
+ const output = canonical;
492
+ if (!Array.isArray(output.evidence) || !Array.isArray(output.positions))
493
+ return canonical;
494
+ const evidence = output.evidence
495
+ .map((item) => EvidenceCardBase.strip().superRefine(checkEvidenceCard).safeParse(item))
496
+ .flatMap((result) => (result.success ? [result.data] : []));
497
+ const keptEvidence = new Set(evidence.map((card) => card.id));
498
+ const parsedPositions = output.positions
499
+ .map((item) => ClaimPosition.strip().safeParse(item))
500
+ .flatMap((result) => (result.success ? [result.data] : []));
501
+ if (parsedPositions.length === 0)
502
+ return canonical; // empty claim set → let strict validation fail it
503
+ const keptPositions = new Set(parsedPositions.map((position) => position.local_id));
504
+ const positions = parsedPositions.map((position) => ({
505
+ ...position,
506
+ evidence_ids: position.evidence_ids.filter((id) => keptEvidence.has(id)),
507
+ depends_on: position.depends_on.filter((id) => keptPositions.has(id)),
508
+ }));
509
+ const scrubIds = (value, key) => {
510
+ if (!Array.isArray(value))
511
+ return value;
512
+ return value.map((item) => {
513
+ if (!item || typeof item !== 'object' || Array.isArray(item))
514
+ return item;
515
+ const entry = item;
516
+ if (!Array.isArray(entry[key]))
517
+ return item;
518
+ return { ...entry, [key]: entry[key].filter((id) => keptPositions.has(id)) };
519
+ });
520
+ };
521
+ const parseOrDrop = (value, parse) => Array.isArray(value)
522
+ ? value.map((item) => parse(item)).flatMap((result) => (result.success ? [result.data] : []))
523
+ : value;
524
+ const calculations = Array.isArray(output.calculations)
525
+ ? parseOrDrop(output.calculations, (item) => CalculationLedger.strip().safeParse(item))
526
+ .filter((calc) => keptPositions.has(calc.claim_id)
527
+ && calc.inputs.every((input) => input.evidence_ids.every((id) => keptEvidence.has(id))))
528
+ .slice(0, 8)
529
+ : output.calculations;
530
+ return {
531
+ ...output,
532
+ evidence,
533
+ positions,
534
+ calculations,
535
+ coverage: parseOrDrop(scrubIds(output.coverage, 'position_ids'), (item) => CoverageEntryBase.strip().superRefine(checkCoverageEntry).safeParse(item)),
536
+ decision_questions: parseOrDrop(scrubIds(output.decision_questions, 'claim_ids'), (item) => DecisionQuestion.strip().safeParse(item)),
537
+ };
538
+ }
539
+ /** The model-facing S4 shape. Exact known enum aliases are canonicalized, then the strict schema validates
540
+ * the full output; persisted `IdeaRoleOutput` remains canonical-only. */
541
+ export const IdeaRoleOutputModel = z.preprocess(canonicalizeIdeaRoleOutputModel, StrictIdeaRoleOutputModel);
174
542
  // ── S3: StagePrompts (§9, §13) ──────────────────────────────────────────────
175
543
  //
176
544
  // S3 output: the role-specific S4 prompts with every {{SLOT}} filled. Deterministic validator
@@ -191,10 +559,59 @@ export const ClaimGroups = z
191
559
  groups: z.array(z.array(z.string().min(1)).min(2)),
192
560
  })
193
561
  .strict();
562
+ // ── R2: typed decision graph ────────────────────────────────────────────────
563
+ const GraphPosition = ClaimPosition.extend({
564
+ id: z.string().min(1),
565
+ provider: ProviderIdSchema,
566
+ source_id: z.string().min(1),
567
+ });
568
+ const GraphEvidence = EvidenceCardBase.extend({
569
+ id: z.string().min(1),
570
+ provider: ProviderIdSchema,
571
+ source_id: z.string().min(1),
572
+ }).superRefine(checkEvidenceCard);
573
+ const GraphCalculation = CalculationLedger.extend({
574
+ id: z.string().min(1),
575
+ claim_id: z.string().min(1),
576
+ provider: ProviderIdSchema,
577
+ source_id: z.string().min(1),
578
+ });
579
+ export const CalculationCheck = z.object({
580
+ calculation_id: z.string().min(1),
581
+ claim_id: z.string().min(1),
582
+ status: z.enum(['PASS', 'FAIL']),
583
+ issues: z.array(z.string()),
584
+ }).strict();
585
+ export const DecisionClaim = z.object({
586
+ id: z.string().min(1),
587
+ proposition: z.string().min(1),
588
+ position_ids: z.array(z.string().min(1)).min(1),
589
+ state: z.enum(['CONSENSUS', 'SHARED_CONCERN', 'DISAGREEMENT', 'UNIQUE', 'UNCERTAIN']),
590
+ evidence_state: z.enum(['SUPPORTED', 'CONFLICTED', 'UNVERIFIED']),
591
+ load_bearing: z.boolean(),
592
+ if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
593
+ sensitivity: z.enum(['DECISIVE', 'MATERIAL', 'LOW']),
594
+ });
595
+ export const DecisionGraph = z.object({
596
+ positions: z.array(GraphPosition),
597
+ evidence: z.array(GraphEvidence),
598
+ calculations: z.array(GraphCalculation).default([]),
599
+ calculation_checks: z.array(CalculationCheck).default([]),
600
+ claims: z.array(DecisionClaim),
601
+ edges: z.array(z.object({
602
+ from: z.string().min(1),
603
+ to: z.string().min(1),
604
+ type: z.enum(['DEPENDS_ON', 'SUPPORTS', 'ATTACKS', 'CONTRADICTS']),
605
+ })),
606
+ holes: z.object({
607
+ coverage: z.array(z.object({ dimension_id: z.string().min(1), label: z.string().min(1) })),
608
+ evidence: z.array(z.object({ claim_id: z.string().min(1), reason: z.string().min(1) })),
609
+ }),
610
+ });
194
611
  // ── S8: Verification (§13) ──────────────────────────────────────────────────
195
612
  //
196
613
  // `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).
614
+ // actual S8 stage output. Each item is judged independently; no verdict distribution is required.
198
615
  export const Verification = z
199
616
  .object({
200
617
  target_id: z.string().min(1),
@@ -206,10 +623,65 @@ export const Verification = z
206
623
  export const VerificationSet = z
207
624
  .object({
208
625
  verifications: z.array(Verification),
209
- // Required by the S8 prompt only when the verifier issued zero REFUTEs (§13).
626
+ // Accepted for backward compatibility with pre-R1 artifacts; the R1 prompt no longer requests it.
210
627
  all_confirmed_justification: z.string().optional(),
211
628
  })
212
629
  .strict();
630
+ export const ClaimVerification = z.object({
631
+ claim_id: z.string().min(1),
632
+ status: z.enum(['VERIFIED', 'PARTIAL', 'CONTRADICTED', 'UNVERIFIABLE']),
633
+ reasoning: z.string().min(1),
634
+ evidence_ids: z.array(z.string().min(1)),
635
+ calculation_check: z.enum(['PASS', 'FAIL', 'NOT_APPLICABLE']).optional(),
636
+ missing_evidence: z.array(z.string().min(1)),
637
+ }).strict();
638
+ export const ClaimVerificationSet = z.object({
639
+ verifications: z.array(ClaimVerification),
640
+ }).strict();
641
+ // ── R5: bounded, append-only rebuttal events ───────────────────────────────
642
+ const RebuttalResponseBase = z.object({
643
+ claim_id: z.string().min(1),
644
+ response: z.enum(['CONCEDE', 'COUNTER', 'NARROW', 'UNRESOLVED']),
645
+ reasoning: z.string().min(1),
646
+ evidence_ids: z.array(z.string().min(1)),
647
+ narrowed_proposition: z.string().min(1).optional(),
648
+ }).strict();
649
+ const checkNarrowedRebuttal = (event, ctx) => {
650
+ if (event.response === 'NARROW' && !event.narrowed_proposition) {
651
+ ctx.addIssue({
652
+ code: z.ZodIssueCode.custom,
653
+ path: ['narrowed_proposition'],
654
+ message: 'NARROW requires narrowed_proposition',
655
+ });
656
+ }
657
+ };
658
+ export const RebuttalResponse = RebuttalResponseBase.superRefine(checkNarrowedRebuttal);
659
+ /** Exact model output for one scout's single rebuttal round. */
660
+ export const RebuttalResponseSet = z.object({
661
+ events: z.array(RebuttalResponse).max(3),
662
+ }).strict();
663
+ export const RebuttalEvent = RebuttalResponseBase.extend({
664
+ id: z.string().min(1),
665
+ round: z.literal(1),
666
+ responder: ProviderIdSchema,
667
+ target_position_ids: z.array(z.string().min(1)),
668
+ }).superRefine(checkNarrowedRebuttal);
669
+ /** Persisted separately from DecisionGraph so original claims/evidence remain immutable. */
670
+ export const RebuttalEventSet = z.object({
671
+ round: z.literal(1),
672
+ selected_claim_ids: z.array(z.string().min(1)).max(3),
673
+ events: z.array(RebuttalEvent).max(6),
674
+ stop_reason: z.enum([
675
+ 'NO_ESCALATIONS',
676
+ 'NO_ELIGIBLE_SCOUT',
677
+ 'BUDGET_RESERVED',
678
+ 'ROUND_COMPLETE',
679
+ 'NO_NEW_EVIDENCE',
680
+ 'CALL_CAP_REACHED',
681
+ ]),
682
+ }).strict();
683
+ /** Shared artifact slot: code review keeps the v1 cross-exam shape; idea refinement uses R4 claims. */
684
+ export const VerificationArtifact = z.union([VerificationSet, ClaimVerificationSet]);
213
685
  // ── S7: DisagreementMap (§7, §9) ────────────────────────────────────────────
214
686
  //
215
687
  // The plan (§7/§9) names only the four arrays. `Claim` shape is from §6. NOTE two under-specified
@@ -280,16 +752,69 @@ const Adjudication = z
280
752
  id: z.string().min(1), // disputed item id
281
753
  ruling: z.enum(['UPHOLD', 'REJECT', 'UNRESOLVED']),
282
754
  reasoning: z.string().min(1), // ≤3 sentences
283
- evidence_cited: z.string().min(1),
755
+ evidence_cited: z.string().min(1).optional(), // code-review / legacy idea artifacts
756
+ evidence_ids: z.array(z.string().min(1)).optional(), // R4 idea chair citations, validated by reference
757
+ effect_on_decision: z.string().min(1).optional(), // R5 idea chair; optional for legacy/code-review artifacts
758
+ what_would_change_it: z.string().min(1).optional(), // required by S9 when an idea ruling is UNRESOLVED
284
759
  })
285
- .strict();
760
+ .strict()
761
+ .refine((item) => item.evidence_cited || item.evidence_ids?.length, { message: 'adjudication requires evidence' });
286
762
  export const Recommendation = z.enum(['PROCEED', 'PROCEED_WITH_CONDITIONS', 'PIVOT', 'STOP']);
763
+ const DecisionSnapshotItem = z.object({
764
+ label: z.string().min(1),
765
+ value: z.string().min(1),
766
+ meaning: z.string().min(1),
767
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
768
+ }).strict();
769
+ const DecisionOption = z.object({
770
+ label: z.string().min(1),
771
+ commitment: z.string().min(1),
772
+ commitment_kind: z.enum(['KNOWN', 'TARGET_CAP', 'UNKNOWN']),
773
+ tradeoff: z.string().min(1),
774
+ claim_ids: z.array(z.string().min(1)).max(4),
775
+ }).strict().superRefine((option, ctx) => {
776
+ if (option.commitment_kind !== 'UNKNOWN' && option.claim_ids.length === 0) {
777
+ ctx.addIssue({
778
+ code: z.ZodIssueCode.custom,
779
+ path: ['claim_ids'],
780
+ message: 'known commitments and target caps require a graph claim anchor',
781
+ });
782
+ }
783
+ });
784
+ /** Reader-first, graph-anchored numeric comparison for decision tasks where amounts or thresholds matter. */
785
+ export const DecisionSnapshot = z.object({
786
+ decisive_numbers: z.array(DecisionSnapshotItem).min(1).max(5),
787
+ payback: z.object({
788
+ status: z.enum(['ACHIEVED', 'NOT_ACHIEVED', 'NOT_COMPUTABLE']),
789
+ result: z.string().min(1),
790
+ basis: z.string().min(1),
791
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
792
+ }).strict().optional(),
793
+ options: z.array(DecisionOption).min(2).max(4),
794
+ tripwire: z.object({
795
+ metric: z.string().min(1),
796
+ threshold: z.string().min(1),
797
+ decision_rule: z.string().min(1),
798
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
799
+ }).strict().optional(),
800
+ }).strict();
287
801
  const JudgeReportBase = z
288
802
  .object({
289
803
  adjudications: z.array(Adjudication),
290
804
  verdict: z.string().min(1), // the recommendation + core reason (idea: 2-5 sentences; grounded in adjudicated + consensus claims)
291
805
  recommendation: Recommendation.optional(), // idea workflow; code-review omits it
292
806
  conditions: z.array(z.string().min(1)).max(6).optional(), // present only for PROCEED_WITH_CONDITIONS
807
+ recommendation_claim_ids: z.array(z.string().min(1)).min(1).max(8).optional(),
808
+ condition_claim_ids: z.array(z.string().min(1)).min(1).max(8).optional(),
809
+ pivot: z.object({
810
+ changed_claim_id: z.string().min(1),
811
+ new_risk_claim_id: z.string().min(1),
812
+ }).strict().optional(),
813
+ strongest_counter_case: z.object({
814
+ claim_ids: z.array(z.string().min(1)).min(1).max(4),
815
+ reasoning: z.string().min(1),
816
+ }).strict().optional(),
817
+ decision_snapshot: DecisionSnapshot.optional(),
293
818
  key_points: z.array(z.string()).max(10).optional(), // chairman's bulleted reasoning (idea workflow); code-review omits it
294
819
  dissent: z.array(z.string()).min(1), // ≥1 — empty dissent is invalid (§9); strongest counter-argument
295
820
  confidence_notes: z.string().min(1), // which conclusions are HIGH/MEDIUM/LOW and why
@@ -319,7 +844,61 @@ export const JudgeReport = JudgeReportBase.superRefine((r, ctx) => {
319
844
  export const JudgeReportModel = JudgeReportBase.extend({
320
845
  dissent: z.array(z.string()),
321
846
  });
847
+ /** R5 idea-chair boundary. Persisted JudgeReport keeps its legacy adjudication names so code-review
848
+ * and old run readers stay compatible; S9 translates only after this exact model shape validates. */
849
+ export const IdeaChairRuling = z.object({
850
+ claim_id: z.string().min(1),
851
+ ruling: z.enum(['HOLDS', 'FAILS', 'UNRESOLVED']),
852
+ reasoning: z.string().min(1),
853
+ evidence_ids: z.array(z.string().min(1)).min(1),
854
+ effect_on_decision: z.string().min(1),
855
+ what_would_change_it: z.string().min(1).optional(),
856
+ }).strict().superRefine((ruling, ctx) => {
857
+ if (ruling.ruling === 'UNRESOLVED' && !ruling.what_would_change_it) {
858
+ ctx.addIssue({
859
+ code: z.ZodIssueCode.custom,
860
+ path: ['what_would_change_it'],
861
+ message: 'UNRESOLVED requires what_would_change_it',
862
+ });
863
+ }
864
+ });
865
+ export const IdeaChairReportModel = JudgeReportModel.omit({ adjudications: true }).extend({
866
+ adjudications: z.array(IdeaChairRuling),
867
+ });
322
868
  // ── S9b: ActionPlan (idea-refinement report v3) ─────────────────────────────
869
+ export const FeatureBacklog = z.object({
870
+ must: z.array(z.object({
871
+ feature: z.string().min(1),
872
+ user_value: z.string().min(1),
873
+ rationale: z.string().min(1),
874
+ effort: z.enum(['S', 'M', 'L']),
875
+ }).strict()).min(1).max(6),
876
+ should: z.array(z.object({
877
+ feature: z.string().min(1),
878
+ user_value: z.string().min(1),
879
+ rationale: z.string().min(1),
880
+ effort: z.enum(['S', 'M', 'L']),
881
+ }).strict()).max(6),
882
+ later: z.array(z.object({
883
+ feature: z.string().min(1),
884
+ user_value: z.string().min(1),
885
+ rationale: z.string().min(1),
886
+ effort: z.enum(['S', 'M', 'L']),
887
+ }).strict()).max(6),
888
+ wont: z.array(z.object({
889
+ feature: z.string().min(1),
890
+ reason: z.string().min(1),
891
+ }).strict()).max(6),
892
+ }).strict();
893
+ export const ImplementationPlan = z.object({
894
+ milestones: z.array(z.object({
895
+ order: z.number().int().min(1),
896
+ timebox: z.string().min(1),
897
+ outcome: z.string().min(1),
898
+ tasks: z.array(z.string().min(1)).min(1).max(6),
899
+ acceptance_test: z.string().min(1),
900
+ }).strict()).min(1).max(7),
901
+ }).strict();
323
902
  export const ActionPlan = z
324
903
  .object({
325
904
  actions: z
@@ -336,8 +915,35 @@ export const ActionPlan = z
336
915
  .min(1)
337
916
  .max(7),
338
917
  sequencing_note: z.string().min(1),
918
+ feature_backlog: FeatureBacklog.optional(),
919
+ implementation_plan: ImplementationPlan.optional(),
339
920
  })
340
921
  .strict();
922
+ export const PlannerUnavailable = z.object({
923
+ kind: z.literal('PlannerUnavailable'),
924
+ reason: z.enum(['budget_exhausted', 'planner_failed']),
925
+ unresolved_questions: z.array(z.string().min(1)).min(1).max(10),
926
+ }).strict();
927
+ export const ActionPlanArtifact = z.union([ActionPlan, PlannerUnavailable]);
928
+ /** R6 quick mode: one strong analyst produces the analysis, recommendation, and plan in one call. */
929
+ export const QuickDecisionModel = z.object({
930
+ analysis: IdeaRoleOutputModel,
931
+ verdict: z.string().min(1),
932
+ recommendation: Recommendation,
933
+ conditions: z.array(z.string().min(1)).max(6),
934
+ key_points: z.array(z.string().min(1)).min(2).max(8),
935
+ dissent: z.array(z.string().min(1)).min(1).max(4),
936
+ confidence_notes: z.string().min(1),
937
+ action_plan: ActionPlan,
938
+ }).strict().superRefine((decision, ctx) => {
939
+ const hasConditions = decision.conditions.length > 0;
940
+ if (decision.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
941
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['conditions'], message: 'conditions are required for PROCEED_WITH_CONDITIONS' });
942
+ }
943
+ if (decision.recommendation !== 'PROCEED_WITH_CONDITIONS' && hasConditions) {
944
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['conditions'], message: 'conditions are only valid for PROCEED_WITH_CONDITIONS' });
945
+ }
946
+ });
341
947
  // ── RunMeta (§15, §16) ──────────────────────────────────────────────────────
342
948
  //
343
949
  // Written by the artifact writer; assembled by the engine's RunCtx (T5). Internal → not strict.
@@ -345,6 +951,7 @@ export const ActionPlan = z
345
951
  export const CallRecord = z.object({
346
952
  provider: ProviderIdSchema,
347
953
  stage: z.string(), // e.g. "S4", "S1"
954
+ category: z.enum(['discovery', 'verification', 'repair', 'planning']).optional(),
348
955
  durationMs: z.number().nonnegative(),
349
956
  error: z.enum(['NOT_FOUND', 'AUTH', 'QUOTA', 'TIMEOUT', 'BAD_OUTPUT', 'CRASH']).optional(),
350
957
  });
@@ -360,6 +967,7 @@ const FlagProfileSchema = z.object({
360
967
  export const RunMeta = z.object({
361
968
  run_id: z.string().min(1), // encodes the timestamp (e.g. 20260702-1412-idea-refinement-a3f9)
362
969
  workflow: WorkflowIdSchema,
970
+ mode: IdeaModeSchema.optional(),
363
971
  provider_versions: z.record(ProviderIdSchema, z.string()), // detected `--version` strings
364
972
  flag_profiles: z.record(ProviderIdSchema, FlagProfileSchema),
365
973
  roles: z.record(z.string(), ProviderIdSchema), // role name → assigned provider
@@ -367,8 +975,23 @@ export const RunMeta = z.object({
367
975
  calls: z.array(CallRecord),
368
976
  call_count: z.number().int().nonnegative(),
369
977
  budget: z.object({ limit: z.number().int().positive(), used: z.number().int().nonnegative() }),
978
+ receipt: z.object({
979
+ discovery: z.number().int().nonnegative(),
980
+ verification: z.number().int().nonnegative(),
981
+ repair: z.number().int().nonnegative(),
982
+ planning: z.number().int().nonnegative(),
983
+ }).optional(),
370
984
  exit_status: z.enum(['ok', 'failed', 'aborted', 'partial']),
371
985
  aborted: z.boolean(), // §16: Ctrl+C finalizes meta with aborted:true
372
986
  // §16 report-header flags; absent = none.
373
- flags: z.array(z.enum(['synthesis_suspect', 'low_diversity', 'plan_skipped', 'plan_fallback'])).optional(),
987
+ flags: z.array(z.enum([
988
+ 'synthesis_suspect',
989
+ 'low_diversity',
990
+ 'plan_skipped',
991
+ 'plan_fallback',
992
+ 'headless_intent',
993
+ 'verification_skipped',
994
+ 'research_ungrounded',
995
+ 'single_model',
996
+ ])).optional(),
374
997
  });