aiki-cli 0.3.0 → 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 (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +16 -8
  3. package/dist/bench/idea-v3-rating.js +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/run.js +10 -7
  6. package/dist/council/view.js +146 -46
  7. package/dist/orchestration/context.js +3 -3
  8. package/dist/orchestration/decision-dossier.js +467 -80
  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 +16 -4
  12. package/dist/orchestration/preflight.js +43 -9
  13. package/dist/orchestration/quick-analysis.js +35 -6
  14. package/dist/orchestration/stages/s10-render.js +179 -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 +208 -35
  22. package/dist/orchestration/url-sources.js +200 -0
  23. package/dist/schemas/index.js +134 -6
  24. package/dist/skills/idea-refinement/analyst.md +5 -5
  25. package/dist/skills/idea-refinement/chair.md +18 -0
  26. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  27. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  28. package/dist/skills/idea-refinement/planner.md +25 -19
  29. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  30. package/dist/skills/idea-refinement/verifier.md +17 -0
  31. package/dist/storage/runs.js +2 -1
  32. package/dist/tui/app.js +19 -4
  33. package/dist/workflows/idea-refinement.js +51 -15
  34. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  // S9 — chair adjudication over graph-selected escalations. Consensus and shared concerns are
2
2
  // read-only context; anonymous position text and the verifier record cross the boundary unchanged.
3
- import { selectEscalations } from '../decision-graph.js';
4
3
  import { IdeaChairReportModel } from '../../schemas/index.js';
5
4
  import { isFatal, StageError } from '../context.js';
6
5
  import { jsonCall } from '../jsonStage.js';
7
- import { claimVerificationRefIssues } from './s8-verify.js';
6
+ import { claimShortLabel } from '../decision-dossier.js';
7
+ import { loadSkill } from '../skills.js';
8
+ import { claimVerificationRefIssues, selectVerificationEscalations } from './s8-verify.js';
8
9
  const EMPTY_REBUTTALS = {
9
10
  round: 1,
10
11
  selected_claim_ids: [],
@@ -39,7 +40,7 @@ Output ONLY JSON matching the judge schema:
39
40
  - key_points: 4-8 standalone decision-relevant bullets.
40
41
  - dissent: a JSON array of strings (an array even when there is only one) — the strongest arguments
41
42
  against your verdict.
42
- - confidence_notes: explain calibrated confidence.
43
+ - confidence_notes: explain calibrated confidence.{{SKILL}}
43
44
  ESCALATED CLAIMS + VERIFICATION: {{ESCALATIONS_JSON}}
44
45
  APPEND-ONLY REBUTTAL EVENTS: {{REBUTTALS_JSON}}
45
46
  SETTLED/UNRESOLVED CONTEXT: {{CONTEXT_JSON}}`;
@@ -173,19 +174,40 @@ export function adjudicableClaimIds(graph, ids, judgeProvider) {
173
174
  return [...ids].filter((id) => !claimById.get(id)?.position_ids
174
175
  .some((positionId) => positionById.get(positionId)?.provider === judgeProvider));
175
176
  }
177
+ /** One condition per distinct claim behind an evidence hole, claim-labeled instead of a generic
178
+ * placeholder reason. Deduped by claim_id and capped at 4 so the fallback conditions list doesn't
179
+ * drown in near-identical entries. */
180
+ export function evidenceHoleConditions(graph) {
181
+ const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
182
+ const seen = new Set();
183
+ const out = [];
184
+ for (const hole of graph.holes.evidence) {
185
+ if (seen.has(hole.claim_id))
186
+ continue;
187
+ seen.add(hole.claim_id);
188
+ const claim = claimById.get(hole.claim_id);
189
+ out.push(`Obtain independent evidence for: "${claimShortLabel(claim?.proposition ?? 'a load-bearing claim')}".`);
190
+ if (out.length === 4)
191
+ break;
192
+ }
193
+ return out;
194
+ }
176
195
  function fallbackConditions(graph, adjudications) {
177
196
  const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
178
197
  const upheld = adjudications
179
198
  .filter((item) => item.ruling === 'UPHOLD')
180
199
  .map((item) => `Proceed only if you can resolve: ${claimById.get(item.id)?.proposition ?? item.id}.`);
181
200
  const holes = graph.holes.coverage.map((hole) => `Proceed only after examining the ${hole.label} gap.`);
182
- const evidenceHoles = graph.holes.evidence.map((hole) => `Proceed only after resolving evidence gap ${hole.claim_id}: ${hole.reason}.`);
201
+ const evidenceHoles = evidenceHoleConditions(graph);
183
202
  return [...upheld, ...holes, ...evidenceHoles, 'Proceed only after one cheap test confirms the core user need.'].slice(0, 6);
184
203
  }
185
- export function buildJudgePrompt(contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS, judgeProvider) {
186
- return judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider).prompt;
204
+ export function buildJudgePrompt(contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS, judgeProvider, skill = '') {
205
+ return judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider, skill).prompt;
206
+ }
207
+ export function buildChairRepairPrompt(basePrompt, corrections) {
208
+ return `${basePrompt}\n\n---\nCorrect these problems:\n${corrections}Output ONLY corrected JSON.`;
187
209
  }
188
- function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider) {
210
+ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProvider, skill = '') {
189
211
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
190
212
  const verificationById = new Map(verifications.verifications.map((item) => [item.claim_id, item]));
191
213
  const citedEvidence = [...new Set([
@@ -194,7 +216,7 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
194
216
  ])];
195
217
  const evidenceRefs = new Map(citedEvidence.map((id, index) => [`E${index + 1}`, id]));
196
218
  const aliasByEvidence = new Map([...evidenceRefs].map(([alias, id]) => [id, alias]));
197
- const selectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id);
219
+ const selectedIds = selectVerificationEscalations(graph).map((item) => item.claim_id);
198
220
  const eligibleIds = judgeProvider ? adjudicableClaimIds(graph, selectedIds, judgeProvider) : selectedIds;
199
221
  const escalationIds = new Set(eligibleIds);
200
222
  const escalations = graph.claims.filter((claim) => escalationIds.has(claim.id)).map((claim) => {
@@ -234,6 +256,7 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
234
256
  ...(event.narrowed_proposition ? { narrowed_proposition: event.narrowed_proposition } : {}),
235
257
  }));
236
258
  const prompt = S9_PROMPT
259
+ .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
237
260
  .replace('{{RUBRIC_JSON}}', JSON.stringify(rubric.map((item) => item.label)))
238
261
  .replace('{{ESCALATIONS_JSON}}', JSON.stringify(escalations, null, 2))
239
262
  .replace('{{REBUTTALS_JSON}}', JSON.stringify(rebuttalContext, null, 2))
@@ -242,13 +265,13 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
242
265
  return { prompt, evidenceRefs };
243
266
  }
244
267
  export async function s9Judge(ctx, contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS) {
245
- const selectedIds = selectEscalations(graph, { max: 8 }).map((item) => item.claim_id);
268
+ const selectedIds = selectVerificationEscalations(graph).map((item) => item.claim_id);
246
269
  const verificationIssues = claimVerificationRefIssues(graph, verifications, selectedIds);
247
270
  if (verificationIssues.length)
248
271
  throw new StageError('S9', 'BAD_OUTPUT', `invalid verification references: ${verificationIssues.join('; ')}`);
249
272
  const ids = adjudicableClaimIds(graph, selectedIds, ctx.roles.judge);
250
273
  const selfAuthored = new Set(selectedIds.filter((id) => !ids.includes(id)));
251
- const input = judgeInput(contract, graph, verifications, rubric, rebuttals, ctx.roles.judge);
274
+ const input = judgeInput(contract, graph, verifications, rubric, rebuttals, ctx.roles.judge, loadSkill('idea-refinement', 'chair'));
252
275
  const basePrompt = input.prompt;
253
276
  const judge = ctx.handle(ctx.roles.judge);
254
277
  const positionById = new Map(graph.positions.map((position) => [position.id, position]));
@@ -291,13 +314,13 @@ export async function s9Judge(ctx, contract, graph, verifications, rubric, rebut
291
314
  ];
292
315
  if ((violations.length || evidenceViolations.length || report.dissent.length === 0 || recIssues.length || chairIssues.length)
293
316
  && ctx.budget.limit - ctx.budget.used > 1) {
294
- const repair = `${basePrompt}\n\n---\nCorrect these problems:\n`
317
+ const corrections = ''
295
318
  + (violations.length ? `- adjudications may reference only [${ids.join(', ')}], not ${violations.join(', ')}\n` : '')
296
319
  + (evidenceViolations.length ? `- evidence reference errors: ${evidenceViolations.join('; ')}\n` : '')
297
320
  + (report.dissent.length === 0 ? '- dissent must contain at least one item\n' : '')
298
321
  + (recIssues.length ? `- ${recIssues.join('; ')}\n` : '')
299
- + (chairIssues.length ? `- chair contract errors: ${chairIssues.join('; ')}\n` : '')
300
- + 'Output ONLY corrected JSON.';
322
+ + (chairIssues.length ? `- chair contract errors: ${chairIssues.join('; ')}\n` : '');
323
+ const repair = buildChairRepairPrompt(basePrompt, corrections);
301
324
  try {
302
325
  report = translateChairReport(await jsonCall(ctx, judge, 'S9-repair', repair, IdeaChairReportModel, {
303
326
  repair: ctx.budget.limit - ctx.budget.used > 2,
@@ -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 } 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,24 +16,55 @@ 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
- }).strict();
21
- const S9B_PROMPT = `ROLE: Validation planner. You write the next actions for a decision-maker after an
22
- idea council has already debated and judged the idea. Do not write a build roadmap. Write only validation
23
- actions that test unsettled risks, blind spots, or open questions. Cheapest decisive test first.{{SKILL}}
21
+ feature_backlog: FeatureBacklog.optional(),
22
+ implementation_plan: ImplementationPlan.optional(),
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}}
24
31
 
25
32
  Output ONLY JSON matching the ActionPlan schema:
26
- - 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.
27
48
  - validates MUST anchor to one of:
28
49
  - a graph claim id from upheld_risks, e.g. "G3"
29
50
  - a blind spot label as "blind:<label>"
30
51
  - an open-question prefix as "Q:<question prefix>"
31
52
  - why ties the action to the risk, blind spot, or question.
32
53
  - kill_signal is the result that should stop or reshape the idea.
33
- - Preserve the chair's numeric distinctions: operating break-even is not capital payback, and a target cap
34
- 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.
35
59
  - sequencing_note explains why this order is cheapest and decisive.
60
+ - Read requested_outputs in CONTEXT. When it includes FEATURE_BACKLOG, include feature_backlog with
61
+ must/should/later items {feature,user_value,rationale,effort:S|M|L} and wont items {feature,reason}.
62
+ Prioritize the smallest judge-impressing golden path; MUST is required, not a wishlist.
63
+ - When requested_outputs includes IMPLEMENTATION_PLAN, include implementation_plan.milestones with
64
+ {order,timebox,outcome,tasks,acceptance_test}. This is a concrete build sequence, not validation prose.
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.
36
68
 
37
69
  CONTEXT: {{CONTEXT_JSON}}`;
38
70
  function clip(s, n) {
@@ -45,19 +77,21 @@ function sevRank(s) {
45
77
  function norm(s) {
46
78
  return s.trim().toLowerCase();
47
79
  }
48
- function unresolvedRisks(graph, judgeReport) {
80
+ export function unresolvedRisks(graph, judgeReport) {
49
81
  const claimById = new Map(graph.claims.map((claim) => [claim.id, claim]));
50
82
  const rulingById = new Map(judgeReport.adjudications.map((a) => [a.id, a]));
51
83
  const risks = graph.claims
52
84
  .map((claim) => {
53
85
  const adj = rulingById.get(claim.id);
54
- if (adj?.ruling !== 'UPHOLD')
86
+ const outcome = interpretClaimOutcome(graph, claim, adj);
87
+ if ((!adj && outcome.decisionEffect !== 'FAILED')
88
+ || (outcome.propositionTruth === 'HOLDS' && outcome.decisionEffect === 'HELD'))
55
89
  return null;
56
90
  return {
57
91
  id: claim.id,
58
92
  assumption: claim.proposition,
59
93
  severity: claim.sensitivity === 'DECISIVE' ? 'HIGH' : claim.sensitivity === 'MATERIAL' ? 'MED' : 'LOW',
60
- reasoning: adj.reasoning,
94
+ reasoning: adj?.reasoning ?? 'This supported concern works against the decision.',
61
95
  };
62
96
  })
63
97
  .filter((risk) => risk !== null);
@@ -77,6 +111,96 @@ function unresolvedRisks(graph, judgeReport) {
77
111
  }
78
112
  return risks.sort((a, b) => sevRank(a.severity) - sevRank(b.severity));
79
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
+ }
80
204
  export function buildActionPlannerPrompt(input, skill) {
81
205
  return S9B_PROMPT
82
206
  .replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
@@ -102,14 +226,42 @@ export function validAnchor(anchor, anchors) {
102
226
  }
103
227
  return false;
104
228
  }
105
- export function anchoredActionPlan(plan, anchors) {
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;
106
251
  const actions = plan.actions
107
252
  .filter((a) => validAnchor(a.validates, anchors))
108
253
  .sort((a, b) => a.order - b.order)
254
+ .slice(0, requireReaderBrief ? 4 : 7)
109
255
  .map((a, i) => ({ ...a, order: i + 1 }));
110
- if (actions.length === 0)
256
+ if (actions.length === 0 && !requireReaderBrief)
111
257
  return null;
112
- return { actions, sequencing_note: plan.sequencing_note };
258
+ return {
259
+ actions,
260
+ sequencing_note: plan.sequencing_note,
261
+ ...(requestedOutputs.includes('FEATURE_BACKLOG') && plan.feature_backlog ? { feature_backlog: plan.feature_backlog } : {}),
262
+ ...(requestedOutputs.includes('IMPLEMENTATION_PLAN') && plan.implementation_plan ? { implementation_plan: plan.implementation_plan } : {}),
263
+ ...(readerBrief ? { reader_brief: readerBrief } : {}),
264
+ };
113
265
  }
114
266
  export function normalizeEffort(raw) {
115
267
  const value = raw?.trim().toLowerCase();
@@ -123,7 +275,19 @@ export function normalizeEffort(raw) {
123
275
  const days = amount * (unit === 'minute' ? 1 / 1440 : unit === 'hour' ? 1 / 24 : unit === 'day' ? 1 : unit === 'week' ? 7 : unit === 'month' ? 30 : 365);
124
276
  return days <= 2 ? 'S' : days <= 14 ? 'M' : 'L';
125
277
  }
126
- 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) {
127
291
  return ActionPlan.parse({
128
292
  actions: plan.actions.map((action, i) => ({
129
293
  ...action,
@@ -131,6 +295,14 @@ export function normalizePlannerOutput(plan) {
131
295
  effort: normalizeEffort(action.effort),
132
296
  })),
133
297
  sequencing_note: plan.sequencing_note,
298
+ ...(plan.feature_backlog ? { feature_backlog: plan.feature_backlog } : {}),
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 } : {}),
134
306
  });
135
307
  }
136
308
  function unavailablePlan(reason, contract, risks, blindSpots, openQuestions) {
@@ -144,14 +316,20 @@ function unavailablePlan(reason, contract, risks, blindSpots, openQuestions) {
144
316
  unresolved_questions: (unresolved.length ? unresolved : [`What evidence would change the verdict for ${clip(contract.task, 100)}?`]).slice(0, 10),
145
317
  };
146
318
  }
147
- export async function s9bPlan(ctx, contract, seats, graph, judgeReport) {
148
- const openQuestions = mergeOpenQuestions(seats);
149
- const risks = unresolvedRisks(graph, judgeReport);
150
- 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;
151
327
  const anchors = {
152
328
  claimIds: risks.map((r) => r.id),
329
+ knownReaderIds: graph.claims.map((claim) => claim.id),
153
330
  blindSpots,
154
331
  openQuestions,
332
+ sourceIds,
155
333
  };
156
334
  const fallback = async (flag) => {
157
335
  ctx.addFlag(flag);
@@ -161,33 +339,28 @@ export async function s9bPlan(ctx, contract, seats, graph, judgeReport) {
161
339
  };
162
340
  if (ctx.budget.limit - ctx.budget.used < 1)
163
341
  return fallback('plan_skipped');
164
- const prompt = buildActionPlannerPrompt({
165
- task: contract.task,
166
- recommendation: judgeReport.recommendation,
167
- conditions: judgeReport.conditions,
168
- decision_snapshot: judgeReport.decision_snapshot,
169
- upheld_risks: risks,
170
- blind_spots: blindSpots,
171
- open_questions: openQuestions,
172
- }, loadSkill('idea-refinement', 'planner'));
342
+ const prompt = buildActionPlannerPrompt(answerContext, loadSkill('idea-refinement', 'planner'));
343
+ const calendarAllowed = hasExplicitSchedule(answerContext);
173
344
  try {
174
345
  const first = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-plan', prompt, PlannerOutput, {
175
346
  repair: ctx.budget.limit - ctx.budget.used >= 2,
176
- }));
177
- const anchored = anchoredActionPlan(first, anchors);
347
+ }), calendarAllowed);
348
+ const anchored = anchoredActionPlan(first, anchors, requestedOutputs, requireReaderBrief);
178
349
  if (anchored) {
179
350
  await ctx.writer.writeJson('action-plan', anchored);
180
351
  return anchored;
181
352
  }
182
353
  if (ctx.budget.limit - ctx.budget.used < 1)
183
354
  return fallback('plan_fallback');
184
- const repair = `${prompt}\n\n---\nYour previous plan had no actions with valid anchors.\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` +
185
356
  `Valid graph claim ids: ${anchors.claimIds.join(', ') || '(none)'}\n` +
186
357
  `Valid blind spots: ${anchors.blindSpots.join(' | ') || '(none)'}\n` +
187
358
  `Valid open questions: ${anchors.openQuestions.join(' | ') || '(none)'}\n` +
188
- `Output ONLY corrected JSON with every action anchored to one of those values.`;
189
- const repaired = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-anchor-repair', repair, PlannerOutput, { repair: false }));
190
- const repairedAnchored = anchoredActionPlan(repaired, anchors);
359
+ `Valid source ids: ${anchors.sourceIds.join(', ') || '(none)'}\n` +
360
+ `Required outputs: ${requestedOutputs.join(', ')}\n` +
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);
191
364
  if (repairedAnchored) {
192
365
  await ctx.writer.writeJson('action-plan', repairedAnchored);
193
366
  return repairedAnchored;