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.
- package/CHANGELOG.md +23 -0
- package/README.md +16 -8
- package/dist/bench/idea-v3-rating.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/run.js +10 -7
- package/dist/council/view.js +146 -46
- package/dist/orchestration/context.js +3 -3
- package/dist/orchestration/decision-dossier.js +467 -80
- package/dist/orchestration/decision-graph.js +31 -0
- package/dist/orchestration/legacy-idea-adapter.js +3 -0
- package/dist/orchestration/modes.js +16 -4
- package/dist/orchestration/preflight.js +43 -9
- package/dist/orchestration/quick-analysis.js +35 -6
- package/dist/orchestration/stages/s10-render.js +179 -79
- package/dist/orchestration/stages/s4-analyze.js +3 -1
- 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 +26 -9
- package/dist/orchestration/stages/s8b-rebuttal.js +11 -4
- package/dist/orchestration/stages/s9-judge.js +36 -13
- package/dist/orchestration/stages/s9b-plan.js +208 -35
- package/dist/orchestration/url-sources.js +200 -0
- package/dist/schemas/index.js +134 -6
- 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/tui/app.js +19 -4
- package/dist/workflows/idea-refinement.js +51 -15
- 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 {
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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()).
|
|
19
|
+
}).strict()).max(7),
|
|
19
20
|
sequencing_note: z.string().min(1),
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
149
|
-
const
|
|
150
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
`
|
|
189
|
-
|
|
190
|
-
|
|
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;
|