aiki-cli 0.3.2 → 0.3.5
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 +68 -0
- package/README.md +64 -4
- package/dist/bench/idea-v3-bench.js +104 -5
- package/dist/bench/idea-v3-rating.js +158 -3
- 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 +75 -4
- package/dist/cli/serve.js +48 -0
- package/dist/config/config.js +7 -2
- package/dist/council/view.js +13 -4
- package/dist/orchestration/auto-profile.js +97 -0
- package/dist/orchestration/claim-groups.js +56 -0
- package/dist/orchestration/context.js +66 -3
- package/dist/orchestration/decision-dossier.js +42 -10
- package/dist/orchestration/decision-graph.js +2 -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/modes.js +4 -2
- package/dist/orchestration/preflight.js +19 -0
- package/dist/orchestration/quick-analysis.js +31 -6
- package/dist/orchestration/sanitize-paths.js +10 -0
- package/dist/orchestration/stages/s10-render.js +31 -7
- package/dist/orchestration/stages/s4-analyze.js +7 -1
- package/dist/orchestration/stages/s4b-challenge.js +97 -0
- package/dist/orchestration/stages/s5-drift.js +13 -3
- package/dist/orchestration/stages/s8-verify.js +44 -7
- package/dist/orchestration/stages/s9-judge.js +20 -5
- package/dist/orchestration/stages/s9b-plan.js +44 -15
- 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 +46 -0
- 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/storage/runs.js +2 -1
- package/dist/workflows/idea-refinement.js +94 -16
- package/package.json +2 -2
|
@@ -7,7 +7,8 @@ import { ReaderBrief as ReaderBriefSchema, readerBriefIssues } from '../../schem
|
|
|
7
7
|
import { DISPLAY_NAME } from '../../providers/types.js';
|
|
8
8
|
import { overlap, tokenize } from '../cluster.js';
|
|
9
9
|
import { interpretClaimOutcome } from '../decision-graph.js';
|
|
10
|
-
import {
|
|
10
|
+
import { evidenceOrigin } from '../evidence-origin.js';
|
|
11
|
+
import { buildReaderProjection, formatTokenLine, renderDecisionDossierMarkdown, sanitizeReaderText } from '../decision-dossier.js';
|
|
11
12
|
import { callCategory } from '../modes.js';
|
|
12
13
|
/** Pure graph-backed decision audit. */
|
|
13
14
|
export function deriveAudit(graph, judgeReport) {
|
|
@@ -90,8 +91,10 @@ export function computeConfidence(graph, flags) {
|
|
|
90
91
|
? verificationClaims.filter((claim) => claim.evidence_state === 'SUPPORTED').length / verificationClaims.length : 0;
|
|
91
92
|
const independentConvergence = graph.claims.length
|
|
92
93
|
? graph.claims.filter((claim) => claim.state === 'CONSENSUS' || claim.state === 'SHARED_CONCERN').length / graph.claims.length : 0;
|
|
94
|
+
// v6: only independent EXTERNAL evidence counts as quality — the user's own material restated
|
|
95
|
+
// as cards inflated f740's coverage to theater (8 of 12 cards were the user's idea-brief).
|
|
93
96
|
const evidenceQuality = graph.evidence.length
|
|
94
|
-
? graph.evidence.filter((card) => card
|
|
97
|
+
? graph.evidence.filter((card) => evidenceOrigin(card) === 'EXTERNAL').length / graph.evidence.length : 0;
|
|
95
98
|
const stability = Math.max(0, 1 - 0.25 * DEGRADATION_FLAGS.filter((flag) => flags.has(flag)).length);
|
|
96
99
|
const criticalRiskPenalty = Math.min(20, 5 * loadBearing.filter((claim) => claim.if_false === 'STOP' && claim.evidence_state !== 'SUPPORTED').length);
|
|
97
100
|
let score = Math.round(verificationCoverage * 40 + independentConvergence * 25 + evidenceQuality * 20 + stability * 15 - criticalRiskPenalty);
|
|
@@ -440,6 +443,8 @@ function buildDossier(args) {
|
|
|
440
443
|
export function buildDecisionReport(ctx, args) {
|
|
441
444
|
const { contract, seats, graph, verifications, judgeReport, actionPlan } = args;
|
|
442
445
|
const mode = ctx.mode ?? 'council';
|
|
446
|
+
const adaptiveAuto = ctx.isAuto === true;
|
|
447
|
+
const autoEscalationReasons = ctx.autoEscalationReasons ?? [];
|
|
443
448
|
const flags = new Set(ctx.flags);
|
|
444
449
|
const newDecisionContract = 'success_bar' in contract;
|
|
445
450
|
const hasReaderBrief = Boolean(actionPlan && !('kind' in actionPlan) && actionPlan.reader_brief);
|
|
@@ -494,7 +499,7 @@ export function buildDecisionReport(ctx, args) {
|
|
|
494
499
|
.filter((position) => position.stance === 'OPPOSE')
|
|
495
500
|
.map((position) => ({ provider: disp(position.provider), proposition: position.proposition }));
|
|
496
501
|
const unresolvedDecisive = disagreements.some((d) => d.status === 'UNRESOLVED' && claimById.get(d.id)?.sensitivity === 'DECISIVE');
|
|
497
|
-
const consensusType = mode === 'quick' ? 'single_analyst' : disagreements.length === 0
|
|
502
|
+
const consensusType = adaptiveAuto || mode === 'quick' ? 'single_analyst' : disagreements.length === 0
|
|
498
503
|
? (claims.some((claim) => claim.ruling === 'UNRESOLVED') ? 'convergent_with_unresolved_claims' : 'unanimous')
|
|
499
504
|
: disagreements.every((d) => d.status === 'RESOLVED') ? 'majority_with_dissent' : 'contested';
|
|
500
505
|
// Frame the slot as what it is — a decisive assumption that is NOT proven. Echoing the bare
|
|
@@ -538,16 +543,29 @@ export function buildDecisionReport(ctx, args) {
|
|
|
538
543
|
});
|
|
539
544
|
const byProvider = {};
|
|
540
545
|
let modelTimeMs = 0;
|
|
546
|
+
let inputTokens = 0, outputTokens = 0, estimatedCalls = 0;
|
|
541
547
|
for (const call of ctx.calls) {
|
|
542
548
|
byProvider[call.provider] = (byProvider[call.provider] ?? 0) + 1;
|
|
543
549
|
modelTimeMs += call.durationMs;
|
|
550
|
+
if (call.usage) {
|
|
551
|
+
inputTokens += call.usage.inputTokens ?? 0;
|
|
552
|
+
outputTokens += call.usage.outputTokens ?? 0;
|
|
553
|
+
if (call.usage.estimated)
|
|
554
|
+
estimatedCalls++;
|
|
555
|
+
}
|
|
544
556
|
}
|
|
545
557
|
const roles = ctx.roles;
|
|
546
|
-
const reportProviders =
|
|
558
|
+
const reportProviders = adaptiveAuto
|
|
559
|
+
? [...new Set([...seats.map((seat) => seat.provider), ...ctx.calls.map((call) => call.provider)])]
|
|
560
|
+
: mode === 'quick' ? [...new Set(seats.map((seat) => seat.provider))] : ctx.available();
|
|
547
561
|
const models = reportProviders.map((provider) => ({
|
|
548
562
|
provider,
|
|
549
563
|
name: disp(provider),
|
|
550
|
-
roles:
|
|
564
|
+
roles: adaptiveAuto ? [
|
|
565
|
+
...(seats.some((seat) => seat.provider === provider) ? ['primary analyst'] : []),
|
|
566
|
+
...(ctx.calls.some((call) => call.provider === provider && call.stage.startsWith('P0-')) ? ['preflight reader'] : []),
|
|
567
|
+
...(ctx.calls.some((call) => call.provider === provider && call.stage === 'S4b') ? ['delta challenger'] : []),
|
|
568
|
+
] : mode === 'quick' ? ['single analyst'] : [
|
|
551
569
|
...(roles.analyst === provider ? ['analyst'] : []),
|
|
552
570
|
...(roles.judge === provider ? ['judge'] : []),
|
|
553
571
|
...(roles.verifier === provider ? ['verifier'] : []),
|
|
@@ -619,6 +637,10 @@ export function buildDecisionReport(ctx, args) {
|
|
|
619
637
|
reportId: ctx.runId,
|
|
620
638
|
generatedAt: new Date().toISOString(),
|
|
621
639
|
mode,
|
|
640
|
+
...(ctx.fastPath ? { fastPath: true } : {}),
|
|
641
|
+
...(adaptiveAuto ? { adaptiveAuto: true } : {}),
|
|
642
|
+
...(autoEscalationReasons.length ? { autoEscalationReasons } : {}),
|
|
643
|
+
...(adaptiveAuto && ctx.attemptedStages?.includes('S4b') ? { autoChallengeUsed: true } : {}),
|
|
622
644
|
task: {
|
|
623
645
|
original: args.original ?? contract.task,
|
|
624
646
|
normalized: contract.task,
|
|
@@ -669,7 +691,7 @@ export function buildDecisionReport(ctx, args) {
|
|
|
669
691
|
: [],
|
|
670
692
|
openQuestions,
|
|
671
693
|
flags: [...flags],
|
|
672
|
-
receipt: { calls: ctx.calls.length, budget: ctx.budget.limit, byProvider, modelTimeMs, categories: receiptCategories(ctx) },
|
|
694
|
+
receipt: { calls: ctx.calls.length, budget: ctx.budget.limit, byProvider, modelTimeMs, categories: receiptCategories(ctx), tokens: { inputTokens, outputTokens, estimatedCalls } },
|
|
673
695
|
dossier,
|
|
674
696
|
};
|
|
675
697
|
}
|
|
@@ -690,7 +712,8 @@ export function renderTerminalSummary(report, paths) {
|
|
|
690
712
|
const takeaways = [...new Set(candidates.map(terminalLine).filter(Boolean))].slice(0, 3);
|
|
691
713
|
const nextStep = projection?.nextStep ?? report.recommendedActions[0]?.action ?? 'Open the report and choose the smallest decisive next action.';
|
|
692
714
|
return [
|
|
693
|
-
report.
|
|
715
|
+
report.adaptiveAuto ? 'AIKI · ADAPTIVE DECISION'
|
|
716
|
+
: report.mode === 'quick' ? 'AIKI · SINGLE-MODEL DECISION' : 'AIKI · COUNCIL DECISION',
|
|
694
717
|
RULE,
|
|
695
718
|
`Verdict: ${terminalLine(projection?.headline ?? report.verdict.summary)}`,
|
|
696
719
|
terminalLine(projection?.bottomLine ?? report.verdict.primaryReason),
|
|
@@ -700,6 +723,7 @@ export function renderTerminalSummary(report, paths) {
|
|
|
700
723
|
...takeaways.map((item) => `- ${item}`),
|
|
701
724
|
'',
|
|
702
725
|
`Next step: ${terminalLine(nextStep)}`,
|
|
726
|
+
...(report.receipt.tokens ? [`Tokens: ${formatTokenLine(report.receipt.tokens)}`] : []),
|
|
703
727
|
`Report: ${paths.markdownPath}`,
|
|
704
728
|
].join('\n');
|
|
705
729
|
}
|
|
@@ -16,7 +16,13 @@ import { IDEA_LANES } from '../idea-lanes.js';
|
|
|
16
16
|
/** Run one analyst seat → validated `RoleOutput`, persisted to 04-role-outputs/<label>.json.
|
|
17
17
|
* `label` doubles as the artifact filename, so a resample uses a distinct label. */
|
|
18
18
|
async function runSeat(ctx, seat, label, lane, prompt) {
|
|
19
|
-
|
|
19
|
+
// v6 tail guard: an S4 repair may spend only when the worst case (both seats repairing) still
|
|
20
|
+
// leaves the reserved tail — 2 seat calls + 2 repairs + 3 (chair, planner, one tail repair).
|
|
21
|
+
// When disallowed, jsonCall's salvage floor keeps the seat alive (proven on the f740 payloads).
|
|
22
|
+
const model = await jsonCall(ctx, ctx.handle(seat), `S4-${label}`, prompt, IdeaRoleOutputModel, {
|
|
23
|
+
salvage: salvageIdeaRoleOutputModel,
|
|
24
|
+
repair: ctx.budget.limit - ctx.budget.used >= 7,
|
|
25
|
+
});
|
|
20
26
|
const output = { workflow: 'idea-refinement', ...model };
|
|
21
27
|
await ctx.writer.writeRoleOutput(label, output);
|
|
22
28
|
return { provider: seat, sample: label, lane, output };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// v7 Phase D — one auto-only delta challenge over hard-gated claim packets.
|
|
2
|
+
import { ChallengeDeltaSet as ChallengeDeltaSetSchema, } from '../../schemas/index.js';
|
|
3
|
+
import { canProduceNewInformation, } from '../auto-profile.js';
|
|
4
|
+
import { isFatal, StageError } from '../context.js';
|
|
5
|
+
import { jsonCall } from '../jsonStage.js';
|
|
6
|
+
import { buildClaimPackets } from './s8-verify.js';
|
|
7
|
+
const CHALLENGE_PROMPT = `ROLE: Independent delta challenger. You see only the targeted anonymous claim
|
|
8
|
+
packets below, not the full task. Confirm, counter, narrow, replace, or leave each claim unresolved.
|
|
9
|
+
|
|
10
|
+
Output ONLY JSON:
|
|
11
|
+
{"deltas":[{"claimId":"<G-id>","response":"CONFIRM|COUNTER|NARROW|REPLACE|UNRESOLVED",
|
|
12
|
+
"reasoning":"<precise reason>","newEvidenceIds":["<E-id>"],
|
|
13
|
+
"changedDecisionImpact":"<what changes, or why the decision stays stable>"}]}
|
|
14
|
+
|
|
15
|
+
Return exactly one delta per packet. Cite only evidence IDs shown in that packet. Do not invent a
|
|
16
|
+
source, URL, claim ID, or evidence ID. JSON only.
|
|
17
|
+
TARGET CLAIM PACKETS: {{PACKETS_JSON}}`;
|
|
18
|
+
export function buildChallengePrompt(packets) {
|
|
19
|
+
return CHALLENGE_PROMPT.replace('{{PACKETS_JSON}}', JSON.stringify(packets, null, 2));
|
|
20
|
+
}
|
|
21
|
+
function validateAndTranslate(result, targetIds, evidenceRefs, allowedEvidence) {
|
|
22
|
+
const expected = new Set(targetIds);
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const issues = [];
|
|
25
|
+
for (const delta of result.deltas) {
|
|
26
|
+
if (!expected.has(delta.claimId))
|
|
27
|
+
issues.push(`unknown claim id: ${delta.claimId}`);
|
|
28
|
+
if (seen.has(delta.claimId))
|
|
29
|
+
issues.push(`duplicate claim delta: ${delta.claimId}`);
|
|
30
|
+
seen.add(delta.claimId);
|
|
31
|
+
const allowed = allowedEvidence.get(delta.claimId) ?? new Set();
|
|
32
|
+
for (const id of delta.newEvidenceIds)
|
|
33
|
+
if (!allowed.has(id))
|
|
34
|
+
issues.push(`invalid evidence id for ${delta.claimId}: ${id}`);
|
|
35
|
+
}
|
|
36
|
+
for (const id of expected)
|
|
37
|
+
if (!seen.has(id))
|
|
38
|
+
issues.push(`missing claim delta: ${id}`);
|
|
39
|
+
if (issues.length)
|
|
40
|
+
throw new StageError('S4b', 'BAD_OUTPUT', issues.join('; '));
|
|
41
|
+
return {
|
|
42
|
+
deltas: result.deltas.map((delta) => ({
|
|
43
|
+
...delta,
|
|
44
|
+
newEvidenceIds: delta.newEvidenceIds.map((id) => evidenceRefs.get(id)),
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function challengerProvider(ctx, primaryProvider) {
|
|
49
|
+
return [...new Set([ctx.roles.verifier, ...ctx.roles.s4, ...ctx.available()])]
|
|
50
|
+
.find((provider) => provider !== primaryProvider && ctx.available().includes(provider));
|
|
51
|
+
}
|
|
52
|
+
/** At most one provider call; no repair call and no work for an information-free target. */
|
|
53
|
+
export async function s4bChallenge(ctx, graph, gates, primaryProvider) {
|
|
54
|
+
const byClaim = new Map(gates.map((gate) => [gate.claimId, gate]));
|
|
55
|
+
const targets = [...byClaim.values()]
|
|
56
|
+
.filter((gate) => graph.claims.some((claim) => claim.id === gate.claimId))
|
|
57
|
+
.filter((gate) => canProduceNewInformation(graph, gate.claimId))
|
|
58
|
+
.slice(0, 3);
|
|
59
|
+
const provider = challengerProvider(ctx, primaryProvider);
|
|
60
|
+
if (!targets.length || !provider)
|
|
61
|
+
return { deltas: [] };
|
|
62
|
+
const packets = buildClaimPackets(graph, targets.map((gate) => ({
|
|
63
|
+
claim_id: gate.claimId,
|
|
64
|
+
kind: gate.kind,
|
|
65
|
+
})));
|
|
66
|
+
try {
|
|
67
|
+
const result = await jsonCall(ctx, ctx.handle(provider), 'S4b', buildChallengePrompt(packets.packets), ChallengeDeltaSetSchema, { repair: false });
|
|
68
|
+
const translated = validateAndTranslate(result, targets.map((gate) => gate.claimId), packets.evidenceRefs, packets.allowedEvidence);
|
|
69
|
+
await ctx.writer.writeJson('challenge-deltas', translated);
|
|
70
|
+
return translated;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (isFatal(error))
|
|
74
|
+
throw error;
|
|
75
|
+
const empty = { deltas: [] };
|
|
76
|
+
await ctx.writer.writeJson('challenge-deltas', empty);
|
|
77
|
+
return empty;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Derived graph overlay; the stored S7 artifact and every non-target claim stay untouched. */
|
|
81
|
+
export function overlayChallengeDeltas(graph, deltas) {
|
|
82
|
+
if (!deltas.length)
|
|
83
|
+
return graph;
|
|
84
|
+
const stateById = new Map(deltas.map((delta) => [delta.claimId,
|
|
85
|
+
delta.response === 'CONFIRM' ? 'CONSENSUS'
|
|
86
|
+
: delta.response === 'COUNTER' ? 'DISAGREEMENT'
|
|
87
|
+
: 'UNCERTAIN']));
|
|
88
|
+
return {
|
|
89
|
+
...graph,
|
|
90
|
+
claims: graph.claims.map((claim) => {
|
|
91
|
+
const state = stateById.get(claim.id);
|
|
92
|
+
if (!state || (state === 'CONSENSUS' && claim.state === 'DISAGREEMENT'))
|
|
93
|
+
return claim;
|
|
94
|
+
return { ...claim, state };
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
// contract's task? Two code checks (no model call — the §601/§532 T6 "deterministic core"; the §9
|
|
3
3
|
// "verifier spot-check" is deferred): (1) the output produced ≥1 position (an analyst that
|
|
4
4
|
// produced none did not engage the task), and (2) its `task_echo` is similar enough to the contract
|
|
5
|
-
// task (overlap coefficient ≥ DRIFT_MIN_SIMILARITY). Drifted
|
|
5
|
+
// — task + constraints + success_criteria (overlap coefficient ≥ DRIFT_MIN_SIMILARITY). Drifted
|
|
6
|
+
// outputs are excluded from everything
|
|
6
7
|
// downstream and logged; if exclusion drops the survivor count below quorum (2), the run aborts.
|
|
7
8
|
import { StageError } from '../context.js';
|
|
8
9
|
import { overlapCoefficient, tokenize } from '../cluster.js';
|
|
9
|
-
/** `task_echo` must share ≥ this fraction of its tokens with the contract
|
|
10
|
+
/** `task_echo` must share ≥ this fraction of its tokens with the contract to count as on-task.
|
|
10
11
|
* Overlap coefficient (not Jaccard) so a short echo is not penalized against a longer contract
|
|
11
12
|
* paragraph. Lenient by design — this catches an analyst that wandered off-task, not paraphrase
|
|
12
13
|
* distance. Tunable (same footnote class as the S2 clustering threshold). */
|
|
13
14
|
export const DRIFT_MIN_SIMILARITY = 0.3;
|
|
15
|
+
/** The reference token set is the contract the seat was told to address — task + constraints +
|
|
16
|
+
* success_criteria — NOT just the one-sentence task. Run 626e (2026-07-18, REAL QUORUM kill):
|
|
17
|
+
* the prompt says "restate the task" and both seats paraphrased accurately, but against the
|
|
18
|
+
* 17-token model-authored task sentence they scored 0.294/0.250 < 0.3 and the run died with 18
|
|
19
|
+
* good positions on disk. Against the full contract the same echoes score 0.680/0.500 while a
|
|
20
|
+
* genuinely off-task echo stays ≤0.08 — paraphrase passes, drift still fails, threshold untouched. */
|
|
21
|
+
function contractTokens(contract) {
|
|
22
|
+
return tokenize([contract.task, ...contract.constraints, ...contract.success_criteria].join(' '));
|
|
23
|
+
}
|
|
14
24
|
export async function s5Drift(ctx, contract, seats, minSeats = 2) {
|
|
15
|
-
const taskTokens =
|
|
25
|
+
const taskTokens = contractTokens(contract);
|
|
16
26
|
const entries = [];
|
|
17
27
|
const kept = [];
|
|
18
28
|
const excluded = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// S8 — independently verify graph-selected claims from stored evidence/calculation references.
|
|
2
2
|
// Provider identity stays hidden; model output is translated back to graph evidence IDs before S9.
|
|
3
3
|
import { selectEscalations } from '../decision-graph.js';
|
|
4
|
+
import { sanitizeClaimGroups } from '../claim-groups.js';
|
|
4
5
|
import { ClaimVerificationSet } from '../../schemas/index.js';
|
|
5
6
|
import { isFatal, StageError } from '../context.js';
|
|
6
7
|
import { jsonCall } from '../jsonStage.js';
|
|
@@ -9,12 +10,18 @@ const S8_PROMPT = `ROLE: Independent verifier. Check each anonymous decision cla
|
|
|
9
10
|
provided evidence cards and deterministic calculation checks. Output ONLY JSON:
|
|
10
11
|
{"verifications": [{"claim_id": "<G-id>", "status": "VERIFIED|PARTIAL|CONTRADICTED|UNVERIFIABLE",
|
|
11
12
|
"reasoning": "<concise reason>", "evidence_ids": ["<E-id>"],
|
|
12
|
-
"calculation_check": "PASS|FAIL|NOT_APPLICABLE", "missing_evidence": ["<gap>"]}]
|
|
13
|
+
"calculation_check": "PASS|FAIL|NOT_APPLICABLE", "missing_evidence": ["<gap>"]}],
|
|
14
|
+
"claim_groups": [{"ids": ["<G-id>", "<G-id>"], "relation": "SAME|OPPOSES"}]}
|
|
13
15
|
VERIFIED = the proposition is supported. CONTRADICTED = it is refuted. PARTIAL = evidence is mixed or
|
|
14
16
|
incomplete. UNVERIFIABLE = the supplied sources cannot decide. Cite only supplied evidence IDs. Model
|
|
15
17
|
knowledge cannot verify a current, numeric, legal, medical, financial, market, or regulatory fact.
|
|
16
|
-
Judge each item independently; no verdict quota.
|
|
17
|
-
|
|
18
|
+
Judge each item independently; no verdict quota.
|
|
19
|
+
claim_groups: scan ALL CLAIMS below (not only ITEMS). Group ids that assert the SAME proposition in
|
|
20
|
+
different words (relation "SAME") and pair ids that directly contradict each other (relation
|
|
21
|
+
"OPPOSES"). Only group claims from different seats; use existing claim ids only; omit the field when
|
|
22
|
+
there are none. JSON only.{{SKILL}}
|
|
23
|
+
ITEMS: {{ITEMS_JSON}}
|
|
24
|
+
ALL CLAIMS (id, proposition, seat — for claim_groups only): {{CLAIMS_JSON}}`;
|
|
18
25
|
export function selectVerificationTargets(claims, max) {
|
|
19
26
|
return claims
|
|
20
27
|
.map((claim, index) => ({ claim, index }))
|
|
@@ -39,14 +46,18 @@ function evidenceIdsForClaim(graph, claimId) {
|
|
|
39
46
|
.flatMap((calculation) => calculation.inputs.flatMap((input) => input.evidence_ids));
|
|
40
47
|
return new Set([...positionEvidence, ...calculationEvidence]);
|
|
41
48
|
}
|
|
42
|
-
|
|
49
|
+
/** Slim, reusable packet seam: selected claims plus only their linked evidence/calculations. */
|
|
50
|
+
export function buildClaimPackets(graph, targets = selectVerificationEscalations(graph)) {
|
|
43
51
|
const positionById = new Map(graph.positions.map((position) => [position.id, position]));
|
|
44
52
|
const evidenceRefs = new Map(graph.evidence.map((evidence, index) => [`E${index + 1}`, evidence.id]));
|
|
45
53
|
const aliasByEvidence = new Map([...evidenceRefs].map(([alias, id]) => [id, alias]));
|
|
46
54
|
const evidenceById = new Map(graph.evidence.map((evidence) => [evidence.id, evidence]));
|
|
47
|
-
const
|
|
55
|
+
const allowedEvidence = new Map();
|
|
56
|
+
const packets = targets.map((escalation) => {
|
|
48
57
|
const claim = graph.claims.find((item) => item.id === escalation.claim_id);
|
|
49
58
|
const linkedEvidenceIds = evidenceIdsForClaim(graph, claim.id);
|
|
59
|
+
allowedEvidence.set(claim.id, new Set([...linkedEvidenceIds]
|
|
60
|
+
.map((id) => aliasByEvidence.get(id)).filter((id) => id !== undefined)));
|
|
50
61
|
return {
|
|
51
62
|
id: claim.id,
|
|
52
63
|
kind: escalation.kind,
|
|
@@ -78,9 +89,26 @@ function verifierInput(graph, skill = '') {
|
|
|
78
89
|
evidence_hole: graph.holes.evidence.find((hole) => hole.claim_id === claim.id)?.reason,
|
|
79
90
|
};
|
|
80
91
|
});
|
|
92
|
+
return { packets, evidenceRefs, allowedEvidence };
|
|
93
|
+
}
|
|
94
|
+
function verifierInput(graph, skill = '') {
|
|
95
|
+
const positionById = new Map(graph.positions.map((position) => [position.id, position]));
|
|
96
|
+
const { packets, evidenceRefs } = buildClaimPackets(graph);
|
|
97
|
+
// Anonymous seat aliases (provider identity stays hidden): S1, S2, … by first appearance.
|
|
98
|
+
const seatAlias = new Map();
|
|
99
|
+
for (const position of graph.positions) {
|
|
100
|
+
if (!seatAlias.has(position.provider))
|
|
101
|
+
seatAlias.set(position.provider, `S${seatAlias.size + 1}`);
|
|
102
|
+
}
|
|
103
|
+
const claimsIndex = graph.claims.map((claim) => ({
|
|
104
|
+
id: claim.id,
|
|
105
|
+
proposition: claim.proposition,
|
|
106
|
+
seats: [...new Set(claim.position_ids.map((id) => seatAlias.get(positionById.get(id)?.provider ?? '') ?? '?'))],
|
|
107
|
+
}));
|
|
81
108
|
const prompt = S8_PROMPT
|
|
82
109
|
.replace('{{SKILL}}', skill ? `\n\n${skill}` : '')
|
|
83
|
-
.replace('{{ITEMS_JSON}}', JSON.stringify(
|
|
110
|
+
.replace('{{ITEMS_JSON}}', JSON.stringify(packets, null, 2))
|
|
111
|
+
.replace('{{CLAIMS_JSON}}', JSON.stringify(claimsIndex, null, 2));
|
|
84
112
|
return { prompt, evidenceRefs };
|
|
85
113
|
}
|
|
86
114
|
export function buildVerifierPrompt(graph, skill = '') {
|
|
@@ -89,12 +117,19 @@ export function buildVerifierPrompt(graph, skill = '') {
|
|
|
89
117
|
/** Validate every S8 claim/evidence reference before the chair can see it. */
|
|
90
118
|
export function claimVerificationRefIssues(graph, verifications, expectedIds = selectVerificationEscalations(graph).map((item) => item.claim_id)) {
|
|
91
119
|
const expected = new Set(expectedIds);
|
|
120
|
+
const known = new Set(graph.claims.map((claim) => claim.id));
|
|
92
121
|
const evidence = new Map(graph.evidence.map((item) => [item.id, item]));
|
|
93
122
|
const seen = new Set();
|
|
94
123
|
const issues = [];
|
|
95
124
|
for (const verification of verifications.verifications) {
|
|
96
|
-
|
|
125
|
+
// A claim id absent from the graph is a dangling reference (fatal even when the caller's
|
|
126
|
+
// expected set is derived from the verifications themselves, as S9's is).
|
|
127
|
+
if (!known.has(verification.claim_id)) {
|
|
97
128
|
issues.push(`unknown claim id: ${verification.claim_id}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!expected.has(verification.claim_id))
|
|
132
|
+
issues.push(`unexpected claim verification: ${verification.claim_id}`);
|
|
98
133
|
if (seen.has(verification.claim_id))
|
|
99
134
|
issues.push(`duplicate claim verification: ${verification.claim_id}`);
|
|
100
135
|
seen.add(verification.claim_id);
|
|
@@ -158,11 +193,13 @@ export async function s8Verify(ctx, graph) {
|
|
|
158
193
|
const input = verifierInput(graph, loadSkill('idea-refinement', 'verifier'));
|
|
159
194
|
// Optional work gets no model repair: one logical optional call must remain one call.
|
|
160
195
|
const result = await jsonCall(ctx, ctx.handle(ctx.roles.verifier), 'S8', input.prompt, ClaimVerificationSet, { repair: false });
|
|
196
|
+
const claimGroups = sanitizeClaimGroups(graph, result.claim_groups);
|
|
161
197
|
const translated = {
|
|
162
198
|
verifications: result.verifications.map((verification) => ({
|
|
163
199
|
...verification,
|
|
164
200
|
evidence_ids: verification.evidence_ids.map((id) => input.evidenceRefs.get(id) ?? id),
|
|
165
201
|
})),
|
|
202
|
+
...(claimGroups.length ? { claim_groups: claimGroups } : {}),
|
|
166
203
|
};
|
|
167
204
|
const issues = claimVerificationRefIssues(graph, translated, escalations.map((item) => item.claim_id));
|
|
168
205
|
if (issues.length)
|
|
@@ -5,7 +5,7 @@ import { isFatal, StageError } from '../context.js';
|
|
|
5
5
|
import { jsonCall } from '../jsonStage.js';
|
|
6
6
|
import { claimShortLabel } from '../decision-dossier.js';
|
|
7
7
|
import { loadSkill } from '../skills.js';
|
|
8
|
-
import { claimVerificationRefIssues
|
|
8
|
+
import { claimVerificationRefIssues } from './s8-verify.js';
|
|
9
9
|
const EMPTY_REBUTTALS = {
|
|
10
10
|
round: 1,
|
|
11
11
|
selected_claim_ids: [],
|
|
@@ -148,10 +148,14 @@ export function adjudicationEvidenceViolations(report, verifications) {
|
|
|
148
148
|
return [`${item.id}: missing evidence_ids`];
|
|
149
149
|
const allowed = new Set(verification.evidence_ids);
|
|
150
150
|
const bad = item.evidence_ids.filter((id) => !allowed.has(id));
|
|
151
|
-
|
|
151
|
+
// v6: PARTIAL/UNVERIFIABLE no longer forces UNRESOLVED — the chair may rule on judgment with
|
|
152
|
+
// the caveat carried in its reasoning (run f740 lost 5 nuanced HOLDS rulings and a 125s Opus
|
|
153
|
+
// repair to the old rule). The one indefensible combination stays fatal: ruling HOLDS on
|
|
154
|
+
// positively CONTRADICTED evidence.
|
|
155
|
+
const contradicted = verification.status === 'CONTRADICTED' && (item.ruling === 'HOLDS' || item.ruling === 'UPHOLD');
|
|
152
156
|
return [
|
|
153
157
|
...(bad.length ? [`${item.id}: invalid evidence ids ${bad.join(', ')}`] : []),
|
|
154
|
-
...(
|
|
158
|
+
...(contradicted ? [`${item.id}: CONTRADICTED verification cannot rule HOLDS`] : []),
|
|
155
159
|
];
|
|
156
160
|
});
|
|
157
161
|
}
|
|
@@ -216,7 +220,10 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
|
|
|
216
220
|
])];
|
|
217
221
|
const evidenceRefs = new Map(citedEvidence.map((id, index) => [`E${index + 1}`, id]));
|
|
218
222
|
const aliasByEvidence = new Map([...evidenceRefs].map(([alias, id]) => [id, alias]));
|
|
219
|
-
|
|
223
|
+
// Chair scope = the claims S8 actually verified (see s9Judge) — recomputing the escalation
|
|
224
|
+
// selection here on the overlaid graph would ask the chair to rule on claims that have no
|
|
225
|
+
// verifier record, which adjudicationEvidenceViolations then rejects after a paid call.
|
|
226
|
+
const selectedIds = verifications.verifications.map((item) => item.claim_id);
|
|
220
227
|
const eligibleIds = judgeProvider ? adjudicableClaimIds(graph, selectedIds, judgeProvider) : selectedIds;
|
|
221
228
|
const escalationIds = new Set(eligibleIds);
|
|
222
229
|
const escalations = graph.claims.filter((claim) => escalationIds.has(claim.id)).map((claim) => {
|
|
@@ -265,7 +272,15 @@ function judgeInput(contract, graph, verifications, rubric, rebuttals, judgeProv
|
|
|
265
272
|
return { prompt, evidenceRefs };
|
|
266
273
|
}
|
|
267
274
|
export async function s9Judge(ctx, contract, graph, verifications, rubric, rebuttals = EMPTY_REBUTTALS) {
|
|
268
|
-
|
|
275
|
+
// The chair adjudicates what S8 ACTUALLY verified (persisted in 08, replay-safe) — never a
|
|
276
|
+
// re-ranked selection. S8 picks targets from the pre-overlay graph and validates its own output
|
|
277
|
+
// against them; overlayClaimGroups (built from S8's OWN claim_groups) then shifts claim states,
|
|
278
|
+
// so re-running selectVerificationEscalations on the joined graph can disagree with a selection
|
|
279
|
+
// S8 could not have predicted. Run e1ce (2026-07-18): the re-selection dropped G2 / added G7 and
|
|
280
|
+
// rejected a fully-compliant verification set. The chair also structurally requires a
|
|
281
|
+
// verification per adjudicated claim (adjudicationEvidenceViolations), so the verified set IS
|
|
282
|
+
// the chair's scope; dangling/duplicate/evidence-linkage guards below still apply.
|
|
283
|
+
const selectedIds = verifications.verifications.map((item) => item.claim_id);
|
|
269
284
|
const verificationIssues = claimVerificationRefIssues(graph, verifications, selectedIds);
|
|
270
285
|
if (verificationIssues.length)
|
|
271
286
|
throw new StageError('S9', 'BAD_OUTPUT', `invalid verification references: ${verificationIssues.join('; ')}`);
|
|
@@ -1,10 +1,10 @@
|
|
|
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, ReaderBrief
|
|
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
10
|
import { interpretClaimOutcome } from '../decision-graph.js';
|
|
@@ -235,19 +235,21 @@ function normalizeSourceIds(ids, allowed) {
|
|
|
235
235
|
});
|
|
236
236
|
return [...new Set(normalized)];
|
|
237
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
|
+
}
|
|
238
244
|
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
245
|
if (requireReaderBrief && !plan.reader_brief)
|
|
244
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.
|
|
245
249
|
const readerBrief = plan.reader_brief ? {
|
|
246
250
|
...plan.reader_brief,
|
|
247
251
|
source_ids: normalizeSourceIds(plan.reader_brief.source_ids, anchors.sourceIds),
|
|
248
252
|
} : undefined;
|
|
249
|
-
if (readerBrief && readerBriefIssues(readerBrief, anchors.knownReaderIds ?? anchors.claimIds).length)
|
|
250
|
-
return null;
|
|
251
253
|
const actions = plan.actions
|
|
252
254
|
.filter((a) => validAnchor(a.validates, anchors))
|
|
253
255
|
.sort((a, b) => a.order - b.order)
|
|
@@ -288,7 +290,7 @@ function hasExplicitSchedule(context) {
|
|
|
288
290
|
}));
|
|
289
291
|
}
|
|
290
292
|
export function normalizePlannerOutput(plan, calendarAllowed = true) {
|
|
291
|
-
|
|
293
|
+
const built = {
|
|
292
294
|
actions: plan.actions.map((action, i) => ({
|
|
293
295
|
...action,
|
|
294
296
|
order: i + 1,
|
|
@@ -303,7 +305,16 @@ export function normalizePlannerOutput(plan, calendarAllowed = true) {
|
|
|
303
305
|
})),
|
|
304
306
|
} } : {}),
|
|
305
307
|
...(plan.reader_brief ? { reader_brief: plan.reader_brief } : {}),
|
|
306
|
-
}
|
|
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);
|
|
307
318
|
}
|
|
308
319
|
function unavailablePlan(reason, contract, risks, blindSpots, openQuestions) {
|
|
309
320
|
const unresolved = openQuestions.length ? openQuestions : [
|
|
@@ -341,18 +352,31 @@ export async function s9bPlan(ctx, contract, seats, graph, judgeReport, original
|
|
|
341
352
|
return fallback('plan_skipped');
|
|
342
353
|
const prompt = buildActionPlannerPrompt(answerContext, loadSkill('idea-refinement', 'planner'));
|
|
343
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;
|
|
344
365
|
try {
|
|
345
366
|
const first = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-plan', prompt, PlannerOutput, {
|
|
346
367
|
repair: ctx.budget.limit - ctx.budget.used >= 2,
|
|
347
368
|
}), calendarAllowed);
|
|
348
|
-
|
|
349
|
-
if (anchored) {
|
|
369
|
+
anchored = anchoredActionPlan(first, anchors, requestedOutputs, requireReaderBrief);
|
|
370
|
+
if (anchored && missingDeliverables(anchored, requestedOutputs).length === 0) {
|
|
350
371
|
await ctx.writer.writeJson('action-plan', anchored);
|
|
351
372
|
return anchored;
|
|
352
373
|
}
|
|
353
|
-
if (ctx.budget.limit - ctx.budget.used < 1)
|
|
374
|
+
if (ctx.budget.limit - ctx.budget.used < 1) {
|
|
375
|
+
if (anchored)
|
|
376
|
+
return accept(anchored);
|
|
354
377
|
return fallback('plan_fallback');
|
|
355
|
-
|
|
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` +
|
|
356
380
|
`Valid graph claim ids: ${anchors.claimIds.join(', ') || '(none)'}\n` +
|
|
357
381
|
`Valid blind spots: ${anchors.blindSpots.join(' | ') || '(none)'}\n` +
|
|
358
382
|
`Valid open questions: ${anchors.openQuestions.join(' | ') || '(none)'}\n` +
|
|
@@ -361,15 +385,20 @@ export async function s9bPlan(ctx, contract, seats, graph, judgeReport, original
|
|
|
361
385
|
`Output ONLY corrected JSON with every action anchored, reader_brief present, and every requested deliverable present.`;
|
|
362
386
|
const repaired = normalizePlannerOutput(await jsonCall(ctx, ctx.handle(ctx.roles.judge), 'S9b-anchor-repair', repair, PlannerOutput, { repair: false }), calendarAllowed);
|
|
363
387
|
const repairedAnchored = anchoredActionPlan(repaired, anchors, requestedOutputs, requireReaderBrief);
|
|
364
|
-
if (repairedAnchored) {
|
|
388
|
+
if (repairedAnchored && missingDeliverables(repairedAnchored, requestedOutputs).length === 0) {
|
|
365
389
|
await ctx.writer.writeJson('action-plan', repairedAnchored);
|
|
366
390
|
return repairedAnchored;
|
|
367
391
|
}
|
|
392
|
+
const best = repairedAnchored ?? anchored;
|
|
393
|
+
if (best)
|
|
394
|
+
return accept(best);
|
|
368
395
|
return fallback('plan_fallback');
|
|
369
396
|
}
|
|
370
397
|
catch (e) {
|
|
371
398
|
if (isFatal(e) && !(e instanceof BudgetExceeded))
|
|
372
399
|
throw e;
|
|
400
|
+
if (anchored)
|
|
401
|
+
return accept(anchored);
|
|
373
402
|
return fallback('plan_fallback');
|
|
374
403
|
}
|
|
375
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',
|