aiki-cli 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +80 -5
  2. package/README.md +73 -15
  3. package/dist/bench/idea-v3-bench.js +104 -5
  4. package/dist/bench/idea-v3-rating.js +159 -4
  5. package/dist/cli/bench.js +5 -5
  6. package/dist/cli/index.js +12 -2
  7. package/dist/cli/resume.js +20 -0
  8. package/dist/cli/run.js +76 -5
  9. package/dist/cli/serve.js +48 -0
  10. package/dist/config/config.js +7 -2
  11. package/dist/council/view.js +64 -15
  12. package/dist/orchestration/auto-profile.js +97 -0
  13. package/dist/orchestration/claim-groups.js +56 -0
  14. package/dist/orchestration/context.js +69 -6
  15. package/dist/orchestration/decision-dossier.js +450 -89
  16. package/dist/orchestration/decision-graph.js +33 -2
  17. package/dist/orchestration/engine.js +8 -4
  18. package/dist/orchestration/evidence-origin.js +17 -0
  19. package/dist/orchestration/jsonStage.js +47 -5
  20. package/dist/orchestration/legacy-idea-adapter.js +3 -0
  21. package/dist/orchestration/modes.js +18 -10
  22. package/dist/orchestration/preflight.js +36 -3
  23. package/dist/orchestration/quick-analysis.js +29 -7
  24. package/dist/orchestration/sanitize-paths.js +10 -0
  25. package/dist/orchestration/stages/s10-render.js +196 -84
  26. package/dist/orchestration/stages/s4-analyze.js +10 -2
  27. package/dist/orchestration/stages/s4b-challenge.js +97 -0
  28. package/dist/orchestration/stages/s5-drift.js +13 -3
  29. package/dist/orchestration/stages/s6-positions.js +18 -0
  30. package/dist/orchestration/stages/s7-decision-graph.js +3 -0
  31. package/dist/orchestration/stages/s8-verify.js +66 -12
  32. package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
  33. package/dist/orchestration/stages/s9-judge.js +52 -14
  34. package/dist/orchestration/stages/s9b-plan.js +227 -48
  35. package/dist/orchestration/url-sources.js +21 -0
  36. package/dist/providers/adapter-core.js +1 -1
  37. package/dist/providers/claude.js +18 -0
  38. package/dist/schemas/index.js +112 -3
  39. package/dist/serve/flight-deck.js +830 -0
  40. package/dist/serve/followup.js +50 -0
  41. package/dist/serve/frames.js +168 -0
  42. package/dist/serve/gates.js +72 -0
  43. package/dist/serve/projections.js +283 -0
  44. package/dist/serve/server.js +219 -0
  45. package/dist/serve/threads.js +145 -0
  46. package/dist/serve-ui/Five.png +0 -0
  47. package/dist/serve-ui/app.js +820 -0
  48. package/dist/serve-ui/index.html +171 -0
  49. package/dist/serve-ui/workspace.css +662 -0
  50. package/dist/skills/idea-refinement/analyst.md +5 -5
  51. package/dist/skills/idea-refinement/chair.md +18 -0
  52. package/dist/skills/idea-refinement/economics-delivery.md +11 -0
  53. package/dist/skills/idea-refinement/market-adoption.md +12 -0
  54. package/dist/skills/idea-refinement/planner.md +25 -19
  55. package/dist/skills/idea-refinement/rebuttal.md +15 -0
  56. package/dist/skills/idea-refinement/verifier.md +17 -0
  57. package/dist/storage/runs.js +2 -1
  58. package/dist/workflows/idea-refinement.js +130 -24
  59. package/package.json +2 -2
@@ -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 } from '../../schemas/index.js';
5
5
  import { z } from 'zod';
6
6
  import { BudgetExceeded, isFatal } from '../context.js';
7
- import { jsonCall } from '../jsonStage.js';
7
+ import { coerceToSchema, 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,43 @@ 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
+ /** v6: requested deliverables the accepted plan still lacks — the caller decides whether that is
239
+ * worth a repair call or an honest `plan_partial` flag. Never a reason to discard the plan. */
240
+ export function missingDeliverables(plan, requestedOutputs) {
241
+ return requestedOutputs.filter((output) => (output === 'FEATURE_BACKLOG' && !plan.feature_backlog)
242
+ || (output === 'IMPLEMENTATION_PLAN' && !plan.implementation_plan));
243
+ }
244
+ export function anchoredActionPlan(plan, anchors, requestedOutputs = [], requireReaderBrief = false) {
245
+ if (requireReaderBrief && !plan.reader_brief)
246
+ return null;
247
+ // v6: a reader brief citing known claim ids is KEPT — sanitizeReaderText already substitutes
248
+ // labels at render time; rejecting here cost run f740 a paid repair for zero reader benefit.
249
+ const readerBrief = plan.reader_brief ? {
250
+ ...plan.reader_brief,
251
+ source_ids: normalizeSourceIds(plan.reader_brief.source_ids, anchors.sourceIds),
252
+ } : undefined;
114
253
  const actions = plan.actions
115
254
  .filter((a) => validAnchor(a.validates, anchors))
116
255
  .sort((a, b) => a.order - b.order)
256
+ .slice(0, requireReaderBrief ? 4 : 7)
117
257
  .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)
258
+ if (actions.length === 0 && !requireReaderBrief)
123
259
  return null;
124
260
  return {
125
261
  actions,
126
262
  sequencing_note: plan.sequencing_note,
127
263
  ...(requestedOutputs.includes('FEATURE_BACKLOG') && plan.feature_backlog ? { feature_backlog: plan.feature_backlog } : {}),
128
264
  ...(requestedOutputs.includes('IMPLEMENTATION_PLAN') && plan.implementation_plan ? { implementation_plan: plan.implementation_plan } : {}),
265
+ ...(readerBrief ? { reader_brief: readerBrief } : {}),
129
266
  };
130
267
  }
131
268
  export function normalizeEffort(raw) {
@@ -140,8 +277,20 @@ export function normalizeEffort(raw) {
140
277
  const days = amount * (unit === 'minute' ? 1 / 1440 : unit === 'hour' ? 1 / 24 : unit === 'day' ? 1 : unit === 'week' ? 7 : unit === 'month' ? 30 : 365);
141
278
  return days <= 2 ? 'S' : days <= 14 ? 'M' : 'L';
142
279
  }
143
- export function normalizePlannerOutput(plan) {
144
- return ActionPlan.parse({
280
+ 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;
281
+ 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;
282
+ function hasExplicitSchedule(context) {
283
+ return EXPLICIT_SCHEDULE.test(JSON.stringify({
284
+ original_request: context.original_request,
285
+ constraints: context.constraints,
286
+ success_criteria: context.success_criteria,
287
+ chair: context.chair,
288
+ supported_findings: context.supported_findings,
289
+ sources: context.sources.map((source) => source.supports),
290
+ }));
291
+ }
292
+ export function normalizePlannerOutput(plan, calendarAllowed = true) {
293
+ const built = {
145
294
  actions: plan.actions.map((action, i) => ({
146
295
  ...action,
147
296
  order: i + 1,
@@ -149,8 +298,23 @@ export function normalizePlannerOutput(plan) {
149
298
  })),
150
299
  sequencing_note: plan.sequencing_note,
151
300
  ...(plan.feature_backlog ? { feature_backlog: plan.feature_backlog } : {}),
152
- ...(plan.implementation_plan ? { implementation_plan: plan.implementation_plan } : {}),
153
- });
301
+ ...(plan.implementation_plan ? { implementation_plan: {
302
+ milestones: plan.implementation_plan.milestones.map((milestone, index) => ({
303
+ ...milestone,
304
+ timebox: !calendarAllowed && CALENDAR_TIMEBOX.test(milestone.timebox) ? `Phase ${index + 1}` : milestone.timebox,
305
+ })),
306
+ } } : {}),
307
+ ...(plan.reader_brief ? { reader_brief: plan.reader_brief } : {}),
308
+ };
309
+ const parsed = ActionPlan.safeParse(built);
310
+ if (parsed.success)
311
+ return parsed.data;
312
+ // v6 deterministic floor for offline/replay callers (jsonCall applies the same floor live):
313
+ // clip over-cap strings, truncate over-cap arrays — never discard a complete plan over cosmetics.
314
+ const eased = ActionPlan.safeParse(coerceToSchema(ActionPlan, built, true));
315
+ if (eased.success)
316
+ return eased.data;
317
+ return ActionPlan.parse(built);
154
318
  }
155
319
  function unavailablePlan(reason, contract, risks, blindSpots, openQuestions) {
156
320
  const unresolved = openQuestions.length ? openQuestions : [
@@ -163,16 +327,20 @@ function unavailablePlan(reason, contract, risks, blindSpots, openQuestions) {
163
327
  unresolved_questions: (unresolved.length ? unresolved : [`What evidence would change the verdict for ${clip(contract.task, 100)}?`]).slice(0, 10),
164
328
  };
165
329
  }
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);
330
+ export async function s9bPlan(ctx, contract, seats, graph, judgeReport, originalRequest = contract.task) {
331
+ const answerContext = buildAnswerContext({ originalRequest, contract, seats, graph, judgeReport, flags: ctx.flags });
332
+ const openQuestions = answerContext.open_questions;
333
+ const requestedOutputs = answerContext.requested_outputs;
334
+ const risks = answerContext.upheld_risks;
335
+ const blindSpots = answerContext.blind_spots;
336
+ const sourceIds = graph.evidence.map((evidence) => evidence.id);
337
+ const requireReaderBrief = 'success_bar' in contract;
172
338
  const anchors = {
173
339
  claimIds: risks.map((r) => r.id),
340
+ knownReaderIds: graph.claims.map((claim) => claim.id),
174
341
  blindSpots,
175
342
  openQuestions,
343
+ sourceIds,
176
344
  };
177
345
  const fallback = async (flag) => {
178
346
  ctx.addFlag(flag);
@@ -182,44 +350,55 @@ export async function s9bPlan(ctx, contract, seats, graph, judgeReport) {
182
350
  };
183
351
  if (ctx.budget.limit - ctx.budget.used < 1)
184
352
  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'));
353
+ const prompt = buildActionPlannerPrompt(answerContext, loadSkill('idea-refinement', 'planner'));
354
+ const calendarAllowed = hasExplicitSchedule(answerContext);
355
+ // v6: a plan the model actually produced is never replaced by PlannerUnavailable. Complete →
356
+ // accept; missing a requested deliverable → one repair when budget allows, else accept partial
357
+ // with an honest `plan_partial` flag. PlannerUnavailable is reserved for nothing-parseable.
358
+ const accept = async (plan) => {
359
+ if (missingDeliverables(plan, requestedOutputs).length)
360
+ ctx.addFlag('plan_partial');
361
+ await ctx.writer.writeJson('action-plan', plan);
362
+ return plan;
363
+ };
364
+ let anchored = null;
195
365
  try {
196
366
  const first = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-plan', prompt, PlannerOutput, {
197
367
  repair: ctx.budget.limit - ctx.budget.used >= 2,
198
- }));
199
- const anchored = anchoredActionPlan(first, anchors, requestedOutputs);
200
- if (anchored) {
368
+ }), calendarAllowed);
369
+ anchored = anchoredActionPlan(first, anchors, requestedOutputs, requireReaderBrief);
370
+ if (anchored && missingDeliverables(anchored, requestedOutputs).length === 0) {
201
371
  await ctx.writer.writeJson('action-plan', anchored);
202
372
  return anchored;
203
373
  }
204
- if (ctx.budget.limit - ctx.budget.used < 1)
374
+ if (ctx.budget.limit - ctx.budget.used < 1) {
375
+ if (anchored)
376
+ return accept(anchored);
205
377
  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` +
378
+ }
379
+ const repair = `${prompt}\n\n---\nYour previous response had invalid anchors, omitted a requested deliverable or reader_brief, or cited an unknown source id.\n` +
207
380
  `Valid graph claim ids: ${anchors.claimIds.join(', ') || '(none)'}\n` +
208
381
  `Valid blind spots: ${anchors.blindSpots.join(' | ') || '(none)'}\n` +
209
382
  `Valid open questions: ${anchors.openQuestions.join(' | ') || '(none)'}\n` +
383
+ `Valid source ids: ${anchors.sourceIds.join(', ') || '(none)'}\n` +
210
384
  `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);
214
- if (repairedAnchored) {
385
+ `Output ONLY corrected JSON with every action anchored, reader_brief present, and every requested deliverable present.`;
386
+ const repaired = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-anchor-repair', repair, PlannerOutput, { repair: false }), calendarAllowed);
387
+ const repairedAnchored = anchoredActionPlan(repaired, anchors, requestedOutputs, requireReaderBrief);
388
+ if (repairedAnchored && missingDeliverables(repairedAnchored, requestedOutputs).length === 0) {
215
389
  await ctx.writer.writeJson('action-plan', repairedAnchored);
216
390
  return repairedAnchored;
217
391
  }
392
+ const best = repairedAnchored ?? anchored;
393
+ if (best)
394
+ return accept(best);
218
395
  return fallback('plan_fallback');
219
396
  }
220
397
  catch (e) {
221
398
  if (isFatal(e) && !(e instanceof BudgetExceeded))
222
399
  throw e;
400
+ if (anchored)
401
+ return accept(anchored);
223
402
  return fallback('plan_fallback');
224
403
  }
225
404
  }
@@ -56,6 +56,13 @@ async function assertPublicUrl(url, resolveHostname) {
56
56
  throw new Error('private or local URLs are not allowed');
57
57
  }
58
58
  }
59
+ /** Validate an attached URL before the browser shows a FETCH_URL permission card. This performs
60
+ * the same public-network guard as snapshotting, but does not fetch the page. */
61
+ export async function validatePublicUrl(raw, resolveHostname = true) {
62
+ const url = new URL(raw);
63
+ await assertPublicUrl(url, resolveHostname);
64
+ return url.toString();
65
+ }
59
66
  function npmRegistryUrl(url) {
60
67
  if (url.hostname !== 'npmjs.com' && url.hostname !== 'www.npmjs.com')
61
68
  return undefined;
@@ -198,3 +205,17 @@ export async function snapshotUrlSources(input, options = {}) {
198
205
  const sources = await Promise.all(extractPublicUrls(input).map((url, index) => snapshotOne(url, index, options)));
199
206
  return UrlSourceSet.parse({ sources });
200
207
  }
208
+ /** v6 T10: a URL the user attached is presumptively decision-relevant — if it cannot be read, the
209
+ * run must stop BEFORE any paid call and ask, instead of spending the full council budget on a
210
+ * conditional verdict (run f740 burned 12 calls around a 403'd hackathon page). Returns the stop
211
+ * message, or null to proceed. Quick mode and an explicit override proceed as before. */
212
+ export function blockedSourceStop(sources, mode, allowBlockedSources) {
213
+ if (mode === 'quick' || allowBlockedSources)
214
+ return null;
215
+ const unreadable = sources.sources.filter((source) => source.status !== 'FETCHED');
216
+ if (unreadable.length === 0)
217
+ return null;
218
+ const details = unreadable.map((source) => `${source.url} (${source.status}${source.error ? `: ${source.error}` : ''})`).join('; ');
219
+ return `a source you attached could not be read — ${details}. The council would have to decide without the fact you attached it for. `
220
+ + 'Paste the relevant text into your idea input and rerun, or rerun with --allow-blocked-sources to proceed without it (the report will be conditional).';
221
+ }
@@ -118,7 +118,7 @@ export async function runAdapter(spec, req, flags, deps = {}) {
118
118
  return { ok: false, error: 'BAD_OUTPUT', stderrTail: raw.stderr, durationMs: raw.durationMs };
119
119
  }
120
120
  }
121
- return { ok: true, text, json, durationMs: raw.durationMs, providerMeta: spec.meta?.(raw.stdout) };
121
+ return { ok: true, text, json, durationMs: raw.durationMs, providerMeta: spec.meta?.(raw.stdout), usage: spec.usage?.(raw.stdout, raw.stderr) };
122
122
  };
123
123
  const first = await attempt();
124
124
  if (first.ok)
@@ -1,4 +1,8 @@
1
1
  import { runAdapter } from './adapter-core.js';
2
+ /** Only accept a finite non-negative number; anything else → undefined (field omitted). */
3
+ function num(v) {
4
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : undefined;
5
+ }
2
6
  function envelope(stdout) {
3
7
  try {
4
8
  const v = JSON.parse(stdout);
@@ -49,6 +53,20 @@ const claudeSpec = {
49
53
  usage: env.usage,
50
54
  };
51
55
  },
56
+ usage(stdout) {
57
+ const env = envelope(stdout);
58
+ if (!env)
59
+ return undefined;
60
+ const u = (env.usage ?? {});
61
+ return {
62
+ inputTokens: num(u.input_tokens),
63
+ outputTokens: num(u.output_tokens),
64
+ cacheReadTokens: num(u.cache_read_input_tokens),
65
+ cacheWriteTokens: num(u.cache_creation_input_tokens),
66
+ estimated: false,
67
+ reportedCostUsd: num(env.total_cost_usd),
68
+ };
69
+ },
52
70
  };
53
71
  export const claude = {
54
72
  id: 'claude',