llm-cli-gateway 1.4.0 → 1.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +135 -1
  2. package/README.md +358 -15
  3. package/dist/approval-manager.d.ts +1 -1
  4. package/dist/async-job-manager.d.ts +32 -2
  5. package/dist/async-job-manager.js +101 -16
  6. package/dist/auth.d.ts +15 -0
  7. package/dist/auth.js +46 -0
  8. package/dist/cli-updater.d.ts +19 -2
  9. package/dist/cli-updater.js +110 -7
  10. package/dist/codex-json-parser.d.ts +34 -0
  11. package/dist/codex-json-parser.js +105 -0
  12. package/dist/config.d.ts +30 -0
  13. package/dist/config.js +167 -0
  14. package/dist/doctor.d.ts +110 -0
  15. package/dist/doctor.js +280 -0
  16. package/dist/endpoint-exposure.d.ts +22 -0
  17. package/dist/endpoint-exposure.js +231 -0
  18. package/dist/entrypoint-url.d.ts +1 -0
  19. package/dist/entrypoint-url.js +5 -0
  20. package/dist/executor.d.ts +9 -1
  21. package/dist/executor.js +52 -17
  22. package/dist/flight-recorder.d.ts +3 -1
  23. package/dist/flight-recorder.js +31 -2
  24. package/dist/gateway-server.d.ts +2 -0
  25. package/dist/gateway-server.js +1 -0
  26. package/dist/gemini-json-parser.d.ts +21 -0
  27. package/dist/gemini-json-parser.js +47 -0
  28. package/dist/health.d.ts +7 -0
  29. package/dist/health.js +22 -0
  30. package/dist/http-transport.d.ts +22 -0
  31. package/dist/http-transport.js +164 -0
  32. package/dist/index.d.ts +186 -2
  33. package/dist/index.js +2761 -1454
  34. package/dist/job-store.d.ts +118 -2
  35. package/dist/job-store.js +176 -5
  36. package/dist/logger.d.ts +9 -0
  37. package/dist/logger.js +14 -0
  38. package/dist/model-registry.js +40 -6
  39. package/dist/provider-login-guidance.d.ts +21 -0
  40. package/dist/provider-login-guidance.js +98 -0
  41. package/dist/provider-status.d.ts +41 -0
  42. package/dist/provider-status.js +203 -0
  43. package/dist/request-helpers.d.ts +484 -4
  44. package/dist/request-helpers.js +613 -0
  45. package/dist/resources.js +44 -0
  46. package/dist/session-manager-pg.js +1 -0
  47. package/dist/session-manager.d.ts +1 -1
  48. package/dist/session-manager.js +2 -1
  49. package/dist/upstream-contracts.d.ts +62 -0
  50. package/dist/upstream-contracts.js +620 -0
  51. package/dist/validation-normalizer.d.ts +23 -0
  52. package/dist/validation-normalizer.js +79 -0
  53. package/dist/validation-orchestrator.d.ts +47 -0
  54. package/dist/validation-orchestrator.js +145 -0
  55. package/dist/validation-prompts.d.ts +15 -0
  56. package/dist/validation-prompts.js +52 -0
  57. package/dist/validation-report.d.ts +57 -0
  58. package/dist/validation-report.js +129 -0
  59. package/dist/validation-tools.d.ts +7 -0
  60. package/dist/validation-tools.js +198 -0
  61. package/package.json +25 -10
  62. package/setup/status.schema.json +271 -0
@@ -0,0 +1,79 @@
1
+ export function normalizeStartedJob(provider, model, snapshot, warning) {
2
+ return {
3
+ provider,
4
+ model,
5
+ status: snapshot.status,
6
+ verdict: snapshot.status === "running" ? "pending" : null,
7
+ rationale: snapshot.status === "running" ? "Provider job is running asynchronously." : null,
8
+ risks: [],
9
+ rawJobReference: {
10
+ jobId: snapshot.id,
11
+ correlationId: snapshot.correlationId,
12
+ statusTool: "job_status",
13
+ resultTool: "job_result",
14
+ },
15
+ error: snapshot.error,
16
+ warning,
17
+ };
18
+ }
19
+ export function normalizeSkippedProvider(provider, reason) {
20
+ return {
21
+ provider,
22
+ model: null,
23
+ status: "skipped",
24
+ verdict: "not_run",
25
+ rationale: reason,
26
+ risks: [reason],
27
+ rawJobReference: null,
28
+ error: reason,
29
+ };
30
+ }
31
+ export function normalizeJobResult(provider, model, result) {
32
+ const output = result.stdout.trim();
33
+ const error = result.error || (result.status === "failed" ? result.stderr.trim() : null);
34
+ return {
35
+ provider,
36
+ model,
37
+ status: result.status,
38
+ verdict: inferVerdict(output, result.status),
39
+ rationale: output ? excerpt(output, 1800) : error,
40
+ risks: extractRisks(output, error),
41
+ rawJobReference: {
42
+ jobId: result.id,
43
+ correlationId: result.correlationId,
44
+ statusTool: "job_status",
45
+ resultTool: "job_result",
46
+ },
47
+ error,
48
+ };
49
+ }
50
+ function inferVerdict(output, status) {
51
+ if (status === "running")
52
+ return "pending";
53
+ if (status === "canceled" || status === "orphaned")
54
+ return status;
55
+ if (status === "failed")
56
+ return "failed";
57
+ const verdictMatch = output.match(/(?:^|\n)\s*verdict\s*:\s*(.+)/i);
58
+ if (verdictMatch?.[1])
59
+ return excerpt(verdictMatch[1].trim(), 240);
60
+ if (output)
61
+ return "answered";
62
+ return null;
63
+ }
64
+ function extractRisks(output, error) {
65
+ const risks = output
66
+ .split(/\r?\n/)
67
+ .map(line => line.trim())
68
+ .filter(line => /^(?:[-*]\s*)?(?:risk|risks|concern|caution|limitation)\b/i.test(line))
69
+ .slice(0, 5)
70
+ .map(line => excerpt(line, 300));
71
+ if (error && risks.length === 0)
72
+ risks.push(excerpt(error, 300));
73
+ return risks;
74
+ }
75
+ function excerpt(value, max) {
76
+ if (value.length <= max)
77
+ return value;
78
+ return `${value.slice(0, max - 3)}...`;
79
+ }
@@ -0,0 +1,47 @@
1
+ import type { AsyncJobManager } from "./async-job-manager.js";
2
+ import { type ProviderRuntimeStatus } from "./provider-status.js";
3
+ import { type NormalizedValidationResult, type ValidationProvider } from "./validation-normalizer.js";
4
+ import { type ValidationReport } from "./validation-report.js";
5
+ import { type ValidationIntent } from "./validation-prompts.js";
6
+ export interface ValidationOrchestratorDeps {
7
+ asyncJobManager: AsyncJobManager;
8
+ getProviderRuntimeStatus?: (provider: ValidationProvider) => ProviderRuntimeStatus;
9
+ }
10
+ export interface StartValidationInput {
11
+ intent: ValidationIntent;
12
+ question?: string;
13
+ content?: string;
14
+ providers: ValidationProvider[];
15
+ focus?: string;
16
+ riskLevel?: "normal" | "high";
17
+ judgeProvider?: ValidationProvider;
18
+ }
19
+ export interface ValidationRunReport {
20
+ success: boolean;
21
+ validationId: string;
22
+ status: "running" | "partial" | "not_started";
23
+ startedAt: string;
24
+ intent: ValidationIntent;
25
+ originalRequest: {
26
+ question?: string;
27
+ content?: string;
28
+ focus?: string;
29
+ };
30
+ modelList: ValidationProvider[];
31
+ results: NormalizedValidationResult[];
32
+ synthesis: {
33
+ status: "not_requested" | "waiting_for_provider_results" | "running" | "skipped";
34
+ judgeModel: ValidationProvider | null;
35
+ rawJobReference: NormalizedValidationResult["rawJobReference"];
36
+ note: string;
37
+ };
38
+ report: ValidationReport;
39
+ next: string;
40
+ }
41
+ export declare function startValidationRun(deps: ValidationOrchestratorDeps, input: StartValidationInput): ValidationRunReport;
42
+ export declare function startJudgeSynthesis(deps: ValidationOrchestratorDeps, input: {
43
+ question: string;
44
+ providerResults: NormalizedValidationResult[];
45
+ judgeProvider: ValidationProvider;
46
+ }): ValidationRunReport["synthesis"];
47
+ export declare function collectValidationJobResult(deps: ValidationOrchestratorDeps, provider: ValidationProvider, jobId: string, model: string | null, maxChars?: number): NormalizedValidationResult | null;
@@ -0,0 +1,145 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getProviderRuntimeStatus } from "./provider-status.js";
3
+ import { normalizeJobResult, normalizeSkippedProvider, normalizeStartedJob, } from "./validation-normalizer.js";
4
+ import { buildValidationReport } from "./validation-report.js";
5
+ import { buildJudgePrompt, buildValidationPrompt, } from "./validation-prompts.js";
6
+ export function startValidationRun(deps, input) {
7
+ const validationId = randomUUID();
8
+ const startedAt = new Date().toISOString();
9
+ const prompt = buildValidationPrompt({
10
+ intent: input.intent,
11
+ question: input.question,
12
+ content: input.content,
13
+ focus: input.focus,
14
+ riskLevel: input.riskLevel,
15
+ });
16
+ const providers = uniqueProviders(input.providers);
17
+ const results = providers.map(provider => startProviderJob(deps, provider, prompt, validationId));
18
+ const runningCount = results.filter(result => result.status === "running").length;
19
+ const skippedCount = results.filter(result => result.status === "skipped").length;
20
+ const synthesis = plannedJudgeSynthesis(input);
21
+ const status = runningCount === 0 ? "not_started" : skippedCount > 0 ? "partial" : "running";
22
+ const reportInput = {
23
+ validationId,
24
+ status,
25
+ startedAt,
26
+ intent: input.intent,
27
+ originalRequest: {
28
+ question: input.question,
29
+ content: input.content,
30
+ focus: input.focus,
31
+ },
32
+ modelList: providers,
33
+ results,
34
+ synthesis,
35
+ };
36
+ return {
37
+ success: runningCount > 0,
38
+ validationId,
39
+ status,
40
+ startedAt,
41
+ intent: input.intent,
42
+ originalRequest: reportInput.originalRequest,
43
+ modelList: providers,
44
+ results,
45
+ synthesis,
46
+ report: buildValidationReport(reportInput),
47
+ next: "Use job_status to poll each rawJobReference.jobId, job_result to collect provider outputs, then synthesize_validation if a judge summary is needed.",
48
+ };
49
+ }
50
+ export function startJudgeSynthesis(deps, input) {
51
+ const pending = input.providerResults.find(result => result.status === "running" || result.verdict === "pending");
52
+ if (pending) {
53
+ return {
54
+ status: "waiting_for_provider_results",
55
+ judgeModel: input.judgeProvider,
56
+ rawJobReference: null,
57
+ note: `Provider result for ${pending.provider} is still pending; collect terminal provider results before judge synthesis.`,
58
+ };
59
+ }
60
+ const completedResults = input.providerResults.filter(result => result.status === "completed");
61
+ const omittedResults = input.providerResults.filter(result => result.status !== "completed");
62
+ if (completedResults.length === 0) {
63
+ return {
64
+ status: "skipped",
65
+ judgeModel: input.judgeProvider,
66
+ rawJobReference: null,
67
+ note: "Judge synthesis requires at least one completed provider result; skipped, failed, canceled, or orphaned results are preserved in the report but are not judge evidence.",
68
+ };
69
+ }
70
+ const runtimeStatus = deps.getProviderRuntimeStatus ?? getProviderRuntimeStatus;
71
+ const runtime = runtimeStatus(input.judgeProvider);
72
+ if (!runtime.installed) {
73
+ return {
74
+ status: "skipped",
75
+ judgeModel: input.judgeProvider,
76
+ rawJobReference: null,
77
+ note: `${runtime.displayName} was selected as judge but is not installed.`,
78
+ };
79
+ }
80
+ const snapshot = deps.asyncJobManager.startJob(input.judgeProvider, buildProviderArgs(input.judgeProvider, buildJudgePrompt({
81
+ question: input.question,
82
+ providerResults: completedResults,
83
+ })), `validation-judge-${randomUUID()}-${input.judgeProvider}`);
84
+ return {
85
+ status: "running",
86
+ judgeModel: input.judgeProvider,
87
+ rawJobReference: {
88
+ jobId: snapshot.id,
89
+ correlationId: snapshot.correlationId,
90
+ statusTool: "job_status",
91
+ resultTool: "job_result",
92
+ },
93
+ note: omittedResults.length > 0
94
+ ? `Judge synthesis is running on ${runtime.displayName} using ${completedResults.length} completed provider result(s); ${omittedResults.length} non-completed result(s) were preserved but omitted.`
95
+ : `Judge synthesis is running on ${runtime.displayName} using completed provider results.`,
96
+ };
97
+ }
98
+ export function collectValidationJobResult(deps, provider, jobId, model, maxChars = 200000) {
99
+ const result = deps.asyncJobManager.getJobResult(jobId, maxChars);
100
+ if (!result)
101
+ return null;
102
+ return normalizeJobResult(provider, model, result);
103
+ }
104
+ function startProviderJob(deps, provider, prompt, validationId) {
105
+ const runtimeStatus = deps.getProviderRuntimeStatus ?? getProviderRuntimeStatus;
106
+ const runtime = runtimeStatus(provider);
107
+ if (!runtime.installed) {
108
+ return normalizeSkippedProvider(provider, `${runtime.displayName} runtime is not installed.`);
109
+ }
110
+ const warning = runtime.loginStatus === "authenticated"
111
+ ? undefined
112
+ : `${runtime.displayName} login status is ${runtime.loginStatus}; the job may fail until login is complete.`;
113
+ const snapshot = deps.asyncJobManager.startJob(provider, buildProviderArgs(provider, prompt), `validation-${validationId}-${provider}`);
114
+ return normalizeStartedJob(provider, runtime.version, snapshot, warning);
115
+ }
116
+ function plannedJudgeSynthesis(input) {
117
+ if (!input.judgeProvider) {
118
+ return {
119
+ status: "not_requested",
120
+ judgeModel: null,
121
+ rawJobReference: null,
122
+ note: "No judge synthesis was requested; provider disagreement is preserved for the caller.",
123
+ };
124
+ }
125
+ return {
126
+ status: "waiting_for_provider_results",
127
+ judgeModel: input.judgeProvider,
128
+ rawJobReference: null,
129
+ note: "Collect provider results first, then call synthesize_validation with those results.",
130
+ };
131
+ }
132
+ function buildProviderArgs(provider, prompt) {
133
+ if (provider === "claude" || provider === "grok" || provider === "mistral") {
134
+ // Mistral Vibe mirrors Grok's `-p PROMPT` headless surface. Model selection
135
+ // is via VIBE_ACTIVE_MODEL env var (no --model flag); for validation runs we
136
+ // let the user's environment pick the active model.
137
+ return ["-p", prompt];
138
+ }
139
+ if (provider === "codex")
140
+ return ["exec", "--skip-git-repo-check", prompt];
141
+ return [prompt];
142
+ }
143
+ function uniqueProviders(providers) {
144
+ return Array.from(new Set(providers));
145
+ }
@@ -0,0 +1,15 @@
1
+ import type { NormalizedValidationResult } from "./validation-normalizer.js";
2
+ export type ValidationIntent = "validate" | "second_opinion" | "red_team" | "consensus" | "ask_model";
3
+ interface BasePromptInput {
4
+ intent: ValidationIntent;
5
+ question?: string;
6
+ content?: string;
7
+ focus?: string;
8
+ riskLevel?: "normal" | "high";
9
+ }
10
+ export declare function buildValidationPrompt(input: BasePromptInput): string;
11
+ export declare function buildJudgePrompt(input: {
12
+ question: string;
13
+ providerResults: NormalizedValidationResult[];
14
+ }): string;
15
+ export {};
@@ -0,0 +1,52 @@
1
+ export function buildValidationPrompt(input) {
2
+ const focus = input.focus || "correctness, missing assumptions, and practical next steps";
3
+ const header = [
4
+ "You are one independent reviewer in a personal cross-LLM validation run.",
5
+ "Return a concise answer with these headings: Verdict, Rationale, Risks, Suggested next step.",
6
+ "Do not claim consensus; other model responses will be compared separately.",
7
+ ];
8
+ if (input.intent === "second_opinion") {
9
+ return [
10
+ ...header,
11
+ `Focus: ${focus}`,
12
+ "",
13
+ `Original question: ${input.question || "(not provided)"}`,
14
+ "",
15
+ "Answer to review:",
16
+ input.content || "",
17
+ ].join("\n");
18
+ }
19
+ if (input.intent === "red_team") {
20
+ return [
21
+ ...header,
22
+ `Review intensity: ${input.riskLevel || "normal"}`,
23
+ "Challenge assumptions, unsafe advice, unsupported claims, and likely failure modes.",
24
+ "",
25
+ input.content || "",
26
+ ].join("\n");
27
+ }
28
+ if (input.intent === "consensus") {
29
+ return [
30
+ ...header,
31
+ "Assess whether the claim is true, false, uncertain, or context-dependent.",
32
+ "",
33
+ `Claim: ${input.content || input.question || ""}`,
34
+ ].join("\n");
35
+ }
36
+ if (input.intent === "ask_model") {
37
+ return [input.question || input.content || ""].join("\n");
38
+ }
39
+ return [...header, `Focus: ${focus}`, "", input.question || input.content || ""].join("\n");
40
+ }
41
+ export function buildJudgePrompt(input) {
42
+ return [
43
+ "You are the explicit judge model for a personal cross-LLM validation run.",
44
+ "Synthesize only from the provider results below. Preserve material disagreement.",
45
+ "Return: Summary, Agreements, Disagreements, Recommendation, Confidence, Limitations.",
46
+ "",
47
+ `Original request: ${input.question}`,
48
+ "",
49
+ "Provider results:",
50
+ JSON.stringify(input.providerResults, null, 2),
51
+ ].join("\n");
52
+ }
@@ -0,0 +1,57 @@
1
+ import type { NormalizedValidationResult, ValidationProvider } from "./validation-normalizer.js";
2
+ import type { ValidationIntent } from "./validation-prompts.js";
3
+ export type ValidationReportConfidence = "none" | "low" | "medium" | "high";
4
+ export interface ValidationReportInput {
5
+ validationId: string;
6
+ status: "running" | "partial" | "not_started";
7
+ startedAt: string;
8
+ intent: ValidationIntent;
9
+ originalRequest: {
10
+ question?: string;
11
+ content?: string;
12
+ focus?: string;
13
+ };
14
+ modelList: ValidationProvider[];
15
+ results: NormalizedValidationResult[];
16
+ synthesis: {
17
+ status: "not_requested" | "waiting_for_provider_results" | "running" | "skipped";
18
+ judgeModel: ValidationProvider | null;
19
+ rawJobReference: NormalizedValidationResult["rawJobReference"];
20
+ note: string;
21
+ };
22
+ }
23
+ export interface ValidationReport {
24
+ schemaVersion: "validation-report.v1";
25
+ humanReadable: string;
26
+ structuredContent: {
27
+ validationId: string;
28
+ status: ValidationReportInput["status"];
29
+ startedAt: string;
30
+ intent: ValidationIntent;
31
+ originalRequest: ValidationReportInput["originalRequest"];
32
+ modelList: ValidationProvider[];
33
+ perModelOutputs: Array<{
34
+ provider: ValidationProvider;
35
+ model: string | null;
36
+ status: NormalizedValidationResult["status"];
37
+ verdict: string | null;
38
+ rationale: string | null;
39
+ risks: string[];
40
+ jobId: string | null;
41
+ correlationId: string | null;
42
+ warning: string | null;
43
+ error: string | null;
44
+ }>;
45
+ disagreements: {
46
+ hasMaterialDisagreement: boolean;
47
+ summary: string;
48
+ signals: string[];
49
+ };
50
+ finalRecommendation: string;
51
+ confidence: ValidationReportConfidence;
52
+ limitations: string[];
53
+ jobIds: string[];
54
+ synthesis: ValidationReportInput["synthesis"];
55
+ };
56
+ }
57
+ export declare function buildValidationReport(input: ValidationReportInput): ValidationReport;
@@ -0,0 +1,129 @@
1
+ export function buildValidationReport(input) {
2
+ const perModelOutputs = input.results.map(result => ({
3
+ provider: result.provider,
4
+ model: result.model,
5
+ status: result.status,
6
+ verdict: result.verdict,
7
+ rationale: result.rationale,
8
+ risks: result.risks,
9
+ jobId: result.rawJobReference?.jobId ?? null,
10
+ correlationId: result.rawJobReference?.correlationId ?? null,
11
+ warning: result.warning ?? null,
12
+ error: result.error,
13
+ }));
14
+ const jobIds = perModelOutputs.flatMap(output => (output.jobId ? [output.jobId] : []));
15
+ const disagreements = summarizeDisagreement(input.results);
16
+ const limitations = summarizeLimitations(input.results, input.synthesis);
17
+ const confidence = confidenceFor(input.results, disagreements.hasMaterialDisagreement);
18
+ const finalRecommendation = recommendationFor(input.results, disagreements.hasMaterialDisagreement);
19
+ const structuredContent = {
20
+ validationId: input.validationId,
21
+ status: input.status,
22
+ startedAt: input.startedAt,
23
+ intent: input.intent,
24
+ originalRequest: input.originalRequest,
25
+ modelList: input.modelList,
26
+ perModelOutputs,
27
+ disagreements,
28
+ finalRecommendation,
29
+ confidence,
30
+ limitations,
31
+ jobIds,
32
+ synthesis: input.synthesis,
33
+ };
34
+ return {
35
+ schemaVersion: "validation-report.v1",
36
+ humanReadable: renderHumanReport(structuredContent),
37
+ structuredContent,
38
+ };
39
+ }
40
+ function summarizeDisagreement(results) {
41
+ const completed = results.filter(result => result.status === "completed");
42
+ const terminalProblems = results.filter(result => ["failed", "canceled", "orphaned", "skipped"].includes(result.status));
43
+ const pending = results.filter(result => result.status === "running" || result.verdict === "pending");
44
+ const verdicts = new Set(completed
45
+ .map(result => normalizeVerdict(result.verdict))
46
+ .filter((verdict) => Boolean(verdict)));
47
+ const signals = [];
48
+ if (verdicts.size > 1)
49
+ signals.push(`Completed providers returned ${verdicts.size} different verdicts.`);
50
+ for (const result of terminalProblems)
51
+ signals.push(`${result.provider} is ${result.status}.`);
52
+ for (const result of pending)
53
+ signals.push(`${result.provider} is still pending.`);
54
+ const hasMaterialDisagreement = verdicts.size > 1 || terminalProblems.length > 0 || pending.length > 0;
55
+ return {
56
+ hasMaterialDisagreement,
57
+ summary: hasMaterialDisagreement
58
+ ? "Do not treat this validation as consensus; inspect the per-model outputs and unresolved states."
59
+ : completed.length > 0
60
+ ? "Completed providers do not show material verdict disagreement in the normalized report."
61
+ : "No completed provider outputs are available yet.",
62
+ signals,
63
+ };
64
+ }
65
+ function summarizeLimitations(results, synthesis) {
66
+ const limitations = [];
67
+ if (results.some(result => result.status === "running")) {
68
+ limitations.push("Some provider jobs are still running; poll job_status and job_result before treating the report as final.");
69
+ }
70
+ if (results.some(result => result.status !== "completed")) {
71
+ limitations.push("Only completed provider outputs are suitable as judge synthesis evidence.");
72
+ }
73
+ if (synthesis.status === "waiting_for_provider_results") {
74
+ limitations.push("Judge synthesis has not run because provider results still need to be collected.");
75
+ }
76
+ else if (synthesis.status === "skipped") {
77
+ limitations.push(`Judge synthesis skipped: ${synthesis.note}`);
78
+ }
79
+ else if (synthesis.status === "not_requested") {
80
+ limitations.push("No explicit judge synthesis was requested; use per-model outputs for the decision.");
81
+ }
82
+ limitations.push("Large raw outputs are intentionally kept behind job_result references to fit normal MCP client responses.");
83
+ return limitations;
84
+ }
85
+ function confidenceFor(results, hasMaterialDisagreement) {
86
+ const completedCount = results.filter(result => result.status === "completed").length;
87
+ if (completedCount === 0)
88
+ return "none";
89
+ if (hasMaterialDisagreement)
90
+ return "low";
91
+ if (completedCount === 1)
92
+ return "medium";
93
+ return "high";
94
+ }
95
+ function recommendationFor(results, hasMaterialDisagreement) {
96
+ const completedCount = results.filter(result => result.status === "completed").length;
97
+ if (completedCount === 0) {
98
+ return "Wait for at least one provider job to complete, then collect job_result before deciding.";
99
+ }
100
+ if (hasMaterialDisagreement) {
101
+ return "Review the per-model outputs and resolve disagreements manually before acting.";
102
+ }
103
+ return "Completed provider outputs show no normalized verdict disagreement; review rationales and risks before acting.";
104
+ }
105
+ function renderHumanReport(content) {
106
+ const lines = [
107
+ `Validation report ${content.validationId}`,
108
+ `Status: ${content.status}`,
109
+ `Models: ${content.modelList.join(", ") || "none"}`,
110
+ "",
111
+ "Per-model outputs:",
112
+ ...content.perModelOutputs.map(output => {
113
+ const job = output.jobId ? ` job=${output.jobId}` : "";
114
+ const verdict = output.verdict ? ` verdict=${output.verdict}` : "";
115
+ return `- ${output.provider}: ${output.status}${verdict}${job}`;
116
+ }),
117
+ "",
118
+ `Disagreement: ${content.disagreements.summary}`,
119
+ `Recommendation: ${content.finalRecommendation}`,
120
+ `Confidence: ${content.confidence}`,
121
+ "",
122
+ "Limitations:",
123
+ ...content.limitations.map(limitation => `- ${limitation}`),
124
+ ];
125
+ return lines.join("\n");
126
+ }
127
+ function normalizeVerdict(verdict) {
128
+ return verdict?.trim().toLowerCase() || null;
129
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { AsyncJobManager } from "./async-job-manager.js";
3
+ import { type ValidationOrchestratorDeps } from "./validation-orchestrator.js";
4
+ export interface ValidationToolDeps extends ValidationOrchestratorDeps {
5
+ asyncJobManager: AsyncJobManager;
6
+ }
7
+ export declare function registerValidationTools(server: McpServer, deps: ValidationToolDeps): void;