aiki-cli 0.3.1 → 0.3.2

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 (31) hide show
  1. package/CHANGELOG.md +12 -5
  2. package/README.md +9 -11
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +1 -1
  6. package/dist/council/view.js +52 -12
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +417 -88
  9. package/dist/orchestration/decision-graph.js +31 -0
  10. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  11. package/dist/orchestration/modes.js +15 -9
  12. package/dist/orchestration/preflight.js +17 -3
  13. package/dist/orchestration/quick-analysis.js +21 -4
  14. package/dist/orchestration/stages/s10-render.js +167 -79
  15. package/dist/orchestration/stages/s4-analyze.js +3 -1
  16. package/dist/orchestration/stages/s6-positions.js +18 -0
  17. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  18. package/dist/orchestration/stages/s8-verify.js +26 -9
  19. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  20. package/dist/orchestration/stages/s9-judge.js +36 -13
  21. package/dist/orchestration/stages/s9b-plan.js +192 -42
  22. package/dist/schemas/index.js +67 -3
  23. package/dist/skills/idea-refinement/analyst.md +5 -5
  24. package/dist/skills/idea-refinement/chair.md +18 -0
  25. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  26. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  27. package/dist/skills/idea-refinement/planner.md +25 -19
  28. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  29. package/dist/skills/idea-refinement/verifier.md +17 -0
  30. package/dist/workflows/idea-refinement.js +42 -14
  31. package/package.json +1 -1
@@ -1,12 +1,13 @@
1
1
  // S9b — idea validation plan. This is the one report-v3 model call after the judge: it turns the
2
2
  // adjudicated risks, blind spots, and open questions into anchored validation actions. Rendering stays
3
3
  // deterministic; if the planner fails or produces unanchored actions, we write flagged unavailability.
4
- import { ActionPlan, FeatureBacklog, ImplementationPlan } from '../../schemas/index.js';
4
+ import { ActionPlan, FeatureBacklog, ImplementationPlan, ReaderBrief, readerBriefIssues } from '../../schemas/index.js';
5
5
  import { z } from 'zod';
6
6
  import { BudgetExceeded, isFatal } from '../context.js';
7
7
  import { jsonCall } from '../jsonStage.js';
8
8
  import { loadSkill } from '../skills.js';
9
9
  import { mergeOpenQuestions } from './s10-render.js';
10
+ import { interpretClaimOutcome } from '../decision-graph.js';
10
11
  const PlannerOutput = z.object({
11
12
  actions: z.array(z.object({
12
13
  order: z.number().int().min(1).optional(),
@@ -15,25 +16,46 @@ const PlannerOutput = z.object({
15
16
  validates: z.string().min(1),
16
17
  effort: z.string().min(1).optional(),
17
18
  kill_signal: z.string().min(1),
18
- }).strict()).min(1).max(7),
19
+ }).strict()).max(7),
19
20
  sequencing_note: z.string().min(1),
20
21
  feature_backlog: FeatureBacklog.optional(),
21
22
  implementation_plan: ImplementationPlan.optional(),
22
- }).strict();
23
- const S9B_PROMPT = `ROLE: Validation planner. You write the requested practical outputs after an idea
24
- council has debated and judged the idea. The actions list remains validation work: test unsettled risks,
25
- blind spots, or open questions, with the cheapest decisive test first.{{SKILL}}
23
+ reader_brief: ReaderBrief.optional(),
24
+ }).strict().superRefine((plan, ctx) => {
25
+ if (plan.actions.length === 0 && !plan.reader_brief) {
26
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['actions'], message: 'an empty planner answer requires reader_brief' });
27
+ }
28
+ });
29
+ const S9B_PROMPT = `ROLE: User answer editor and action planner. The council has already done the analysis.
30
+ Turn its strongest supported findings and both scouts' proposals into a useful answer to the original user.{{SKILL}}
26
31
 
27
32
  Output ONLY JSON matching the ActionPlan schema:
28
- - actions: 1-7 ordered actions, each imperative and concrete.
33
+ - reader_brief is REQUIRED. It contains:
34
+ - headline: a concrete answer, not a report label.
35
+ - bottom_line: the direct recommendation and why, with no process talk.
36
+ - sections: 2-6 useful sections {heading,summary,bullets}; synthesize the requested deliverables and explain
37
+ why standout ideas matter. Prefer concrete product language over due-diligence language. Explain the verdict,
38
+ selection logic, and trade-offs; do not restate the feature backlog or milestone list.
39
+ - next_step: one action the user should take next.
40
+ - caveats: at most 3 honest limitations that materially affect the answer.
41
+ - source_ids: only ids present in CONTEXT.sources; use [] when no source supports the reader answer.
42
+ - reader_brief must never mention graph/claim ids, verification enums, evidence coverage, structural scoring,
43
+ provider-call mechanics, or the fact that an answer editor assembled it.
44
+ - CONTEXT.chair is decision reasoning, not factual proof. Treat its recommendation, rationale, conditions, and
45
+ claim outcomes as judgment. Do not call a proposition supported, verified, proven, or factual unless the same
46
+ proposition appears in supported_findings; otherwise frame it as a recommendation, risk, or hypothesis.
47
+ - actions: 1-4 ordered validation actions, each imperative and concrete.
29
48
  - validates MUST anchor to one of:
30
49
  - a graph claim id from upheld_risks, e.g. "G3"
31
50
  - a blind spot label as "blind:<label>"
32
51
  - an open-question prefix as "Q:<question prefix>"
33
52
  - why ties the action to the risk, blind spot, or question.
34
53
  - kill_signal is the result that should stop or reshape the idea.
35
- - Preserve the chair's numeric distinctions: operating break-even is not capital payback, and a target cap
36
- is not a known cost. Do not introduce or reinterpret a number that is absent from decision_snapshot.
54
+ - Preserve CONTEXT.chair's numeric distinctions: operating break-even is not capital payback, and a target cap
55
+ is not a known cost. Do not introduce or reinterpret a number that is absent from CONTEXT.chair.decision_snapshot.
56
+ - CONTEXT.as_of_date is the evidence snapshot date, not a deadline. Compare it with any sourced deadline.
57
+ - Treat stated deadlines and available time as hard boundaries. When CONTEXT has no numeric deadline or capacity,
58
+ do not invent a day-count calendar; use ordered phases with explicit acceptance tests.
37
59
  - sequencing_note explains why this order is cheapest and decisive.
38
60
  - Read requested_outputs in CONTEXT. When it includes FEATURE_BACKLOG, include feature_backlog with
39
61
  must/should/later items {feature,user_value,rationale,effort:S|M|L} and wont items {feature,reason}.
@@ -41,6 +63,8 @@ Output ONLY JSON matching the ActionPlan schema:
41
63
  - When requested_outputs includes IMPLEMENTATION_PLAN, include implementation_plan.milestones with
42
64
  {order,timebox,outcome,tasks,acceptance_test}. This is a concrete build sequence, not validation prose.
43
65
  - Do not omit a requested output and do not invent an unrequested product surface.
66
+ - Use deliverable_proposals from BOTH seats as options, then select and improve the strongest coherent set.
67
+ - Treat supported_findings and sources as factual boundaries. Proposals are product judgment, not proof.
44
68
 
45
69
  CONTEXT: {{CONTEXT_JSON}}`;
46
70
  function clip(s, n) {
@@ -53,19 +77,21 @@ function sevRank(s) {
53
77
  function norm(s) {
54
78
  return s.trim().toLowerCase();
55
79
  }
56
- function unresolvedRisks(graph, judgeReport) {
80
+ export function unresolvedRisks(graph, judgeReport) {
57
81
  const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
58
82
  const rulingById = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
59
83
  const risks = graph.claims
60
84
  .map((claim) => {
61
85
  const adj = rulingById.get(claim.id);
62
- if (adj?.ruling !== 'UPHOLD')
86
+ const outcome = interpretClaimOutcome(graph, claim, adj);
87
+ if ((!adj && outcome.decisionEffect !== 'FAILED')
88
+ || (outcome.propositionTruth === 'HOLDS' && outcome.decisionEffect === 'HELD'))
63
89
  return null;
64
90
  return {
65
91
  id: claim.id,
66
92
  assumption: claim.proposition,
67
93
  severity: claim.sensitivity === 'DECISIVE' ? 'HIGH' : claim.sensitivity === 'MATERIAL' ? 'MED' : 'LOW',
68
- reasoning: adj.reasoning,
94
+ reasoning: adj?.reasoning ?? 'This supported concern works against the decision.',
69
95
  };
70
96
  })
71
97
  .filter((risk) => risk !== null);
@@ -85,6 +111,96 @@ function unresolvedRisks(graph, judgeReport) {
85
111
  }
86
112
  return risks.sort((a, b) => sevRank(a.severity) - sevRank(b.severity));
87
113
  }
114
+ const ANSWER_MATERIAL_FLAGS = new Set([
115
+ 'synthesis_suspect', 'low_diversity', 'weak_seat', 'deliverable_gap', 'headless_intent',
116
+ 'verification_skipped', 'research_ungrounded', 'source_fallback_search',
117
+ ]);
118
+ function safePublicUrl(raw) {
119
+ if (!raw)
120
+ return undefined;
121
+ try {
122
+ const url = new URL(raw);
123
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : undefined;
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ /** Canonical, decision-relevant input to the existing S9b answer-editor call. */
130
+ export function buildAnswerContext(input) {
131
+ const { contract, graph, judgeReport, seats } = input;
132
+ const evidenceDates = graph.evidence
133
+ .filter((evidence) => evidence.source_kind === 'PRIMARY' || evidence.source_kind === 'SECONDARY')
134
+ .map((evidence) => evidence.accessed_at?.match(/^\d{4}-\d{2}-\d{2}/)?.[0])
135
+ .filter((date) => Boolean(date))
136
+ .sort();
137
+ const asOfDate = evidenceDates.at(-1);
138
+ const requestedOutputs = contract.requested_outputs ?? ['DECISION'];
139
+ const adjudicationById = new Map(judgeReport.adjudications.map((item) => [item.id, item]));
140
+ const outcomes = graph.claims.map((claim) => {
141
+ const adjudication = adjudicationById.get(claim.id);
142
+ const outcome = interpretClaimOutcome(graph, claim, adjudication);
143
+ return {
144
+ id: claim.id,
145
+ proposition: claim.proposition,
146
+ proposition_truth: outcome.propositionTruth,
147
+ decision_effect: outcome.decisionEffect,
148
+ ...(adjudication ? { reasoning: adjudication.reasoning } : {}),
149
+ };
150
+ });
151
+ const outcomeById = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
152
+ const deliverableProposals = seats.flatMap((seat) => {
153
+ const seatId = seat.sample ?? seat.provider;
154
+ return (seat.output.deliverable_proposals ?? []).map((proposal) => ({
155
+ ...proposal,
156
+ provider: seat.provider,
157
+ seat_id: seatId,
158
+ evidence_ids: [...new Set(proposal.evidence_ids.flatMap((localId) => graph.evidence
159
+ .filter((evidence) => evidence.provider === seat.provider
160
+ && evidence.source_id === seatId
161
+ && evidence.id === `${seatId}/${localId}`)
162
+ .map((evidence) => evidence.id)))],
163
+ }));
164
+ });
165
+ return {
166
+ original_request: input.originalRequest,
167
+ ...(asOfDate ? { as_of_date: asOfDate } : {}),
168
+ task: contract.task,
169
+ constraints: contract.constraints,
170
+ success_criteria: contract.success_criteria,
171
+ requested_outputs: requestedOutputs,
172
+ chair: {
173
+ epistemic_status: 'DECISION_REASONING_NOT_FACT',
174
+ recommendation: judgeReport.recommendation,
175
+ decision_reasoning: judgeReport.verdict,
176
+ rationale: judgeReport.key_points ?? [],
177
+ decision_conditions: judgeReport.conditions ?? [],
178
+ decision_snapshot: judgeReport.decision_snapshot,
179
+ claim_outcomes: outcomes,
180
+ },
181
+ upheld_risks: unresolvedRisks(graph, judgeReport),
182
+ blind_spots: graph.holes.coverage.map((hole) => hole.label),
183
+ open_questions: mergeOpenQuestions(seats),
184
+ deliverable_proposals: deliverableProposals,
185
+ supported_findings: graph.claims
186
+ .filter((claim) => claim.nature === 'FACTUAL'
187
+ && claim.evidence_state === 'SUPPORTED'
188
+ && outcomeById.get(claim.id)?.proposition_truth === 'HOLDS')
189
+ .map((claim) => ({ id: claim.id, finding: claim.proposition })),
190
+ material_flags: [...input.flags].filter((flag) => ANSWER_MATERIAL_FLAGS.has(flag)),
191
+ sources: graph.evidence.map((evidence) => {
192
+ const url = safePublicUrl(evidence.url) ?? safePublicUrl(evidence.locator);
193
+ return {
194
+ id: evidence.id,
195
+ kind: evidence.source_kind,
196
+ ...(evidence.title ? { title: evidence.title } : {}),
197
+ ...(url ? { url } : {}),
198
+ ...(evidence.accessed_at ? { accessed_at: evidence.accessed_at } : {}),
199
+ supports: evidence.claim_supported,
200
+ };
201
+ }),
202
+ };
203
+ }
88
204
  export function buildActionPlannerPrompt(input, skill) {
89
205
  return S9B_PROMPT
90
206
  .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
@@ -110,22 +226,41 @@ export function validAnchor(anchor, anchors) {
110
226
  }
111
227
  return false;
112
228
  }
113
- export function anchoredActionPlan(plan, anchors, requestedOutputs = []) {
229
+ function normalizeSourceIds(ids, allowed) {
230
+ const normalized = ids.flatMap((id) => {
231
+ if (allowed.includes(id))
232
+ return [id];
233
+ const matches = allowed.filter((known) => known.endsWith(`/${id}`));
234
+ return matches.length === 1 ? matches : [];
235
+ });
236
+ return [...new Set(normalized)];
237
+ }
238
+ export function anchoredActionPlan(plan, anchors, requestedOutputs = [], requireReaderBrief = false) {
239
+ if (requestedOutputs.includes('FEATURE_BACKLOG') && !plan.feature_backlog)
240
+ return null;
241
+ if (requestedOutputs.includes('IMPLEMENTATION_PLAN') && !plan.implementation_plan)
242
+ return null;
243
+ if (requireReaderBrief && !plan.reader_brief)
244
+ return null;
245
+ const readerBrief = plan.reader_brief ? {
246
+ ...plan.reader_brief,
247
+ source_ids: normalizeSourceIds(plan.reader_brief.source_ids, anchors.sourceIds),
248
+ } : undefined;
249
+ if (readerBrief && readerBriefIssues(readerBrief, anchors.knownReaderIds ?? anchors.claimIds).length)
250
+ return null;
114
251
  const actions = plan.actions
115
252
  .filter((a) => validAnchor(a.validates, anchors))
116
253
  .sort((a, b) => a.order - b.order)
254
+ .slice(0, requireReaderBrief ? 4 : 7)
117
255
  .map((a, i) => ({ ...a, order: i + 1 }));
118
- if (actions.length === 0)
119
- return null;
120
- if (requestedOutputs.includes('FEATURE_BACKLOG') && !plan.feature_backlog)
121
- return null;
122
- if (requestedOutputs.includes('IMPLEMENTATION_PLAN') && !plan.implementation_plan)
256
+ if (actions.length === 0 && !requireReaderBrief)
123
257
  return null;
124
258
  return {
125
259
  actions,
126
260
  sequencing_note: plan.sequencing_note,
127
261
  ...(requestedOutputs.includes('FEATURE_BACKLOG') && plan.feature_backlog ? { feature_backlog: plan.feature_backlog } : {}),
128
262
  ...(requestedOutputs.includes('IMPLEMENTATION_PLAN') && plan.implementation_plan ? { implementation_plan: plan.implementation_plan } : {}),
263
+ ...(readerBrief ? { reader_brief: readerBrief } : {}),
129
264
  };
130
265
  }
131
266
  export function normalizeEffort(raw) {
@@ -140,7 +275,19 @@ export function normalizeEffort(raw) {
140
275
  const days = amount * (unit === 'minute' ? 1 / 1440 : unit === 'hour' ? 1 / 24 : unit === 'day' ? 1 : unit === 'week' ? 7 : unit === 'month' ? 30 : 365);
141
276
  return days <= 2 ? 'S' : days <= 14 ? 'M' : 'L';
142
277
  }
143
- export function normalizePlannerOutput(plan) {
278
+ const CALENDAR_TIMEBOX = /\b(?:days?|weeks?|months?|years?)\b|\b\d{4}-\d{2}-\d{2}\b|\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b/i;
279
+ const EXPLICIT_SCHEDULE = /\b(?:\d{4}-\d{2}-\d{2}|\d+(?:\.\d+)?\s*[-–]?\s*(?:hours?|days?|weeks?|months?|years?)|(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4})?)\b/i;
280
+ function hasExplicitSchedule(context) {
281
+ return EXPLICIT_SCHEDULE.test(JSON.stringify({
282
+ original_request: context.original_request,
283
+ constraints: context.constraints,
284
+ success_criteria: context.success_criteria,
285
+ chair: context.chair,
286
+ supported_findings: context.supported_findings,
287
+ sources: context.sources.map((source) => source.supports),
288
+ }));
289
+ }
290
+ export function normalizePlannerOutput(plan, calendarAllowed = true) {
144
291
  return ActionPlan.parse({
145
292
  actions: plan.actions.map((action, i) => ({
146
293
  ...action,
@@ -149,7 +296,13 @@ export function normalizePlannerOutput(plan) {
149
296
  })),
150
297
  sequencing_note: plan.sequencing_note,
151
298
  ...(plan.feature_backlog ? { feature_backlog: plan.feature_backlog } : {}),
152
- ...(plan.implementation_plan ? { implementation_plan: plan.implementation_plan } : {}),
299
+ ...(plan.implementation_plan ? { implementation_plan: {
300
+ milestones: plan.implementation_plan.milestones.map((milestone, index) => ({
301
+ ...milestone,
302
+ timebox: !calendarAllowed && CALENDAR_TIMEBOX.test(milestone.timebox) ? `Phase ${index + 1}` : milestone.timebox,
303
+ })),
304
+ } } : {}),
305
+ ...(plan.reader_brief ? { reader_brief: plan.reader_brief } : {}),
153
306
  });
154
307
  }
155
308
  function unavailablePlan(reason, contract, risks, blindSpots, openQuestions) {
@@ -163,16 +316,20 @@ function unavailablePlan(reason, contract, risks, blindSpots, openQuestions) {
163
316
  unresolved_questions: (unresolved.length ? unresolved : [`What evidence would change the verdict for ${clip(contract.task, 100)}?`]).slice(0, 10),
164
317
  };
165
318
  }
166
- export async function s9bPlan(ctx, contract, seats, graph, judgeReport) {
167
- const openQuestions = mergeOpenQuestions(seats);
168
- const requestedOutputs = contract.requested_outputs
169
- ?? ['DECISION'];
170
- const risks = unresolvedRisks(graph, judgeReport);
171
- const blindSpots = graph.holes.coverage.map((hole) => hole.label);
319
+ export async function s9bPlan(ctx, contract, seats, graph, judgeReport, originalRequest = contract.task) {
320
+ const answerContext = buildAnswerContext({ originalRequest, contract, seats, graph, judgeReport, flags: ctx.flags });
321
+ const openQuestions = answerContext.open_questions;
322
+ const requestedOutputs = answerContext.requested_outputs;
323
+ const risks = answerContext.upheld_risks;
324
+ const blindSpots = answerContext.blind_spots;
325
+ const sourceIds = graph.evidence.map((evidence) => evidence.id);
326
+ const requireReaderBrief = 'success_bar' in contract;
172
327
  const anchors = {
173
328
  claimIds: risks.map((r) => r.id),
329
+ knownReaderIds: graph.claims.map((claim) => claim.id),
174
330
  blindSpots,
175
331
  openQuestions,
332
+ sourceIds,
176
333
  };
177
334
  const fallback = async (flag) => {
178
335
  ctx.addFlag(flag);
@@ -182,35 +339,28 @@ export async function s9bPlan(ctx, contract, seats, graph, judgeReport) {
182
339
  };
183
340
  if (ctx.budget.limit - ctx.budget.used < 1)
184
341
  return fallback('plan_skipped');
185
- const prompt = buildActionPlannerPrompt({
186
- task: contract.task,
187
- recommendation: judgeReport.recommendation,
188
- conditions: judgeReport.conditions,
189
- decision_snapshot: judgeReport.decision_snapshot,
190
- upheld_risks: risks,
191
- blind_spots: blindSpots,
192
- open_questions: openQuestions,
193
- requested_outputs: requestedOutputs,
194
- }, loadSkill('idea-refinement', 'planner'));
342
+ const prompt = buildActionPlannerPrompt(answerContext, loadSkill('idea-refinement', 'planner'));
343
+ const calendarAllowed = hasExplicitSchedule(answerContext);
195
344
  try {
196
345
  const first = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-plan', prompt, PlannerOutput, {
197
346
  repair: ctx.budget.limit - ctx.budget.used >= 2,
198
- }));
199
- const anchored = anchoredActionPlan(first, anchors, requestedOutputs);
347
+ }), calendarAllowed);
348
+ const anchored = anchoredActionPlan(first, anchors, requestedOutputs, requireReaderBrief);
200
349
  if (anchored) {
201
350
  await ctx.writer.writeJson('action-plan', anchored);
202
351
  return anchored;
203
352
  }
204
353
  if (ctx.budget.limit - ctx.budget.used < 1)
205
354
  return fallback('plan_fallback');
206
- const repair = `${prompt}\n\n---\nYour previous plan had no actions with valid anchors or omitted a requested deliverable.\n` +
355
+ const repair = `${prompt}\n\n---\nYour previous response had invalid anchors, omitted a requested deliverable or reader_brief, used internal audit language, or cited an unknown source id.\n` +
207
356
  `Valid graph claim ids: ${anchors.claimIds.join(', ') || '(none)'}\n` +
208
357
  `Valid blind spots: ${anchors.blindSpots.join(' | ') || '(none)'}\n` +
209
358
  `Valid open questions: ${anchors.openQuestions.join(' | ') || '(none)'}\n` +
359
+ `Valid source ids: ${anchors.sourceIds.join(', ') || '(none)'}\n` +
210
360
  `Required outputs: ${requestedOutputs.join(', ')}\n` +
211
- `Output ONLY corrected JSON with every action anchored and every requested deliverable present.`;
212
- const repaired = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-anchor-repair', repair, PlannerOutput, { repair: false }));
213
- const repairedAnchored = anchoredActionPlan(repaired, anchors, requestedOutputs);
361
+ `Output ONLY corrected JSON with every action anchored, reader_brief present, and every requested deliverable present.`;
362
+ const repaired = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-anchor-repair', repair, PlannerOutput, { repair: false }), calendarAllowed);
363
+ const repairedAnchored = anchoredActionPlan(repaired, anchors, requestedOutputs, requireReaderBrief);
214
364
  if (repairedAnchored) {
215
365
  await ctx.writer.writeJson('action-plan', repairedAnchored);
216
366
  return repairedAnchored;
@@ -186,6 +186,7 @@ export const PreflightReading = z.object({
186
186
  missing_evidence: z.array(z.string().min(1)).max(8),
187
187
  domain_dimensions: z.array(DomainDimension).min(3).max(5),
188
188
  questions: z.array(RunBriefQuestion).max(4),
189
+ requested_outputs: z.array(RequestedOutputSchema).max(4).default([]),
189
190
  }).strict().superRefine((reading, ctx) => {
190
191
  checkQuestionIds(reading.questions, ctx);
191
192
  checkDomainDimensionIds(reading.domain_dimensions, ctx);
@@ -236,6 +237,7 @@ export const ClaimPosition = z
236
237
  dimension_id: z.string().min(1),
237
238
  stance: z.enum(['SUPPORT', 'OPPOSE', 'MIXED', 'UNKNOWN']),
238
239
  basis: z.enum(['EVIDENCE', 'INFERENCE', 'ASSUMPTION']),
240
+ nature: z.enum(['FACTUAL', 'JUDGMENT']).default('JUDGMENT'),
239
241
  load_bearing: z.boolean(),
240
242
  if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
241
243
  reasoning: z.string().min(1),
@@ -316,6 +318,14 @@ export const DecisionQuestion = z
316
318
  claim_ids: z.array(z.string().min(1)),
317
319
  })
318
320
  .strict();
321
+ export const DeliverableProposal = z.object({
322
+ output: z.enum(['FEATURE_BACKLOG', 'IMPLEMENTATION_PLAN']),
323
+ title: z.string().min(1),
324
+ detail: z.string().min(1),
325
+ user_value: z.string().min(1),
326
+ why_distinctive: z.string().min(1),
327
+ evidence_ids: z.array(z.string().min(1)).max(6),
328
+ }).strict();
319
329
  const IdeaRoleOutputBase = z
320
330
  .object({
321
331
  workflow: z.literal('idea-refinement'),
@@ -326,6 +336,7 @@ const IdeaRoleOutputBase = z
326
336
  calculations: z.array(CalculationLedger).max(8).default([]),
327
337
  coverage: z.array(CoverageEntry).max(18), // 13 core + up to 5 preflight domain dimensions
328
338
  decision_questions: z.array(DecisionQuestion).max(8),
339
+ deliverable_proposals: z.array(DeliverableProposal).max(8).default([]),
329
340
  })
330
341
  .strict();
331
342
  function checkSubmissionRefs(submission, ctx) {
@@ -395,6 +406,12 @@ function checkSubmissionRefs(submission, ctx) {
395
406
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['decision_questions', index, 'claim_ids'], message: `unknown position id: ${id}` });
396
407
  }
397
408
  }
409
+ for (const [index, proposal] of submission.deliverable_proposals.entries()) {
410
+ for (const id of proposal.evidence_ids) {
411
+ if (!evidenceIds.has(id))
412
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['deliverable_proposals', index, 'evidence_ids'], message: `unknown evidence id: ${id}` });
413
+ }
414
+ }
398
415
  }
399
416
  export const IdeaRoleOutput = IdeaRoleOutputBase.superRefine(checkSubmissionRefs);
400
417
  /** Defect categories a finding (and a seeded bug, T11) can carry. BENCHMARK.md's "defect class" match
@@ -527,6 +544,17 @@ export function salvageIdeaRoleOutputModel(input) {
527
544
  && calc.inputs.every((input) => input.evidence_ids.every((id) => keptEvidence.has(id))))
528
545
  .slice(0, 8)
529
546
  : output.calculations;
547
+ const deliverableProposals = parseOrDrop(output.deliverable_proposals, (item) => {
548
+ if (!item || typeof item !== 'object' || Array.isArray(item))
549
+ return DeliverableProposal.safeParse(item);
550
+ const proposal = item;
551
+ return DeliverableProposal.strip().safeParse({
552
+ ...proposal,
553
+ evidence_ids: Array.isArray(proposal.evidence_ids)
554
+ ? proposal.evidence_ids.filter((id) => keptEvidence.has(id))
555
+ : proposal.evidence_ids,
556
+ });
557
+ });
530
558
  return {
531
559
  ...output,
532
560
  evidence,
@@ -534,6 +562,7 @@ export function salvageIdeaRoleOutputModel(input) {
534
562
  calculations,
535
563
  coverage: parseOrDrop(scrubIds(output.coverage, 'position_ids'), (item) => CoverageEntryBase.strip().superRefine(checkCoverageEntry).safeParse(item)),
536
564
  decision_questions: parseOrDrop(scrubIds(output.decision_questions, 'claim_ids'), (item) => DecisionQuestion.strip().safeParse(item)),
565
+ deliverable_proposals: deliverableProposals,
537
566
  };
538
567
  }
539
568
  /** The model-facing S4 shape. Exact known enum aliases are canonicalized, then the strict schema validates
@@ -588,6 +617,7 @@ export const DecisionClaim = z.object({
588
617
  position_ids: z.array(z.string().min(1)).min(1),
589
618
  state: z.enum(['CONSENSUS', 'SHARED_CONCERN', 'DISAGREEMENT', 'UNIQUE', 'UNCERTAIN']),
590
619
  evidence_state: z.enum(['SUPPORTED', 'CONFLICTED', 'UNVERIFIED']),
620
+ nature: z.enum(['FACTUAL', 'JUDGMENT']).default('JUDGMENT'),
591
621
  load_bearing: z.boolean(),
592
622
  if_false: z.enum(['STOP', 'PIVOT', 'CONDITION', 'MINOR']),
593
623
  sensitivity: z.enum(['DECISIVE', 'MATERIAL', 'LOW']),
@@ -899,7 +929,32 @@ export const ImplementationPlan = z.object({
899
929
  acceptance_test: z.string().min(1),
900
930
  }).strict()).min(1).max(7),
901
931
  }).strict();
902
- export const ActionPlan = z
932
+ const ReaderBriefBase = z.object({
933
+ headline: z.string().min(1).max(160),
934
+ bottom_line: z.string().min(1).max(1200),
935
+ sections: z.array(z.object({
936
+ heading: z.string().min(1).max(120),
937
+ summary: z.string().min(1).max(1000),
938
+ bullets: z.array(z.string().min(1).max(500)).max(6),
939
+ }).strict()).min(2).max(6),
940
+ next_step: z.string().min(1).max(600),
941
+ caveats: z.array(z.string().min(1).max(500)).max(3),
942
+ source_ids: z.array(z.string().min(1)).max(8),
943
+ }).strict();
944
+ const READER_AUDIT_ENUM = /\b(?:UNVERIFIED|UNVERIFIABLE|PARTIAL|CONFLICTED)\b/;
945
+ const READER_AUDIT_LANGUAGE = /\b(?:structural score|evidence coverage|verification coverage|claim ids?|graph ids?|provider calls?|model calls?|answer editor|reader_brief)\b/i;
946
+ export const ReaderBrief = ReaderBriefBase.superRefine((brief, ctx) => {
947
+ const text = JSON.stringify(brief);
948
+ if (READER_AUDIT_ENUM.test(text) || READER_AUDIT_LANGUAGE.test(text)) {
949
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'reader_brief contains internal audit language' });
950
+ }
951
+ });
952
+ /** Contextual reader-language check: only reject IDs that actually exist in this run. */
953
+ export function readerBriefIssues(brief, knownIds) {
954
+ const text = JSON.stringify(brief);
955
+ return [...knownIds].filter((id) => new RegExp(`\\b${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(text));
956
+ }
957
+ const ActionPlanBase = z
903
958
  .object({
904
959
  actions: z
905
960
  .array(z
@@ -912,13 +967,19 @@ export const ActionPlan = z
912
967
  kill_signal: z.string().min(1),
913
968
  })
914
969
  .strict())
915
- .min(1)
916
970
  .max(7),
917
971
  sequencing_note: z.string().min(1),
918
972
  feature_backlog: FeatureBacklog.optional(),
919
973
  implementation_plan: ImplementationPlan.optional(),
974
+ // Optional only so pre-v5 run artifacts remain replayable. New decision contracts require it in S9b.
975
+ reader_brief: ReaderBrief.optional(),
920
976
  })
921
977
  .strict();
978
+ export const ActionPlan = ActionPlanBase.superRefine((plan, ctx) => {
979
+ if (plan.actions.length === 0 && !plan.reader_brief) {
980
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['actions'], message: 'an empty action plan requires reader_brief' });
981
+ }
982
+ });
922
983
  export const PlannerUnavailable = z.object({
923
984
  kind: z.literal('PlannerUnavailable'),
924
985
  reason: z.enum(['budget_exhausted', 'planner_failed']),
@@ -934,7 +995,7 @@ export const QuickDecisionModel = z.object({
934
995
  key_points: z.array(z.string().min(1)).min(2).max(8),
935
996
  dissent: z.array(z.string().min(1)).min(1).max(4),
936
997
  confidence_notes: z.string().min(1),
937
- action_plan: ActionPlan,
998
+ action_plan: ActionPlanBase.extend({ reader_brief: ReaderBrief }),
938
999
  }).strict().superRefine((decision, ctx) => {
939
1000
  const hasConditions = decision.conditions.length > 0;
940
1001
  if (decision.recommendation === 'PROCEED_WITH_CONDITIONS' && !hasConditions) {
@@ -987,11 +1048,14 @@ export const RunMeta = z.object({
987
1048
  flags: z.array(z.enum([
988
1049
  'synthesis_suspect',
989
1050
  'low_diversity',
1051
+ 'weak_seat',
990
1052
  'plan_skipped',
991
1053
  'plan_fallback',
992
1054
  'headless_intent',
993
1055
  'verification_skipped',
994
1056
  'research_ungrounded',
1057
+ 'source_fallback_search',
1058
+ 'deliverable_gap',
995
1059
  'single_model',
996
1060
  ])).optional(),
997
1061
  });
@@ -1,11 +1,11 @@
1
- # Analyst playbook — pressure-test the idea
1
+ # Analyst playbook — strengthen and pressure-test the idea
2
2
 
3
- You are an adversarial analyst on a decision panel. Your job is not to like the idea it is to find
4
- where it breaks. Steelman it once, then attack it honestly.
3
+ You are an independent analyst on a decision panel. Build the strongest useful version for your lane,
4
+ then expose where it breaks. Creation and criticism are both required.
5
5
 
6
6
  ## Strongest version first
7
7
  - Before attacking, state the best honest version of the idea: who it is for and why it would win.
8
- - Attack THAT version, not a weak strawman. If the steelman is thin, that itself is a finding.
8
+ - Test THAT version, not a weak strawman. If the steelman is thin, that itself is a finding.
9
9
 
10
10
  ## Positions — make the stance explicit
11
11
  - State each decision-critical proposition and take an explicit SUPPORT, OPPOSE, MIXED, or UNKNOWN
@@ -45,5 +45,5 @@ ownership; emit one explicit coverage entry for every owned dimension.
45
45
  - Phrase each so that a yes/no or a single number would actually flip your assessment.
46
46
 
47
47
  ## Do not
48
- - No motivational framing, no pitch language, no summarizing your own output.
48
+ - No unsupported hype or generic feature wishlists. Explain the mechanism and user value.
49
49
  - No unanchored position or evidence. No proposition you cannot tie to the decision.
@@ -0,0 +1,18 @@
1
+ # Chair playbook — rule on the real disagreement
2
+
3
+ Add to your base instructions; the schema and caps are unchanged and always win.
4
+
5
+ ## Synthesis discipline
6
+ - Count convergence only when seats reached the same conclusion independently with different evidence.
7
+ Two seats repeating the same hedge is not agreement.
8
+ - Never smooth over a clash. State both readings, compare their evidence quality, and rule.
9
+ - A strong dissent beats a weak majority. Side with the better-evidenced minority when warranted and
10
+ explain why in `key_points`.
11
+ - Never answer "it depends". Choose the required verdict enum; put contingencies into concrete,
12
+ checkable conditions that name their claim.
13
+ - Lead `key_points` with the evidence or result that would change the verdict.
14
+ - Report a blind spot only when verification or rebuttal actually exposed it.
15
+
16
+ ## First action
17
+ - Anchor the first action to the single cheapest check that could kill or confirm the decision.
18
+ - Pick one thing first, not a list disguised as one action.
@@ -3,5 +3,16 @@
3
3
  Own unit economics and capital needs, technical and operational feasibility, supply constraints,
4
4
  legal and compliance exposure, scalability, failure modes, and the cheapest decisive kill criteria.
5
5
 
6
+ Act as the skeptic/executor for requested deliverables. When FEATURE_BACKLOG is requested, produce 2–4
7
+ `deliverable_proposals` that preserve the standout value while remaining credible under the stated deadline.
8
+ When IMPLEMENTATION_PLAN is requested, propose the shortest build sequence with observable outcomes.
9
+
6
10
  Steelman the idea first. Cross into another lane only for a load-bearing issue. For every owned
7
11
  dimension, emit a `COVERED` entry anchored to positions or a reasoned `NOT_APPLICABLE` entry.
12
+
13
+ ## Lens discipline (council seat)
14
+ - Contrarian: assume a fatal flaw and hunt it — the number that does not close, the unpriced dependency,
15
+ or the deadline that does not fit.
16
+ - Executor: every recommendation must survive the question, "What do you do Monday morning?"
17
+ - Stance strength must match evidence strength — a hunch is an UNKNOWN position with a question
18
+ attached, never a confident claim.
@@ -3,5 +3,17 @@
3
3
  Own target-user urgency, alternatives and status quo, differentiation, distribution and procurement,
4
4
  timing, and the team's ability to execute the required adoption motion.
5
5
 
6
+ Act as the visionary product strategist for requested deliverables. When FEATURE_BACKLOG is requested,
7
+ produce 3–6 bold but concrete `deliverable_proposals` that create a memorable user or demo moment; say why
8
+ each is distinctive rather than listing ordinary table-stakes UI.
9
+
6
10
  Steelman the idea first. Cross into another lane only for a load-bearing issue. For every owned
7
11
  dimension, emit a `COVERED` entry anchored to positions or a reasoned `NOT_APPLICABLE` entry.
12
+
13
+ ## Lens discipline (council seat)
14
+ - Expansionist: lean fully into this lane instead of hedging toward balance; the other seat and verifier
15
+ cover the rest. Name the undervalued upside with a number and evidence ID, or mark it UNKNOWN.
16
+ - Outsider: read the pitch with zero context. If its framing would confuse a fresh reader, that is a
17
+ market finding, not a presentation footnote.
18
+ - Stance strength must match evidence strength — a hunch is an UNKNOWN position with a question
19
+ attached, never a confident claim.