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.
- package/CHANGELOG.md +80 -5
- package/README.md +73 -15
- package/dist/bench/idea-v3-bench.js +104 -5
- package/dist/bench/idea-v3-rating.js +159 -4
- package/dist/cli/bench.js +5 -5
- package/dist/cli/index.js +12 -2
- package/dist/cli/resume.js +20 -0
- package/dist/cli/run.js +76 -5
- package/dist/cli/serve.js +48 -0
- package/dist/config/config.js +7 -2
- package/dist/council/view.js +64 -15
- package/dist/orchestration/auto-profile.js +97 -0
- package/dist/orchestration/claim-groups.js +56 -0
- package/dist/orchestration/context.js +69 -6
- package/dist/orchestration/decision-dossier.js +450 -89
- package/dist/orchestration/decision-graph.js +33 -2
- package/dist/orchestration/engine.js +8 -4
- package/dist/orchestration/evidence-origin.js +17 -0
- package/dist/orchestration/jsonStage.js +47 -5
- package/dist/orchestration/legacy-idea-adapter.js +3 -0
- package/dist/orchestration/modes.js +18 -10
- package/dist/orchestration/preflight.js +36 -3
- package/dist/orchestration/quick-analysis.js +29 -7
- package/dist/orchestration/sanitize-paths.js +10 -0
- package/dist/orchestration/stages/s10-render.js +196 -84
- package/dist/orchestration/stages/s4-analyze.js +10 -2
- package/dist/orchestration/stages/s4b-challenge.js +97 -0
- package/dist/orchestration/stages/s5-drift.js +13 -3
- package/dist/orchestration/stages/s6-positions.js +18 -0
- package/dist/orchestration/stages/s7-decision-graph.js +3 -0
- package/dist/orchestration/stages/s8-verify.js +66 -12
- package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
- package/dist/orchestration/stages/s9-judge.js +52 -14
- package/dist/orchestration/stages/s9b-plan.js +227 -48
- package/dist/orchestration/url-sources.js +21 -0
- package/dist/providers/adapter-core.js +1 -1
- package/dist/providers/claude.js +18 -0
- package/dist/schemas/index.js +112 -3
- package/dist/serve/flight-deck.js +830 -0
- package/dist/serve/followup.js +50 -0
- package/dist/serve/frames.js +168 -0
- package/dist/serve/gates.js +72 -0
- package/dist/serve/projections.js +283 -0
- package/dist/serve/server.js +219 -0
- package/dist/serve/threads.js +145 -0
- package/dist/serve-ui/Five.png +0 -0
- package/dist/serve-ui/app.js +820 -0
- package/dist/serve-ui/index.html +171 -0
- package/dist/serve-ui/workspace.css +662 -0
- package/dist/skills/idea-refinement/analyst.md +5 -5
- package/dist/skills/idea-refinement/chair.md +18 -0
- package/dist/skills/idea-refinement/economics-delivery.md +11 -0
- package/dist/skills/idea-refinement/market-adoption.md +12 -0
- package/dist/skills/idea-refinement/planner.md +25 -19
- package/dist/skills/idea-refinement/rebuttal.md +15 -0
- package/dist/skills/idea-refinement/verifier.md +17 -0
- package/dist/storage/runs.js +2 -1
- package/dist/workflows/idea-refinement.js +130 -24
- 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()).
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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:
|
|
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
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const risks =
|
|
171
|
-
const blindSpots =
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/providers/claude.js
CHANGED
|
@@ -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',
|