@lcv-ideas-software/cross-review 4.0.0
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 +2568 -0
- package/LICENSE +201 -0
- package/NOTICE +26 -0
- package/README.md +208 -0
- package/SECURITY.md +52 -0
- package/dist/scripts/api-streaming-smoke.d.ts +1 -0
- package/dist/scripts/api-streaming-smoke.js +78 -0
- package/dist/scripts/api-streaming-smoke.js.map +1 -0
- package/dist/scripts/runtime-default-smoke.d.ts +1 -0
- package/dist/scripts/runtime-default-smoke.js +88 -0
- package/dist/scripts/runtime-default-smoke.js.map +1 -0
- package/dist/scripts/runtime-smoke.d.ts +1 -0
- package/dist/scripts/runtime-smoke.js +148 -0
- package/dist/scripts/runtime-smoke.js.map +1 -0
- package/dist/scripts/smoke.d.ts +1 -0
- package/dist/scripts/smoke.js +6156 -0
- package/dist/scripts/smoke.js.map +1 -0
- package/dist/src/core/cache-manifest.d.ts +22 -0
- package/dist/src/core/cache-manifest.js +133 -0
- package/dist/src/core/cache-manifest.js.map +1 -0
- package/dist/src/core/caller-tokens.d.ts +32 -0
- package/dist/src/core/caller-tokens.js +240 -0
- package/dist/src/core/caller-tokens.js.map +1 -0
- package/dist/src/core/config.d.ts +9 -0
- package/dist/src/core/config.js +643 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/convergence.d.ts +5 -0
- package/dist/src/core/convergence.js +186 -0
- package/dist/src/core/convergence.js.map +1 -0
- package/dist/src/core/cost.d.ts +59 -0
- package/dist/src/core/cost.js +359 -0
- package/dist/src/core/cost.js.map +1 -0
- package/dist/src/core/file-config.d.ts +316 -0
- package/dist/src/core/file-config.js +490 -0
- package/dist/src/core/file-config.js.map +1 -0
- package/dist/src/core/orchestrator.d.ts +199 -0
- package/dist/src/core/orchestrator.js +3430 -0
- package/dist/src/core/orchestrator.js.map +1 -0
- package/dist/src/core/prompt-parts.d.ts +58 -0
- package/dist/src/core/prompt-parts.js +122 -0
- package/dist/src/core/prompt-parts.js.map +1 -0
- package/dist/src/core/relator-lottery.d.ts +23 -0
- package/dist/src/core/relator-lottery.js +112 -0
- package/dist/src/core/relator-lottery.js.map +1 -0
- package/dist/src/core/reports.d.ts +2 -0
- package/dist/src/core/reports.js +82 -0
- package/dist/src/core/reports.js.map +1 -0
- package/dist/src/core/session-store.d.ts +149 -0
- package/dist/src/core/session-store.js +1923 -0
- package/dist/src/core/session-store.js.map +1 -0
- package/dist/src/core/status.d.ts +61 -0
- package/dist/src/core/status.js +249 -0
- package/dist/src/core/status.js.map +1 -0
- package/dist/src/core/timeouts.d.ts +2 -0
- package/dist/src/core/timeouts.js +3 -0
- package/dist/src/core/timeouts.js.map +1 -0
- package/dist/src/core/types.d.ts +604 -0
- package/dist/src/core/types.js +36 -0
- package/dist/src/core/types.js.map +1 -0
- package/dist/src/dashboard/server.d.ts +2 -0
- package/dist/src/dashboard/server.js +339 -0
- package/dist/src/dashboard/server.js.map +1 -0
- package/dist/src/mcp/server.d.ts +54 -0
- package/dist/src/mcp/server.js +1584 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/observability/logger.d.ts +9 -0
- package/dist/src/observability/logger.js +24 -0
- package/dist/src/observability/logger.js.map +1 -0
- package/dist/src/peers/anthropic.d.ts +14 -0
- package/dist/src/peers/anthropic.js +290 -0
- package/dist/src/peers/anthropic.js.map +1 -0
- package/dist/src/peers/base.d.ts +72 -0
- package/dist/src/peers/base.js +416 -0
- package/dist/src/peers/base.js.map +1 -0
- package/dist/src/peers/deepseek.d.ts +12 -0
- package/dist/src/peers/deepseek.js +246 -0
- package/dist/src/peers/deepseek.js.map +1 -0
- package/dist/src/peers/errors.d.ts +2 -0
- package/dist/src/peers/errors.js +185 -0
- package/dist/src/peers/errors.js.map +1 -0
- package/dist/src/peers/gemini.d.ts +13 -0
- package/dist/src/peers/gemini.js +215 -0
- package/dist/src/peers/gemini.js.map +1 -0
- package/dist/src/peers/grok.d.ts +17 -0
- package/dist/src/peers/grok.js +346 -0
- package/dist/src/peers/grok.js.map +1 -0
- package/dist/src/peers/model-selection.d.ts +4 -0
- package/dist/src/peers/model-selection.js +260 -0
- package/dist/src/peers/model-selection.js.map +1 -0
- package/dist/src/peers/openai.d.ts +14 -0
- package/dist/src/peers/openai.js +299 -0
- package/dist/src/peers/openai.js.map +1 -0
- package/dist/src/peers/perplexity.d.ts +18 -0
- package/dist/src/peers/perplexity.js +375 -0
- package/dist/src/peers/perplexity.js.map +1 -0
- package/dist/src/peers/registry.d.ts +3 -0
- package/dist/src/peers/registry.js +77 -0
- package/dist/src/peers/registry.js.map +1 -0
- package/dist/src/peers/retry.d.ts +2 -0
- package/dist/src/peers/retry.js +36 -0
- package/dist/src/peers/retry.js.map +1 -0
- package/dist/src/peers/stub.d.ts +13 -0
- package/dist/src/peers/stub.js +344 -0
- package/dist/src/peers/stub.js.map +1 -0
- package/dist/src/peers/text.d.ts +18 -0
- package/dist/src/peers/text.js +39 -0
- package/dist/src/peers/text.js.map +1 -0
- package/dist/src/security/redact.d.ts +2 -0
- package/dist/src/security/redact.js +128 -0
- package/dist/src/security/redact.js.map +1 -0
- package/docs/api-keys.md +34 -0
- package/docs/architecture.md +118 -0
- package/docs/caching.md +135 -0
- package/docs/costs.md +40 -0
- package/docs/evidence-preflight.md +88 -0
- package/docs/github-security-baseline.md +32 -0
- package/docs/model-selection.md +105 -0
- package/docs/reports/cross-review-v2-api-capability-smoke-2026-04-30.md +354 -0
- package/docs/reports/cross-review-v2-format-recovery-findings-2026-04-28.md +223 -0
- package/docs/reports/cross-review-v2-official-provider-docs-refresh-2026-05-05.md +60 -0
- package/docs/reports/cross-review-v2-token-streaming-smoke-2026-04-30.md +119 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { RELEASE_DATE, VERSION, loadConfig, missingFinancialControlVars } from "../core/config.js";
|
|
9
|
+
import { CrossReviewOrchestrator } from "../core/orchestrator.js";
|
|
10
|
+
import { PEERS } from "../core/types.js";
|
|
11
|
+
import { sessionReportMarkdown } from "../core/reports.js";
|
|
12
|
+
import { EventLog } from "../observability/logger.js";
|
|
13
|
+
import { safeErrorMessage } from "../security/redact.js";
|
|
14
|
+
const PeerSchema = z.enum(PEERS);
|
|
15
|
+
// v2.18.6 / Gemini-API compat: `caller` accepts any peer + "operator".
|
|
16
|
+
// Pre-v2.18.6 we used `CallerSchema`
|
|
17
|
+
// which the MCP SDK serialized as `anyOf: [enum, const]` — Gemini API's
|
|
18
|
+
// function-declaration validator rejects that shape. A flat enum is
|
|
19
|
+
// runtime-equivalent (same accepted values, same TS inferred type) and
|
|
20
|
+
// produces a clean single `enum` in the wire JSON Schema.
|
|
21
|
+
const CallerSchema = z.enum([...PEERS, "operator"]);
|
|
22
|
+
const ResponseFormatSchema = z.enum(["json", "markdown"]).default("json");
|
|
23
|
+
// v2.15.0 (item 2): per-call reasoning_effort overrides. Optional partial
|
|
24
|
+
// record keyed by peer id; missing keys fall back to the global config
|
|
25
|
+
// default (CROSS_REVIEW_<PEER>_REASONING_EFFORT env var, ultimately
|
|
26
|
+
// resolved by core/config.ts). The string enum mirrors `ReasoningEffort`
|
|
27
|
+
// in core/types.ts. Each adapter that consumes effort reads the override
|
|
28
|
+
// from `PeerCallContext.reasoning_effort_override`. Adapters without an
|
|
29
|
+
// effort knob (gemini today) silently ignore it.
|
|
30
|
+
//
|
|
31
|
+
// v2.18.6 / Gemini-API compat: pre-v2.18.6 this was `z.record(PeerSchema,
|
|
32
|
+
// ReasoningEffortSchema).optional()`. The MCP SDK serialized that as
|
|
33
|
+
// `{type:"object", propertyNames:{enum:[...]}, additionalProperties:{enum:[...]},
|
|
34
|
+
// required:[<all peers>]}` — non-standard OpenAPI 3.0 (Gemini API
|
|
35
|
+
// rejects `propertyNames`) plus a phantom `required` listing all peers
|
|
36
|
+
// despite the field being `.optional()`. Flattening to an explicit
|
|
37
|
+
// `z.object({codex?, claude?, gemini?, deepseek?, grok?, perplexity?})`
|
|
38
|
+
// (v3.7.1 / AUDIT-4: comment refreshed — perplexity has been the 6th peer
|
|
39
|
+
// since v3.0.0) produces a clean `{type:"object", properties:{...}}`
|
|
40
|
+
// accepted by every host; runtime accepts the same
|
|
41
|
+
// `{codex:"high", claude:"low"}` shape.
|
|
42
|
+
const ReasoningEffortSchema = z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
43
|
+
const ReasoningEffortOverridesSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
codex: ReasoningEffortSchema.optional(),
|
|
46
|
+
claude: ReasoningEffortSchema.optional(),
|
|
47
|
+
gemini: ReasoningEffortSchema.optional(),
|
|
48
|
+
deepseek: ReasoningEffortSchema.optional(),
|
|
49
|
+
grok: ReasoningEffortSchema.optional(),
|
|
50
|
+
// v3.0.0: Perplexity 6th peer. Sonar API accepts only
|
|
51
|
+
// `minimal|low|medium|high` on-the-wire (clamped at the adapter
|
|
52
|
+
// boundary); the schema still accepts the full internal scale so
|
|
53
|
+
// operators can mirror their global config style — the adapter
|
|
54
|
+
// narrows to Perplexity's accepted set at call time.
|
|
55
|
+
perplexity: ReasoningEffortSchema.optional(),
|
|
56
|
+
})
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Optional per-peer reasoning_effort overrides for this call. Keys are peer ids (codex|claude|gemini|deepseek|grok|perplexity); missing keys fall back to global config. Useful to dial down expensive peers (e.g. Grok grok-4.20-multi-agent xhigh = 16 agents, or Perplexity sonar-deep-research that bills citation + reasoning + search queries separately) for routine reviews without editing the host MCP configs.");
|
|
59
|
+
// v2.4.0 / audit closure (P1.2): UUIDv4 regex was already accepting
|
|
60
|
+
// case-insensitive matches via the /i flag, but zod did not normalize the
|
|
61
|
+
// output. On case-sensitive filesystems (Linux, macOS) the same logical
|
|
62
|
+
// session would resolve to two different on-disk paths depending on how
|
|
63
|
+
// the caller capitalized the id; on Windows the read/write paths could
|
|
64
|
+
// drift between contexts. The transform below collapses the value to
|
|
65
|
+
// lowercase before any downstream consumer touches it, eliminating that
|
|
66
|
+
// TOCTOU surface without breaking existing UUIDv4 producers.
|
|
67
|
+
export const SessionIdSchema = z
|
|
68
|
+
.string()
|
|
69
|
+
.regex(/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i, "session_id must be a valid UUIDv4")
|
|
70
|
+
.transform((value) => value.toLowerCase());
|
|
71
|
+
const ReviewFocusSchema = z
|
|
72
|
+
.string()
|
|
73
|
+
.trim()
|
|
74
|
+
.min(1)
|
|
75
|
+
.max(4_000)
|
|
76
|
+
.describe("Optional provider-neutral review scope anchor. This is not Claude Code's /focus UI command; it is injected as a front-loaded Review Focus prompt block for every selected peer, including OUT OF SCOPE handling for unrelated findings.")
|
|
77
|
+
.optional();
|
|
78
|
+
// v2.4.0 / audit closure (P2.5): MCP input-schema caps for the high-volume
|
|
79
|
+
// LLM input fields that previously only enforced `.min(1)`. The MCP
|
|
80
|
+
// StdioServerTransport does not impose a per-message cap, so a misbehaving
|
|
81
|
+
// caller — or any deployment that drifts off the trusted-host model — can
|
|
82
|
+
// OOM the orchestrator or burn provider tokens with one large prompt. The
|
|
83
|
+
// caps below are deliberately generous (an order of magnitude above the
|
|
84
|
+
// in-process `config.prompt.max_*` values) so they let normal usage
|
|
85
|
+
// through while rejecting obvious abuse before parser/spawn/persistence
|
|
86
|
+
// touch the bytes. Mirrors the v1.6.7 P1.1 hardening.
|
|
87
|
+
const SCHEMA_TASK_MAX_CHARS = 32_000;
|
|
88
|
+
const SCHEMA_DRAFT_MAX_CHARS = 200_000;
|
|
89
|
+
const SCHEMA_INITIAL_DRAFT_MAX_CHARS = 200_000;
|
|
90
|
+
function textResult(value, responseFormat = "json") {
|
|
91
|
+
const text = responseFormat === "markdown" && typeof value === "string"
|
|
92
|
+
? value
|
|
93
|
+
: JSON.stringify(value, null, 2);
|
|
94
|
+
return { content: [{ type: "text", text }] };
|
|
95
|
+
}
|
|
96
|
+
// v2.18.0 / F1 caller capability tokens — runtime record set at boot.
|
|
97
|
+
// Surfaced to verifyCallerIdentity for the token-overlay step. Module-level
|
|
98
|
+
// state because the token map is loaded once per server boot (file I/O on
|
|
99
|
+
// every call would be wasteful and gives an attacker a TOCTOU window).
|
|
100
|
+
import { ensureHostTokens, generateHostTokens as f1GenerateHostTokens, getParentProcessSnapshot, isHardEnforceMode, verifyTokenForCaller, } from "../core/caller-tokens.js";
|
|
101
|
+
let HOST_TOKENS_RECORD = null;
|
|
102
|
+
export function getHostTokensRecord() {
|
|
103
|
+
return HOST_TOKENS_RECORD;
|
|
104
|
+
}
|
|
105
|
+
export function setHostTokensRecord(record) {
|
|
106
|
+
HOST_TOKENS_RECORD = record;
|
|
107
|
+
}
|
|
108
|
+
export function initHostTokensRecord(dataDir) {
|
|
109
|
+
try {
|
|
110
|
+
const record = ensureHostTokens(dataDir);
|
|
111
|
+
HOST_TOKENS_RECORD = record || null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
HOST_TOKENS_RECORD = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function getCallerCandidatesFromClientInfo(clientInfo) {
|
|
118
|
+
const name = String(clientInfo?.name || "").toLowerCase();
|
|
119
|
+
if (!name)
|
|
120
|
+
return [];
|
|
121
|
+
const candidates = [];
|
|
122
|
+
for (const peer of PEERS) {
|
|
123
|
+
if (name.includes(peer))
|
|
124
|
+
candidates.push(peer);
|
|
125
|
+
}
|
|
126
|
+
return candidates;
|
|
127
|
+
}
|
|
128
|
+
// v2.18.0 / F1: token verification overlays the v2.17.0 clientInfo gate.
|
|
129
|
+
// Decision tree (in order):
|
|
130
|
+
// 1. caller="operator" → human-driven, non-agent identity. Returns
|
|
131
|
+
// identity_verified=false, verification_method="none". Token check is
|
|
132
|
+
// skipped by design — operator is a non-PEER identity, the gate-setter
|
|
133
|
+
// themselves; AI agents cannot forge "I'm not an AI agent" because:
|
|
134
|
+
// (a) F1 cross-review R2 codex catch hardening: if the calling host
|
|
135
|
+
// carries CROSS_REVIEW_CALLER_TOKEN, it IS an agent host (the token
|
|
136
|
+
// bind is to a specific AI agent's identity). Declaring caller="operator"
|
|
137
|
+
// from such a host is identity forgery and throws. Only HOSTS WITHOUT
|
|
138
|
+
// a token (genuinely human-driven curl/dashboard/stdio) can declare
|
|
139
|
+
// operator. (b) downstream privilege model: operator caller is never
|
|
140
|
+
// added to PEERS panels, never participates in tribunal review, never
|
|
141
|
+
// gets identity_verified=true — verifying code paths that gate on
|
|
142
|
+
// identity_verified or peer-membership are unaffected by operator
|
|
143
|
+
// caller. Hard-enforce mode does NOT apply to operator (the
|
|
144
|
+
// gate-setter is exempt from their own gate by design).
|
|
145
|
+
// 2. v2.17.0 clientInfo cross-check throws → propagate (preserves all
|
|
146
|
+
// existing forgery rejections).
|
|
147
|
+
// 3. CROSS_REVIEW_CALLER_TOKEN env present → must resolve to declaredCaller
|
|
148
|
+
// via host-tokens.json; mismatch / unknown / file-missing → throws.
|
|
149
|
+
// Match → upgrade verification_method to "token".
|
|
150
|
+
// 4. CROSS_REVIEW_CALLER_TOKEN absent + CROSS_REVIEW_REQUIRE_TOKEN=true →
|
|
151
|
+
// throws (hard-enforce mode opted into by operator).
|
|
152
|
+
// 5. CROSS_REVIEW_CALLER_TOKEN absent + permissive (default) → return
|
|
153
|
+
// whatever clientInfo cross-check yielded ("client_info" if matched,
|
|
154
|
+
// "none" if unknown).
|
|
155
|
+
// All paths attach identity_metadata with a best-effort parent-process
|
|
156
|
+
// snapshot for forensics (Option C / Hybrid per design memory).
|
|
157
|
+
export function verifyCallerIdentity(declaredCaller, clientInfo) {
|
|
158
|
+
const identity_metadata = getParentProcessSnapshot();
|
|
159
|
+
// operator is a non-agent identity; nothing to forge against PEERS list.
|
|
160
|
+
// BUT: if the calling host carries CROSS_REVIEW_CALLER_TOKEN, it IS an
|
|
161
|
+
// agent host (token binds to a specific agent's identity). Declaring
|
|
162
|
+
// operator from such a host is identity forgery — throw.
|
|
163
|
+
if (declaredCaller === "operator") {
|
|
164
|
+
const presented = process.env.CROSS_REVIEW_CALLER_TOKEN;
|
|
165
|
+
if (typeof presented === "string" && presented.trim().length > 0) {
|
|
166
|
+
throw new Error("identity_forgery_blocked: caller='operator' is not permitted from a host that carries CROSS_REVIEW_CALLER_TOKEN. The token binds to a specific AI agent's identity; declaring operator from such a host is a forgery attempt. Either drop the token from the calling host's env (genuine human-driven invocations should not carry an agent token) or pass the actual agent caller that matches the token.");
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
identity_verified: false, // no agent claim made; nothing to verify
|
|
170
|
+
verification_method: "none",
|
|
171
|
+
client_info_name: clientInfo?.name ?? null,
|
|
172
|
+
identity_metadata,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const candidates = getCallerCandidatesFromClientInfo(clientInfo);
|
|
176
|
+
if (candidates.length >= 2) {
|
|
177
|
+
throw new Error(`identity_forgery_blocked: clientInfo.name='${clientInfo?.name}' matches multiple agents (${candidates.join(", ")}); cannot validate declared caller='${declaredCaller}' against an ambiguous client. Pass the request from a host whose clientInfo.name resolves to a single agent.`);
|
|
178
|
+
}
|
|
179
|
+
if (candidates.length === 1 && candidates[0] !== declaredCaller) {
|
|
180
|
+
throw new Error(`identity_forgery_blocked: declared caller='${declaredCaller}' contradicts clientInfo.name='${clientInfo?.name}' which resolves to '${candidates[0]}'. An agent cannot self-declare a different identity than its MCP host (operator directive 2026-05-05). If this is a legitimate cross-host setup, ensure clientInfo.name does not contain a different agent's name as substring.`);
|
|
181
|
+
}
|
|
182
|
+
let verification_method = candidates.length === 1 ? "client_info" : "none";
|
|
183
|
+
let identity_verified = candidates.length === 1;
|
|
184
|
+
// Token overlay (v2.18.0 F1).
|
|
185
|
+
const tokenResult = verifyTokenForCaller(declaredCaller, HOST_TOKENS_RECORD);
|
|
186
|
+
if (tokenResult.verified) {
|
|
187
|
+
verification_method = "token";
|
|
188
|
+
identity_verified = true;
|
|
189
|
+
}
|
|
190
|
+
else if (isHardEnforceMode()) {
|
|
191
|
+
throw new Error("identity_forgery_blocked: CROSS_REVIEW_REQUIRE_TOKEN=true is set but no CROSS_REVIEW_CALLER_TOKEN was provided in this call's environment. Either remove the hard-enforce flag or distribute host-tokens.json to the calling host's MCP env.");
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
identity_verified,
|
|
195
|
+
verification_method,
|
|
196
|
+
client_info_name: clientInfo?.name ?? null,
|
|
197
|
+
identity_metadata,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// v3.3.0 (operator directive 2026-05-12): caller peer-selection lock.
|
|
201
|
+
// "TODOS OS AGENTES/PEERS SEMPRE PARTICIPAM, INDEPENDENTE DA ESCOLHA OU
|
|
202
|
+
// VONTADE DO CALLER." Applied at the MCP-tool boundary so every
|
|
203
|
+
// externally-driven call has caller-supplied `peers` and (for peer
|
|
204
|
+
// callers) `lead_peer` stripped before reaching the orchestrator.
|
|
205
|
+
// Internal call sites (orchestrator's own runUntilUnanimous → askPeers
|
|
206
|
+
// loop, smoke harness) bypass the lock by construction — they do not go
|
|
207
|
+
// through this boundary. Operator caller may still pin `lead_peer`
|
|
208
|
+
// explicitly (legitimate testing/debug; operator is the meta-authority,
|
|
209
|
+
// not a session participant whose vote can be biased).
|
|
210
|
+
//
|
|
211
|
+
// `emitFn` carries the audit trail to the eventLog/store so the operator
|
|
212
|
+
// can inspect who tried to game which peer in/out via `session_events`.
|
|
213
|
+
export function lockCallerPeerSelection(input, ctx) {
|
|
214
|
+
const caller = input.caller ?? "operator";
|
|
215
|
+
// peers panel: locked for ALL callers (including operator). The
|
|
216
|
+
// server-configured `peer_enabled` set is the only knob; operators
|
|
217
|
+
// tune via env vars, not via per-call overrides that callers can
|
|
218
|
+
// exploit.
|
|
219
|
+
const callerSuppliedPeers = Array.isArray(input.peers) ? [...input.peers] : undefined;
|
|
220
|
+
// v3.7.5 (A2): treat caller-supplied panel as an OVERRIDE only when
|
|
221
|
+
// it differs from the enabled set. Sorted set-equality (case-sensitive
|
|
222
|
+
// since PeerId is a closed string union). Backward-compat: when
|
|
223
|
+
// `enabledPeers` is undefined (no caller passed it), the lock keeps
|
|
224
|
+
// the v3.3.0 behavior — any non-empty list is treated as an override.
|
|
225
|
+
const callerPanelMatchesEnabled = ctx.enabledPeers !== undefined &&
|
|
226
|
+
callerSuppliedPeers !== undefined &&
|
|
227
|
+
callerSuppliedPeers.length === ctx.enabledPeers.length &&
|
|
228
|
+
[...callerSuppliedPeers].sort().join("|") === [...ctx.enabledPeers].sort().join("|");
|
|
229
|
+
const peerPanelOverridden = !!callerSuppliedPeers && callerSuppliedPeers.length > 0 && !callerPanelMatchesEnabled;
|
|
230
|
+
// lead_peer: locked for peer callers (forces lottery so callers cannot
|
|
231
|
+
// pin a sympathetic relator). Operator caller may pin lead_peer for
|
|
232
|
+
// legitimate testing.
|
|
233
|
+
const leadPeerOverridden = caller !== "operator" && input.lead_peer !== undefined;
|
|
234
|
+
if (peerPanelOverridden || leadPeerOverridden) {
|
|
235
|
+
ctx.emit({
|
|
236
|
+
type: "session.caller_peer_selection_ignored",
|
|
237
|
+
session_id: input.session_id,
|
|
238
|
+
message: `caller_peer_selection_lock: caller=${caller} attempted to ${peerPanelOverridden ? "override the reviewer panel" : "pin lead_peer"} via ${ctx.site}; the request was silently overridden — operator directive 2026-05-12 ("TODOS OS AGENTES/PEERS SEMPRE PARTICIPAM").`,
|
|
239
|
+
data: {
|
|
240
|
+
site: ctx.site,
|
|
241
|
+
caller,
|
|
242
|
+
peer_panel_overridden: peerPanelOverridden,
|
|
243
|
+
ignored_peers: peerPanelOverridden ? callerSuppliedPeers : undefined,
|
|
244
|
+
lead_peer_overridden: leadPeerOverridden,
|
|
245
|
+
ignored_lead_peer: leadPeerOverridden ? input.lead_peer : undefined,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// Strip the locked fields. The orchestrator's defaults (full PEERS,
|
|
250
|
+
// lottery for lead_peer when caller is a peer) take over.
|
|
251
|
+
const sanitized = { ...input };
|
|
252
|
+
if (peerPanelOverridden)
|
|
253
|
+
delete sanitized.peers;
|
|
254
|
+
if (leadPeerOverridden)
|
|
255
|
+
delete sanitized.lead_peer;
|
|
256
|
+
return sanitized;
|
|
257
|
+
}
|
|
258
|
+
// v3.6.0 (B3 + B4, logs+sessions study) — top-level human-readable
|
|
259
|
+
// `notices` for tool responses. The 169-session corpus showed two
|
|
260
|
+
// recurring misreads even after the relevant metadata existed:
|
|
261
|
+
// - B3: a caller reading the relator's exclusion from the voting panel
|
|
262
|
+
// as "the runtime dropped a peer" (v3.5.0 added
|
|
263
|
+
// convergence_scope.lead_peer_role but it sits nested — Codex still
|
|
264
|
+
// misread it live on session a3c2660d).
|
|
265
|
+
// - B4: `session.caller_peer_selection_ignored` fired 30x — callers
|
|
266
|
+
// repeatedly try to curate the panel; the v3.3.0 lock silently
|
|
267
|
+
// overrides but nothing surfaces in the response they read.
|
|
268
|
+
// This helper derives a short, can't-miss `notices: string[]` from the
|
|
269
|
+
// pre-lock input vs the orchestrator output. Bounded (max 2 entries),
|
|
270
|
+
// only populated when applicable.
|
|
271
|
+
export function buildResponseNotices(originalInput, output) {
|
|
272
|
+
const notices = [];
|
|
273
|
+
// B4 — peer-selection lock notice. If the caller supplied `peers` or
|
|
274
|
+
// (as a peer caller) `lead_peer`, the v3.3.0 lock stripped it.
|
|
275
|
+
const caller = originalInput.caller ?? "operator";
|
|
276
|
+
const triedPeers = Array.isArray(originalInput.peers) && originalInput.peers.length > 0;
|
|
277
|
+
const triedLeadPeer = caller !== "operator" && originalInput.lead_peer !== undefined;
|
|
278
|
+
if (triedPeers || triedLeadPeer) {
|
|
279
|
+
notices.push(`peer_selection_lock: your ${triedPeers ? "`peers` panel" : "`lead_peer` pin"} was ignored — ` +
|
|
280
|
+
`cross-review always uses the full server-configured peer set (operator directive 2026-05-12: ` +
|
|
281
|
+
`"TODOS OS AGENTES/PEERS SEMPRE PARTICIPAM"). Tune the panel via CROSS_REVIEW_PEER_<NAME> env vars, not per-call.`);
|
|
282
|
+
}
|
|
283
|
+
// B3 — relator-non-voting notice. When a lead_peer is set, spell out
|
|
284
|
+
// that it is the non-voting relator and who the voting colegiado is,
|
|
285
|
+
// so its absence from the vote is never misread as a dropped peer.
|
|
286
|
+
const scope = output.session?.convergence_scope;
|
|
287
|
+
if (scope?.lead_peer && scope.lead_peer_role === "relator_non_voting") {
|
|
288
|
+
const voters = (scope.voting_peers ?? scope.reviewer_peers ?? []).join(", ");
|
|
289
|
+
notices.push(`relator_non_voting: \`${scope.lead_peer}\` is the lottery-selected relator — it authors/revises the ` +
|
|
290
|
+
`artifact and is DELIBERATELY excluded from the voting colegiado (anti-self-review HARD GATE). ` +
|
|
291
|
+
`Voting peers: ${voters || "(none)"}. This is by design, not a dropped peer.`);
|
|
292
|
+
}
|
|
293
|
+
return notices;
|
|
294
|
+
}
|
|
295
|
+
function createRuntime() {
|
|
296
|
+
const config = loadConfig();
|
|
297
|
+
const eventLog = new EventLog(config);
|
|
298
|
+
const holder = {};
|
|
299
|
+
const emit = (event) => {
|
|
300
|
+
eventLog.emit(event);
|
|
301
|
+
holder.orchestrator?.store.appendEvent(event);
|
|
302
|
+
};
|
|
303
|
+
const orchestrator = new CrossReviewOrchestrator(config, emit);
|
|
304
|
+
holder.orchestrator = orchestrator;
|
|
305
|
+
return {
|
|
306
|
+
config,
|
|
307
|
+
eventLog,
|
|
308
|
+
orchestrator,
|
|
309
|
+
// v3.3.0: exposed so the caller-peer-selection lock can route audit
|
|
310
|
+
// events through the same emitter the orchestrator uses (eventLog +
|
|
311
|
+
// session-store append). Public so the handler closures below can
|
|
312
|
+
// grab it without re-plumbing the orchestrator's private emit.
|
|
313
|
+
emit,
|
|
314
|
+
jobs: new Map(),
|
|
315
|
+
controllers: new Map(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function now() {
|
|
319
|
+
return new Date().toISOString();
|
|
320
|
+
}
|
|
321
|
+
export function pruneCompletedJobs(jobs, maxCompleted = 500) {
|
|
322
|
+
const completed = [...jobs.values()]
|
|
323
|
+
.filter((job) => job.status !== "running")
|
|
324
|
+
.sort((a, b) => (a.completed_at ?? "").localeCompare(b.completed_at ?? ""));
|
|
325
|
+
for (const job of completed.slice(0, Math.max(0, completed.length - maxCompleted))) {
|
|
326
|
+
jobs.delete(job.job_id);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function summarizeJobResult(result) {
|
|
330
|
+
if (result && typeof result === "object" && "session" in result) {
|
|
331
|
+
const session = result.session;
|
|
332
|
+
return {
|
|
333
|
+
session_id: session?.session_id,
|
|
334
|
+
outcome: session?.outcome,
|
|
335
|
+
converged: "converged" in result ? result.converged : undefined,
|
|
336
|
+
rounds: "rounds" in result ? result.rounds : undefined,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {};
|
|
340
|
+
}
|
|
341
|
+
function startJob(runtime, kind, sessionId, run) {
|
|
342
|
+
const controller = new AbortController();
|
|
343
|
+
const job = {
|
|
344
|
+
job_id: crypto.randomUUID(),
|
|
345
|
+
kind,
|
|
346
|
+
session_id: sessionId,
|
|
347
|
+
status: "running",
|
|
348
|
+
started_at: now(),
|
|
349
|
+
};
|
|
350
|
+
runtime.jobs.set(job.job_id, job);
|
|
351
|
+
pruneCompletedJobs(runtime.jobs);
|
|
352
|
+
runtime.controllers.set(job.job_id, controller);
|
|
353
|
+
void run(controller.signal)
|
|
354
|
+
.then((result) => {
|
|
355
|
+
job.status = controller.signal.aborted ? "cancelled" : "completed";
|
|
356
|
+
job.completed_at = now();
|
|
357
|
+
job.result_summary = summarizeJobResult(result);
|
|
358
|
+
runtime.controllers.delete(job.job_id);
|
|
359
|
+
if (controller.signal.aborted) {
|
|
360
|
+
try {
|
|
361
|
+
runtime.orchestrator.store.markCancelled(sessionId, "session_cancelled");
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// The job status remains visible even if a session write fails.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
.catch((error) => {
|
|
369
|
+
job.status = controller.signal.aborted ? "cancelled" : "failed";
|
|
370
|
+
job.completed_at = now();
|
|
371
|
+
job.error = safeErrorMessage(error);
|
|
372
|
+
runtime.controllers.delete(job.job_id);
|
|
373
|
+
try {
|
|
374
|
+
if (controller.signal.aborted) {
|
|
375
|
+
runtime.orchestrator.store.markCancelled(sessionId, "session_cancelled");
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
runtime.orchestrator.store.escalateToOperator(sessionId, {
|
|
379
|
+
reason: `Background job failed: ${job.error}`,
|
|
380
|
+
severity: "critical",
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Job state remains available even if the session cannot be updated.
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
return job;
|
|
389
|
+
}
|
|
390
|
+
function runtimeCapabilities(runtime) {
|
|
391
|
+
return {
|
|
392
|
+
stable_release: true,
|
|
393
|
+
api_only: true,
|
|
394
|
+
cli_execution: false,
|
|
395
|
+
durable_sessions: true,
|
|
396
|
+
async_jobs: true,
|
|
397
|
+
cancellation: true,
|
|
398
|
+
restart_recovery: true,
|
|
399
|
+
event_streaming: true,
|
|
400
|
+
token_streaming: runtime.config.streaming.tokens,
|
|
401
|
+
budget_preflight: true,
|
|
402
|
+
// v3.7.3 (operator no-fallback directive 2026-05-14): honest flag —
|
|
403
|
+
// `true` ONLY when the user has explicitly declared fallback models in
|
|
404
|
+
// the central config. The default is NO fallback: a peer whose pinned
|
|
405
|
+
// model is unavailable is retried on the SAME model, then skipped (the
|
|
406
|
+
// round converges on the remaining peers). cross-review never
|
|
407
|
+
// hardcodes a model downgrade — fallback is a deliberate user opt-in.
|
|
408
|
+
model_fallback: Object.values(runtime.config.fallback_models).some((models) => models.length > 0),
|
|
409
|
+
metrics: true,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const TOOL_NAMES = [
|
|
413
|
+
"server_info",
|
|
414
|
+
"runtime_capabilities",
|
|
415
|
+
"probe_peers",
|
|
416
|
+
"session_init",
|
|
417
|
+
"session_list",
|
|
418
|
+
"session_read",
|
|
419
|
+
"ask_peers",
|
|
420
|
+
"session_start_round",
|
|
421
|
+
"run_until_unanimous",
|
|
422
|
+
"session_start_unanimous",
|
|
423
|
+
"session_cancel_job",
|
|
424
|
+
"session_recover_interrupted",
|
|
425
|
+
"session_poll",
|
|
426
|
+
"session_events",
|
|
427
|
+
"session_metrics",
|
|
428
|
+
"session_doctor",
|
|
429
|
+
"session_report",
|
|
430
|
+
"session_check_convergence",
|
|
431
|
+
"session_attach_evidence",
|
|
432
|
+
"session_evidence_checklist_update",
|
|
433
|
+
"session_evidence_judge_pass",
|
|
434
|
+
"session_evidence_judge_consensus_pass",
|
|
435
|
+
"session_judgment_precision_report",
|
|
436
|
+
"contest_verdict",
|
|
437
|
+
"escalate_to_operator",
|
|
438
|
+
"regenerate_caller_tokens",
|
|
439
|
+
"session_sweep",
|
|
440
|
+
"session_finalize",
|
|
441
|
+
];
|
|
442
|
+
export async function main() {
|
|
443
|
+
const runtime = createRuntime();
|
|
444
|
+
// v2.18.0 / F1: initialize the per-host token map (load existing OR
|
|
445
|
+
// generate with mode 0o600). Failure is non-fatal — the v2.17.0
|
|
446
|
+
// clientInfo gate still works for non-migrated hosts. One-shot stderr
|
|
447
|
+
// line on first generation publishes the file path so the operator can
|
|
448
|
+
// distribute the per-agent secrets.
|
|
449
|
+
initHostTokensRecord(runtime.config.data_dir);
|
|
450
|
+
const tokensRecord = getHostTokensRecord();
|
|
451
|
+
if (tokensRecord && process.env.CROSS_REVIEW_TEST_QUIET !== "1") {
|
|
452
|
+
process.stderr.write(`[cross-review] F1 caller capability tokens loaded from ${tokensRecord.filePath} (generated_at=${tokensRecord.generated_at || "unknown"}; distribute the per-agent secrets to each MCP host config as CROSS_REVIEW_CALLER_TOKEN to enable verification_method=token; v2.17.0 clientInfo gate remains active as fallback).\n`);
|
|
453
|
+
}
|
|
454
|
+
else if (!tokensRecord && process.env.CROSS_REVIEW_TEST_QUIET !== "1") {
|
|
455
|
+
process.stderr.write(`[cross-review] F1 caller capability tokens unavailable (failed to load or generate host-tokens.json); the v2.17.0 clientInfo identity gate remains active. Set CROSS_REVIEW_TOKENS_FILE to a writable path or fix data_dir permissions to enable token verification.\n`);
|
|
456
|
+
}
|
|
457
|
+
const server = new McpServer({
|
|
458
|
+
name: "cross-review",
|
|
459
|
+
version: VERSION,
|
|
460
|
+
});
|
|
461
|
+
// v3.7.5 (A2, logs+sessions study 2026-05-15): snapshot the enabled
|
|
462
|
+
// peer set once at boot. Static after config load — `peer_enabled` is
|
|
463
|
+
// env-driven and the runtime does not mutate it. Each lock call
|
|
464
|
+
// passes this into ctx so the audit event only fires when the caller
|
|
465
|
+
// actually overrides the panel (not when the supplied list happens
|
|
466
|
+
// to equal the enabled set).
|
|
467
|
+
const enabledPeersSnapshot = PEERS.filter((peer) => runtime.config.peer_enabled[peer]);
|
|
468
|
+
server.registerTool("server_info", {
|
|
469
|
+
title: "Server Info",
|
|
470
|
+
description: "Return runtime information for the API-only Cross Review MCP server, including version, data directory and active security mode.",
|
|
471
|
+
inputSchema: z.object({ response_format: ResponseFormatSchema }),
|
|
472
|
+
annotations: {
|
|
473
|
+
readOnlyHint: true,
|
|
474
|
+
destructiveHint: false,
|
|
475
|
+
idempotentHint: true,
|
|
476
|
+
openWorldHint: false,
|
|
477
|
+
},
|
|
478
|
+
}, async ({ response_format }) => textResult({
|
|
479
|
+
name: "cross-review",
|
|
480
|
+
publisher: "LCV Ideas & Software",
|
|
481
|
+
version: VERSION,
|
|
482
|
+
release_date: RELEASE_DATE,
|
|
483
|
+
sponsors_url: "https://cross-review.lcv.dev",
|
|
484
|
+
transport: "stdio",
|
|
485
|
+
api_only: true,
|
|
486
|
+
cli_execution: false,
|
|
487
|
+
stable_release: true,
|
|
488
|
+
capabilities: runtimeCapabilities(runtime),
|
|
489
|
+
tools: TOOL_NAMES,
|
|
490
|
+
data_dir: runtime.config.data_dir,
|
|
491
|
+
log_file: runtime.eventLog.path(),
|
|
492
|
+
stub: runtime.config.stub,
|
|
493
|
+
retry_timeout_ms: runtime.config.retry.timeout_ms,
|
|
494
|
+
budget: runtime.config.budget,
|
|
495
|
+
financial_controls: (() => {
|
|
496
|
+
// v3.7.0 (AUDIT-4, Codex super-audit 2026-05-14): readiness
|
|
497
|
+
// is computed over the ENABLED peer subset, not the full
|
|
498
|
+
// PEERS roster. Pre-v3.7.0 a missing rate card for a peer
|
|
499
|
+
// the operator had disabled (CROSS_REVIEW_PEER_<NAME>=off)
|
|
500
|
+
// would falsely report paid_calls_ready=false even though
|
|
501
|
+
// that peer is never called.
|
|
502
|
+
const enabledPeers = PEERS.filter((peer) => runtime.config.peer_enabled[peer]);
|
|
503
|
+
const missingVars = missingFinancialControlVars(runtime.config, enabledPeers, {
|
|
504
|
+
untilStopped: true,
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
paid_calls_ready: missingVars.length === 0,
|
|
508
|
+
missing_variables: missingVars,
|
|
509
|
+
policy: "Paid provider calls are blocked until budget ceilings and per-peer USD-per-million rate cards are explicitly configured.",
|
|
510
|
+
};
|
|
511
|
+
})(),
|
|
512
|
+
prompt: runtime.config.prompt,
|
|
513
|
+
max_output_tokens: runtime.config.max_output_tokens,
|
|
514
|
+
streaming: runtime.config.streaming,
|
|
515
|
+
// v2.12.0: judge auto-wire is now a first-class observable. Operators
|
|
516
|
+
// checking `server_info` know whether shadow is collecting data,
|
|
517
|
+
// which peer is rated, and whether a typo invalidated the config.
|
|
518
|
+
// v2.15.1: surface `consensus_peers` and `configured_consensus_peers_raw`
|
|
519
|
+
// so the multi-peer judge configuration (parsed from
|
|
520
|
+
// CROSS_REVIEW_EVIDENCE_JUDGE_AUTOWIRE_CONSENSUS_PEERS) is visible
|
|
521
|
+
// here instead of silently invisible despite being honored by the
|
|
522
|
+
// dispatcher. v2.15.0 added the parser but forgot the serialization.
|
|
523
|
+
evidence_judge_autowire: {
|
|
524
|
+
mode: runtime.config.evidence_judge_autowire.mode,
|
|
525
|
+
peer: runtime.config.evidence_judge_autowire.peer ?? null,
|
|
526
|
+
active: runtime.config.evidence_judge_autowire.active,
|
|
527
|
+
max_items_per_pass: runtime.config.evidence_judge_autowire.max_items_per_pass,
|
|
528
|
+
configured_mode_raw: runtime.config.evidence_judge_autowire.configured_mode_raw,
|
|
529
|
+
configured_peer_raw: runtime.config.evidence_judge_autowire.configured_peer_raw,
|
|
530
|
+
consensus_peers: runtime.config.evidence_judge_autowire.consensus_peers,
|
|
531
|
+
configured_consensus_peers_raw: runtime.config.evidence_judge_autowire.configured_consensus_peers_raw,
|
|
532
|
+
},
|
|
533
|
+
// v2.14.0: per-peer enable/disable surface. Operators inspecting
|
|
534
|
+
// server_info see the resolved enabled/disabled state of each peer.
|
|
535
|
+
peer_enabled: runtime.config.peer_enabled,
|
|
536
|
+
peers_enabled_count: Object.values(runtime.config.peer_enabled).filter(Boolean).length,
|
|
537
|
+
// v2.18.0 / F1: caller capability tokens status. Surfaces (a)
|
|
538
|
+
// whether host-tokens.json is loaded (operators confirm gate is
|
|
539
|
+
// armed without reading the file), (b) the file path so the
|
|
540
|
+
// operator can locate secrets to distribute, (c) hard-enforce
|
|
541
|
+
// mode flag, (d) generated_at timestamp for rotation audit.
|
|
542
|
+
caller_tokens: {
|
|
543
|
+
loaded: getHostTokensRecord() !== null,
|
|
544
|
+
file_path: getHostTokensRecord()?.filePath ?? null,
|
|
545
|
+
generated_at: getHostTokensRecord()?.generated_at ?? null,
|
|
546
|
+
hard_enforce: isHardEnforceMode(),
|
|
547
|
+
agents: getHostTokensRecord() ? Object.keys(getHostTokensRecord()?.map ?? {}) : [],
|
|
548
|
+
},
|
|
549
|
+
codeql_policy: "Default Setup on GitHub; no advanced workflow committed.",
|
|
550
|
+
secrets_policy: "API keys are read from Windows environment variables only.",
|
|
551
|
+
}, response_format));
|
|
552
|
+
server.registerTool("runtime_capabilities", {
|
|
553
|
+
title: "Runtime Capabilities",
|
|
554
|
+
description: "Return the stable cross-review runtime capability contract and active tool list.",
|
|
555
|
+
inputSchema: z.object({ response_format: ResponseFormatSchema }),
|
|
556
|
+
annotations: {
|
|
557
|
+
readOnlyHint: true,
|
|
558
|
+
destructiveHint: false,
|
|
559
|
+
idempotentHint: true,
|
|
560
|
+
openWorldHint: false,
|
|
561
|
+
},
|
|
562
|
+
}, async ({ response_format }) => textResult({
|
|
563
|
+
name: "cross-review",
|
|
564
|
+
version: VERSION,
|
|
565
|
+
release_date: RELEASE_DATE,
|
|
566
|
+
capabilities: runtimeCapabilities(runtime),
|
|
567
|
+
tools: TOOL_NAMES,
|
|
568
|
+
}, response_format));
|
|
569
|
+
server.registerTool("probe_peers", {
|
|
570
|
+
title: "Probe Peers",
|
|
571
|
+
description: "Query official provider APIs to discover available models for the current API keys, select the highest-capability documented model, and verify provider reachability.",
|
|
572
|
+
inputSchema: z.object({ response_format: ResponseFormatSchema }),
|
|
573
|
+
annotations: {
|
|
574
|
+
readOnlyHint: true,
|
|
575
|
+
destructiveHint: false,
|
|
576
|
+
idempotentHint: true,
|
|
577
|
+
openWorldHint: true,
|
|
578
|
+
},
|
|
579
|
+
}, async ({ response_format }) => textResult(await runtime.orchestrator.probeAll(), response_format));
|
|
580
|
+
server.registerTool("session_init", {
|
|
581
|
+
title: "Initialize Session",
|
|
582
|
+
description: "Create a durable cross-review session after probing provider availability and model selection. This does not call reviewer models yet.",
|
|
583
|
+
inputSchema: z.object({
|
|
584
|
+
task: z.string().min(1).describe("Original task or artifact being reviewed."),
|
|
585
|
+
review_focus: ReviewFocusSchema,
|
|
586
|
+
caller: CallerSchema.default("operator"),
|
|
587
|
+
response_format: ResponseFormatSchema,
|
|
588
|
+
}),
|
|
589
|
+
annotations: {
|
|
590
|
+
readOnlyHint: false,
|
|
591
|
+
destructiveHint: false,
|
|
592
|
+
idempotentHint: false,
|
|
593
|
+
openWorldHint: true,
|
|
594
|
+
},
|
|
595
|
+
}, async ({ task, review_focus, caller, response_format }) => {
|
|
596
|
+
// v2.17.0: identity forgery rejection (operator directive 2026-05-05).
|
|
597
|
+
verifyCallerIdentity(caller, server.server.getClientVersion());
|
|
598
|
+
return textResult(await runtime.orchestrator.initSession(task, caller, review_focus), response_format);
|
|
599
|
+
});
|
|
600
|
+
server.registerTool("session_list", {
|
|
601
|
+
title: "List Sessions",
|
|
602
|
+
description: "List durable sessions saved under the local data directory.",
|
|
603
|
+
inputSchema: z.object({ response_format: ResponseFormatSchema }),
|
|
604
|
+
annotations: {
|
|
605
|
+
readOnlyHint: true,
|
|
606
|
+
destructiveHint: false,
|
|
607
|
+
idempotentHint: true,
|
|
608
|
+
openWorldHint: false,
|
|
609
|
+
},
|
|
610
|
+
}, async ({ response_format }) => textResult(runtime.orchestrator.store.list(), response_format));
|
|
611
|
+
server.registerTool("session_read", {
|
|
612
|
+
title: "Read Session",
|
|
613
|
+
description: "Read a durable session meta.json by session_id.",
|
|
614
|
+
inputSchema: z.object({
|
|
615
|
+
session_id: SessionIdSchema,
|
|
616
|
+
response_format: ResponseFormatSchema,
|
|
617
|
+
}),
|
|
618
|
+
annotations: {
|
|
619
|
+
readOnlyHint: true,
|
|
620
|
+
destructiveHint: false,
|
|
621
|
+
idempotentHint: true,
|
|
622
|
+
openWorldHint: false,
|
|
623
|
+
},
|
|
624
|
+
}, async ({ session_id, response_format }) => textResult(runtime.orchestrator.store.read(session_id), response_format));
|
|
625
|
+
server.registerTool("ask_peers", {
|
|
626
|
+
title: "Ask Peers",
|
|
627
|
+
description: "Run a real API review round against selected peers. Runtime default uses real provider APIs; stubs run only when CROSS_REVIEW_STUB=1.",
|
|
628
|
+
inputSchema: z.object({
|
|
629
|
+
session_id: SessionIdSchema.optional(),
|
|
630
|
+
task: z.string().min(1).max(SCHEMA_TASK_MAX_CHARS),
|
|
631
|
+
review_focus: ReviewFocusSchema,
|
|
632
|
+
draft: z.string().min(1).max(SCHEMA_DRAFT_MAX_CHARS),
|
|
633
|
+
caller: CallerSchema.default("operator"),
|
|
634
|
+
caller_status: z.enum(["READY", "NOT_READY", "NEEDS_EVIDENCE"]).default("READY"),
|
|
635
|
+
peers: z
|
|
636
|
+
.array(PeerSchema)
|
|
637
|
+
.min(1)
|
|
638
|
+
// v3.7.0 (AUDIT-3, Codex super-audit 2026-05-14): PEERS has 6
|
|
639
|
+
// entries since v3.0.0 (Perplexity) — `.max(5)` was a stale
|
|
640
|
+
// regression that rejected an explicit full 6-peer panel
|
|
641
|
+
// before the v3.3.0 peer-selection lock could act, and the
|
|
642
|
+
// emitted JSON Schema announced maxItems:5 contradicting the
|
|
643
|
+
// 6-element default. `.max(PEERS.length)` tracks the roster.
|
|
644
|
+
.max(PEERS.length)
|
|
645
|
+
.default([...PEERS]),
|
|
646
|
+
reasoning_effort_overrides: ReasoningEffortOverridesSchema,
|
|
647
|
+
response_format: ResponseFormatSchema,
|
|
648
|
+
}),
|
|
649
|
+
annotations: {
|
|
650
|
+
readOnlyHint: false,
|
|
651
|
+
destructiveHint: false,
|
|
652
|
+
idempotentHint: false,
|
|
653
|
+
openWorldHint: true,
|
|
654
|
+
},
|
|
655
|
+
}, async ({ response_format, ...input }) => {
|
|
656
|
+
// v2.17.0: identity forgery rejection (operator directive 2026-05-05).
|
|
657
|
+
verifyCallerIdentity(input.caller, server.server.getClientVersion());
|
|
658
|
+
// v3.3.0: caller peer-selection lock — silently strips
|
|
659
|
+
// caller-supplied `peers` (and, for peer callers, `lead_peer`) and
|
|
660
|
+
// emits an audit event for the operator. See lockCallerPeerSelection
|
|
661
|
+
// for the full rationale.
|
|
662
|
+
const locked = lockCallerPeerSelection(input, {
|
|
663
|
+
site: "ask_peers",
|
|
664
|
+
emit: runtime.emit,
|
|
665
|
+
enabledPeers: enabledPeersSnapshot,
|
|
666
|
+
});
|
|
667
|
+
const askPeersOut = await runtime.orchestrator.askPeers(locked);
|
|
668
|
+
// v3.6.0 (B3 + B4): surface relator-non-voting + peer-lock notices.
|
|
669
|
+
return textResult({ ...askPeersOut, notices: buildResponseNotices(input, askPeersOut) }, response_format);
|
|
670
|
+
});
|
|
671
|
+
server.registerTool("session_start_round", {
|
|
672
|
+
title: "Start Review Round",
|
|
673
|
+
description: "Start a real peer-review round in the background and return immediately with a session_id/job_id for polling.",
|
|
674
|
+
inputSchema: z.object({
|
|
675
|
+
session_id: SessionIdSchema.optional(),
|
|
676
|
+
task: z.string().min(1).max(SCHEMA_TASK_MAX_CHARS),
|
|
677
|
+
review_focus: ReviewFocusSchema,
|
|
678
|
+
draft: z.string().min(1).max(SCHEMA_DRAFT_MAX_CHARS),
|
|
679
|
+
caller: CallerSchema.default("operator"),
|
|
680
|
+
caller_status: z.enum(["READY", "NOT_READY", "NEEDS_EVIDENCE"]).default("READY"),
|
|
681
|
+
peers: z
|
|
682
|
+
.array(PeerSchema)
|
|
683
|
+
.min(1)
|
|
684
|
+
// v3.7.0 (AUDIT-3, Codex super-audit 2026-05-14): PEERS has 6
|
|
685
|
+
// entries since v3.0.0 (Perplexity) — `.max(5)` was a stale
|
|
686
|
+
// regression that rejected an explicit full 6-peer panel
|
|
687
|
+
// before the v3.3.0 peer-selection lock could act, and the
|
|
688
|
+
// emitted JSON Schema announced maxItems:5 contradicting the
|
|
689
|
+
// 6-element default. `.max(PEERS.length)` tracks the roster.
|
|
690
|
+
.max(PEERS.length)
|
|
691
|
+
.default([...PEERS]),
|
|
692
|
+
reasoning_effort_overrides: ReasoningEffortOverridesSchema,
|
|
693
|
+
response_format: ResponseFormatSchema,
|
|
694
|
+
}),
|
|
695
|
+
annotations: {
|
|
696
|
+
readOnlyHint: false,
|
|
697
|
+
destructiveHint: false,
|
|
698
|
+
idempotentHint: false,
|
|
699
|
+
openWorldHint: true,
|
|
700
|
+
},
|
|
701
|
+
}, async ({ response_format, ...input }) => {
|
|
702
|
+
// v2.17.0: identity forgery rejection (operator directive 2026-05-05).
|
|
703
|
+
verifyCallerIdentity(input.caller, server.server.getClientVersion());
|
|
704
|
+
// v3.3.0: caller peer-selection lock.
|
|
705
|
+
const locked = lockCallerPeerSelection(input, {
|
|
706
|
+
site: "session_start_round",
|
|
707
|
+
emit: runtime.emit,
|
|
708
|
+
enabledPeers: enabledPeersSnapshot,
|
|
709
|
+
});
|
|
710
|
+
const session = locked.session_id
|
|
711
|
+
? runtime.orchestrator.store.read(locked.session_id)
|
|
712
|
+
: await runtime.orchestrator.initSession(locked.task, locked.caller, locked.review_focus);
|
|
713
|
+
const job = startJob(runtime, "ask_peers", session.session_id, (signal) => runtime.orchestrator.askPeers({ ...locked, session_id: session.session_id, signal }));
|
|
714
|
+
return textResult({
|
|
715
|
+
session_id: session.session_id,
|
|
716
|
+
job,
|
|
717
|
+
poll_tool: "session_poll",
|
|
718
|
+
events_tool: "session_events",
|
|
719
|
+
// v3.6.0 (B4): peer-lock notice surfaces at job start; the
|
|
720
|
+
// relator-non-voting notice (B3) surfaces later via session_poll
|
|
721
|
+
// once the round resolves convergence_scope.
|
|
722
|
+
notices: buildResponseNotices(input, {}),
|
|
723
|
+
}, response_format);
|
|
724
|
+
});
|
|
725
|
+
server.registerTool("run_until_unanimous", {
|
|
726
|
+
title: "Run Until Unanimous",
|
|
727
|
+
description: "Generate or revise a draft and continue real API peer-review rounds until unanimous READY or the configured max_rounds is reached. v2.11.0: when `caller` is set to a peer id (claude|codex|gemini|deepseek|grok), the relator lottery activates: omit `lead_peer` to have the server randomly select a non-caller peer as relator (modeled on judicial colegiados), or supply an explicit `lead_peer` that is NOT the caller. An explicit `lead_peer === caller` is rejected at the server with `caller_cannot_be_lead_peer` — an agent never reviews itself (workspace HARD GATE).",
|
|
728
|
+
inputSchema: z.object({
|
|
729
|
+
task: z.string().min(1).max(SCHEMA_TASK_MAX_CHARS),
|
|
730
|
+
review_focus: ReviewFocusSchema,
|
|
731
|
+
initial_draft: z.string().max(SCHEMA_INITIAL_DRAFT_MAX_CHARS).optional(),
|
|
732
|
+
// v2.11.0: lead_peer is now optional. When omitted with a peer
|
|
733
|
+
// caller, the relator lottery picks one. When omitted with an
|
|
734
|
+
// operator caller, the orchestrator uses "codex" if it is enabled,
|
|
735
|
+
// else the first enabled session peer (v3.7.1 / AUDIT-4: comment
|
|
736
|
+
// refreshed — v3.7.0 / AUDIT-2 replaced the pre-v3.7.0 hardcoded
|
|
737
|
+
// "codex" that ignored peer_enabled).
|
|
738
|
+
lead_peer: PeerSchema.optional(),
|
|
739
|
+
// v2.11.0: caller identifies the petitioner for the lottery.
|
|
740
|
+
// Default "operator" preserves v2.10.0 behavior (no exclusion).
|
|
741
|
+
caller: CallerSchema.default("operator"),
|
|
742
|
+
peers: z
|
|
743
|
+
.array(PeerSchema)
|
|
744
|
+
.min(1)
|
|
745
|
+
// v3.7.0 (AUDIT-3, Codex super-audit 2026-05-14): PEERS has 6
|
|
746
|
+
// entries since v3.0.0 (Perplexity) — `.max(5)` was a stale
|
|
747
|
+
// regression that rejected an explicit full 6-peer panel
|
|
748
|
+
// before the v3.3.0 peer-selection lock could act, and the
|
|
749
|
+
// emitted JSON Schema announced maxItems:5 contradicting the
|
|
750
|
+
// 6-element default. `.max(PEERS.length)` tracks the roster.
|
|
751
|
+
.max(PEERS.length)
|
|
752
|
+
.default([...PEERS]),
|
|
753
|
+
max_rounds: z.number().int().min(1).max(1000).default(8),
|
|
754
|
+
until_stopped: z.boolean().default(false),
|
|
755
|
+
max_cost_usd: z.number().positive().optional(),
|
|
756
|
+
reasoning_effort_overrides: ReasoningEffortOverridesSchema,
|
|
757
|
+
// v2.13.0: ship vs review intent. `ship` (default) — initial_draft
|
|
758
|
+
// is the artifact under refinement; lead_peer produces a NEW
|
|
759
|
+
// REVISED VERSION as prose. `review` — initial_draft is the
|
|
760
|
+
// review subject; lead may emit structured responses.
|
|
761
|
+
// Disambiguates the v2.12 lead_peer meta-review drift bug
|
|
762
|
+
// when the `task` field is phrased as a review act
|
|
763
|
+
// ("Review v..."). See session.lead_drift_detected event.
|
|
764
|
+
// v2.25.0: `circular` joins as a third mode — serial deliberative
|
|
765
|
+
// custody (imported from maestro-app). Caller submits the artifact;
|
|
766
|
+
// rotator-of-the-round either approves unchanged or revises;
|
|
767
|
+
// convergence = full rotation completes without substantive change.
|
|
768
|
+
// No parallel peer-voting in circular mode. Best for producing
|
|
769
|
+
// shared prose/spec artifacts. For approve/reject judgments over
|
|
770
|
+
// external code, prefer ship (default) or review.
|
|
771
|
+
mode: z.enum(["ship", "review", "circular"]).default("ship"),
|
|
772
|
+
// v3.5.0 (CRV2-4): optional structured evidence the caller
|
|
773
|
+
// supplies up-front. When present (non-empty) it satisfies the
|
|
774
|
+
// evidence preflight unconditionally — it is the caller's
|
|
775
|
+
// authoritative declaration that concrete evidence exists for
|
|
776
|
+
// the review. cross-review stays API-only: it does not run
|
|
777
|
+
// git/shell to gather evidence; packaging is a caller-side
|
|
778
|
+
// responsibility (see docs/evidence-preflight.md).
|
|
779
|
+
evidence: z.string().max(SCHEMA_INITIAL_DRAFT_MAX_CHARS).optional(),
|
|
780
|
+
response_format: ResponseFormatSchema,
|
|
781
|
+
}),
|
|
782
|
+
annotations: {
|
|
783
|
+
readOnlyHint: false,
|
|
784
|
+
destructiveHint: false,
|
|
785
|
+
idempotentHint: false,
|
|
786
|
+
openWorldHint: true,
|
|
787
|
+
},
|
|
788
|
+
}, async ({ response_format, ...input }) => {
|
|
789
|
+
// v2.17.0: identity forgery rejection (operator directive 2026-05-05).
|
|
790
|
+
verifyCallerIdentity(input.caller, server.server.getClientVersion());
|
|
791
|
+
// v3.3.0: caller peer-selection lock — peers panel always full
|
|
792
|
+
// enabled set; lead_peer ignored for peer callers (forced lottery).
|
|
793
|
+
const locked = lockCallerPeerSelection(input, {
|
|
794
|
+
site: "run_until_unanimous",
|
|
795
|
+
emit: runtime.emit,
|
|
796
|
+
enabledPeers: enabledPeersSnapshot,
|
|
797
|
+
});
|
|
798
|
+
const runOut = await runtime.orchestrator.runUntilUnanimous(locked);
|
|
799
|
+
// v3.6.0 (B3 + B4): surface relator-non-voting + peer-lock notices.
|
|
800
|
+
return textResult({ ...runOut, notices: buildResponseNotices(input, runOut) }, response_format);
|
|
801
|
+
});
|
|
802
|
+
server.registerTool("session_start_unanimous", {
|
|
803
|
+
title: "Start Until Unanimous",
|
|
804
|
+
description: "Start real API generation/revision rounds in the background until unanimity, max_rounds or budget limit. v2.11.0: same `caller` + relator-lottery semantics as `run_until_unanimous` — see that tool for details.",
|
|
805
|
+
inputSchema: z.object({
|
|
806
|
+
session_id: SessionIdSchema.optional(),
|
|
807
|
+
task: z.string().min(1).max(SCHEMA_TASK_MAX_CHARS),
|
|
808
|
+
review_focus: ReviewFocusSchema,
|
|
809
|
+
initial_draft: z.string().max(SCHEMA_INITIAL_DRAFT_MAX_CHARS).optional(),
|
|
810
|
+
lead_peer: PeerSchema.optional(),
|
|
811
|
+
caller: CallerSchema.default("operator"),
|
|
812
|
+
peers: z
|
|
813
|
+
.array(PeerSchema)
|
|
814
|
+
.min(1)
|
|
815
|
+
// v3.7.0 (AUDIT-3, Codex super-audit 2026-05-14): PEERS has 6
|
|
816
|
+
// entries since v3.0.0 (Perplexity) — `.max(5)` was a stale
|
|
817
|
+
// regression that rejected an explicit full 6-peer panel
|
|
818
|
+
// before the v3.3.0 peer-selection lock could act, and the
|
|
819
|
+
// emitted JSON Schema announced maxItems:5 contradicting the
|
|
820
|
+
// 6-element default. `.max(PEERS.length)` tracks the roster.
|
|
821
|
+
.max(PEERS.length)
|
|
822
|
+
.default([...PEERS]),
|
|
823
|
+
max_rounds: z.number().int().min(1).max(1000).default(8),
|
|
824
|
+
until_stopped: z.boolean().default(false),
|
|
825
|
+
max_cost_usd: z.number().positive().optional(),
|
|
826
|
+
reasoning_effort_overrides: ReasoningEffortOverridesSchema,
|
|
827
|
+
// v2.13.0: see run_until_unanimous for `mode` semantics.
|
|
828
|
+
// v2.25.0: `circular` joins as a third mode — serial deliberative
|
|
829
|
+
// custody (imported from maestro-app). Caller submits the artifact;
|
|
830
|
+
// rotator-of-the-round either approves unchanged or revises;
|
|
831
|
+
// convergence = full rotation completes without substantive change.
|
|
832
|
+
// No parallel peer-voting in circular mode. Best for producing
|
|
833
|
+
// shared prose/spec artifacts. For approve/reject judgments over
|
|
834
|
+
// external code, prefer ship (default) or review.
|
|
835
|
+
mode: z.enum(["ship", "review", "circular"]).default("ship"),
|
|
836
|
+
// v3.5.0 (CRV2-4): optional structured evidence the caller
|
|
837
|
+
// supplies up-front. When present (non-empty) it satisfies the
|
|
838
|
+
// evidence preflight unconditionally — it is the caller's
|
|
839
|
+
// authoritative declaration that concrete evidence exists for
|
|
840
|
+
// the review. cross-review stays API-only: it does not run
|
|
841
|
+
// git/shell to gather evidence; packaging is a caller-side
|
|
842
|
+
// responsibility (see docs/evidence-preflight.md).
|
|
843
|
+
evidence: z.string().max(SCHEMA_INITIAL_DRAFT_MAX_CHARS).optional(),
|
|
844
|
+
response_format: ResponseFormatSchema,
|
|
845
|
+
}),
|
|
846
|
+
annotations: {
|
|
847
|
+
readOnlyHint: false,
|
|
848
|
+
destructiveHint: false,
|
|
849
|
+
idempotentHint: false,
|
|
850
|
+
openWorldHint: true,
|
|
851
|
+
},
|
|
852
|
+
}, async ({ response_format, ...input }) => {
|
|
853
|
+
// v2.17.0: identity forgery rejection (operator directive 2026-05-05).
|
|
854
|
+
verifyCallerIdentity(input.caller, server.server.getClientVersion());
|
|
855
|
+
// v3.3.0: caller peer-selection lock.
|
|
856
|
+
const locked = lockCallerPeerSelection(input, {
|
|
857
|
+
site: "session_start_unanimous",
|
|
858
|
+
emit: runtime.emit,
|
|
859
|
+
enabledPeers: enabledPeersSnapshot,
|
|
860
|
+
});
|
|
861
|
+
// v2.16.0: the durable session caller is always the petitioner,
|
|
862
|
+
// never the relator. Older code used lead_peer as caller for some
|
|
863
|
+
// operator-started unanimous jobs, which polluted audits with
|
|
864
|
+
// caller/lead conflation. Relator identity belongs in
|
|
865
|
+
// convergence_scope.lead_peer after runUntilUnanimous resolves it.
|
|
866
|
+
const initCaller = locked.caller;
|
|
867
|
+
const session = locked.session_id
|
|
868
|
+
? runtime.orchestrator.store.read(locked.session_id)
|
|
869
|
+
: await runtime.orchestrator.initSession(locked.task, initCaller, locked.review_focus);
|
|
870
|
+
const job = startJob(runtime, "run_until_unanimous", session.session_id, (signal) => runtime.orchestrator.runUntilUnanimous({
|
|
871
|
+
...locked,
|
|
872
|
+
session_id: session.session_id,
|
|
873
|
+
signal,
|
|
874
|
+
}));
|
|
875
|
+
return textResult({
|
|
876
|
+
session_id: session.session_id,
|
|
877
|
+
job,
|
|
878
|
+
poll_tool: "session_poll",
|
|
879
|
+
events_tool: "session_events",
|
|
880
|
+
// v3.6.0 (B4): peer-lock notice at job start; relator-non-voting
|
|
881
|
+
// notice (B3) surfaces via session_poll once the round resolves.
|
|
882
|
+
notices: buildResponseNotices(input, {}),
|
|
883
|
+
}, response_format);
|
|
884
|
+
});
|
|
885
|
+
server.registerTool("session_cancel_job", {
|
|
886
|
+
title: "Cancel Session Job",
|
|
887
|
+
description: "Request cancellation for running background jobs in a durable session. Provider calls receive AbortSignal where the provider client supports it.",
|
|
888
|
+
inputSchema: z.object({
|
|
889
|
+
session_id: SessionIdSchema,
|
|
890
|
+
job_id: SessionIdSchema.optional(),
|
|
891
|
+
reason: z.string().min(1).max(300).default("operator_requested"),
|
|
892
|
+
response_format: ResponseFormatSchema,
|
|
893
|
+
}),
|
|
894
|
+
annotations: {
|
|
895
|
+
readOnlyHint: false,
|
|
896
|
+
destructiveHint: false,
|
|
897
|
+
idempotentHint: true,
|
|
898
|
+
openWorldHint: false,
|
|
899
|
+
},
|
|
900
|
+
}, async ({ session_id, job_id, reason, response_format }) => {
|
|
901
|
+
const jobs = [...runtime.jobs.values()].filter((job) => job.session_id === session_id &&
|
|
902
|
+
job.status === "running" &&
|
|
903
|
+
(!job_id || job.job_id === job_id));
|
|
904
|
+
const meta = runtime.orchestrator.store.requestCancellation(session_id, reason, job_id);
|
|
905
|
+
for (const job of jobs) {
|
|
906
|
+
runtime.controllers.get(job.job_id)?.abort(reason);
|
|
907
|
+
}
|
|
908
|
+
if (!jobs.length) {
|
|
909
|
+
runtime.orchestrator.store.markCancelled(session_id, reason);
|
|
910
|
+
}
|
|
911
|
+
return textResult({
|
|
912
|
+
session_id,
|
|
913
|
+
requested: true,
|
|
914
|
+
matched_jobs: jobs,
|
|
915
|
+
control: meta.control,
|
|
916
|
+
}, response_format);
|
|
917
|
+
});
|
|
918
|
+
server.registerTool("session_recover_interrupted", {
|
|
919
|
+
title: "Recover Interrupted Sessions",
|
|
920
|
+
description: "Mark unfinished sessions with stale in-flight rounds as recovered after a MCP host restart so they can be resumed explicitly.",
|
|
921
|
+
inputSchema: z.object({ response_format: ResponseFormatSchema }),
|
|
922
|
+
annotations: {
|
|
923
|
+
readOnlyHint: false,
|
|
924
|
+
destructiveHint: false,
|
|
925
|
+
idempotentHint: true,
|
|
926
|
+
openWorldHint: false,
|
|
927
|
+
},
|
|
928
|
+
}, async ({ response_format }) => {
|
|
929
|
+
const active = new Set([...runtime.jobs.values()]
|
|
930
|
+
.filter((job) => job.status === "running")
|
|
931
|
+
.map((job) => job.session_id));
|
|
932
|
+
return textResult({
|
|
933
|
+
recovered: runtime.orchestrator.store.recoverInterruptedSessions(active),
|
|
934
|
+
}, response_format);
|
|
935
|
+
});
|
|
936
|
+
server.registerTool("session_poll", {
|
|
937
|
+
title: "Poll Session",
|
|
938
|
+
description: "Return durable session state and background job status without waiting for provider calls to finish.",
|
|
939
|
+
inputSchema: z.object({
|
|
940
|
+
session_id: SessionIdSchema,
|
|
941
|
+
response_format: ResponseFormatSchema,
|
|
942
|
+
}),
|
|
943
|
+
annotations: {
|
|
944
|
+
readOnlyHint: true,
|
|
945
|
+
destructiveHint: false,
|
|
946
|
+
idempotentHint: true,
|
|
947
|
+
openWorldHint: false,
|
|
948
|
+
},
|
|
949
|
+
}, async ({ session_id, response_format }) => {
|
|
950
|
+
const session = runtime.orchestrator.store.read(session_id);
|
|
951
|
+
const jobs = [...runtime.jobs.values()].filter((job) => job.session_id === session_id);
|
|
952
|
+
// v3.6.0 (B1, logs+sessions study): `needs_attention` — derived
|
|
953
|
+
// convenience flag. The 169-session corpus showed 28 non-terminal
|
|
954
|
+
// sessions (5 open + 9 stale + 14 blocked), many abandoned by the
|
|
955
|
+
// caller until the 24h sweep aborted them. This flag is true when
|
|
956
|
+
// the session has no terminal `outcome` AND its health is stale or
|
|
957
|
+
// blocked AND there is no running job — i.e. it is sitting
|
|
958
|
+
// un-finalized with nothing in flight and needs the caller to
|
|
959
|
+
// finalize, contest, continue, or cancel it.
|
|
960
|
+
const hasRunningJob = jobs.some((job) => job.status === "running");
|
|
961
|
+
const healthState = session.convergence_health?.state;
|
|
962
|
+
const needsAttention = !session.outcome &&
|
|
963
|
+
!hasRunningJob &&
|
|
964
|
+
(healthState === "stale" || healthState === "blocked");
|
|
965
|
+
// v3.6.0 (B3): relator-non-voting notice surfaced on poll, so an
|
|
966
|
+
// async caller that started via session_start_round /
|
|
967
|
+
// session_start_unanimous sees it once convergence_scope resolves.
|
|
968
|
+
const scope = session.convergence_scope;
|
|
969
|
+
const notices = [];
|
|
970
|
+
if (scope?.lead_peer && scope.lead_peer_role === "relator_non_voting") {
|
|
971
|
+
const voters = (scope.voting_peers ?? scope.reviewer_peers ?? []).join(", ");
|
|
972
|
+
notices.push(`relator_non_voting: \`${scope.lead_peer}\` is the lottery-selected relator — it authors/revises the ` +
|
|
973
|
+
`artifact and is DELIBERATELY excluded from the voting colegiado (anti-self-review HARD GATE). ` +
|
|
974
|
+
`Voting peers: ${voters || "(none)"}. This is by design, not a dropped peer.`);
|
|
975
|
+
}
|
|
976
|
+
if (needsAttention) {
|
|
977
|
+
notices.push(`needs_attention: this session is non-terminal (outcome=null), health=${healthState}, and has no ` +
|
|
978
|
+
`running job — finalize, contest, continue, or cancel it. The 24h stale-session sweep is only a backstop.`);
|
|
979
|
+
}
|
|
980
|
+
return textResult({
|
|
981
|
+
session_id,
|
|
982
|
+
outcome: session.outcome,
|
|
983
|
+
health: session.convergence_health,
|
|
984
|
+
in_flight: session.in_flight,
|
|
985
|
+
rounds: session.rounds.length,
|
|
986
|
+
latest_round: session.rounds.at(-1) ?? null,
|
|
987
|
+
jobs,
|
|
988
|
+
control: session.control,
|
|
989
|
+
needs_attention: needsAttention,
|
|
990
|
+
notices,
|
|
991
|
+
}, response_format);
|
|
992
|
+
});
|
|
993
|
+
server.registerTool("session_metrics", {
|
|
994
|
+
title: "Session Metrics",
|
|
995
|
+
description: "Return aggregate observability metrics across all sessions, or only one session when session_id is provided.",
|
|
996
|
+
inputSchema: z.object({
|
|
997
|
+
session_id: SessionIdSchema.optional(),
|
|
998
|
+
response_format: ResponseFormatSchema,
|
|
999
|
+
}),
|
|
1000
|
+
annotations: {
|
|
1001
|
+
readOnlyHint: true,
|
|
1002
|
+
destructiveHint: false,
|
|
1003
|
+
idempotentHint: true,
|
|
1004
|
+
openWorldHint: false,
|
|
1005
|
+
},
|
|
1006
|
+
}, async ({ session_id, response_format }) => textResult(runtime.orchestrator.store.metrics(session_id), response_format));
|
|
1007
|
+
server.registerTool("session_doctor", {
|
|
1008
|
+
title: "Session Doctor",
|
|
1009
|
+
description: 'Operational audit across durable sessions: open/stale/blocked cases, legacy self-lead metadata, open evidence asks (with per-peer item type drill-down + chronic blockers since v2.22), Grok provider errors, and token-event noise. Read-only by default (does not modify sessions). Pass include_legacy=true to enumerate per-session self_lead_metadata entries (hidden by default since v2.22 because pre-v2.16 sessions carry the legacy artifact at ~38% rate; totals.self_lead_metadata count is always visible). v3.6.0: pass repair=true (opt-in) to recompute convergence_health for sessions stuck in the contradictory outcome="converged"+health="blocked" state left by pre-v3.2.0 corruption — only that specific contradiction is touched, only when explicitly requested; the `repaired` array lists what was fixed.',
|
|
1010
|
+
inputSchema: z.object({
|
|
1011
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
1012
|
+
// v2.22.0 (A.P2): opt-in enumeration of legacy self_lead_metadata
|
|
1013
|
+
// entries. Defaults to false; the headline count in totals stays
|
|
1014
|
+
// visible even when the array is suppressed.
|
|
1015
|
+
include_legacy: z.boolean().optional(),
|
|
1016
|
+
// v3.6.0 (C): opt-in repair pass. Default false keeps the tool
|
|
1017
|
+
// strictly read-only. When true, the contradictory
|
|
1018
|
+
// outcome="converged"+health="blocked" state (pre-v3.2.0
|
|
1019
|
+
// corruption artifact) has convergence_health recomputed from the
|
|
1020
|
+
// latest round; the `repaired` array reports what changed.
|
|
1021
|
+
repair: z.boolean().optional(),
|
|
1022
|
+
response_format: ResponseFormatSchema,
|
|
1023
|
+
}),
|
|
1024
|
+
annotations: {
|
|
1025
|
+
// v3.6.0: no longer unconditionally read-only — repair=true
|
|
1026
|
+
// mutates sessions, so readOnlyHint must be false.
|
|
1027
|
+
readOnlyHint: false,
|
|
1028
|
+
destructiveHint: false,
|
|
1029
|
+
idempotentHint: true,
|
|
1030
|
+
openWorldHint: false,
|
|
1031
|
+
},
|
|
1032
|
+
}, async ({ limit, include_legacy, repair, response_format }) => textResult(runtime.orchestrator.store.sessionDoctor(limit, include_legacy ?? false, repair ?? false), response_format));
|
|
1033
|
+
server.registerTool("session_events", {
|
|
1034
|
+
title: "Read Session Events",
|
|
1035
|
+
description: "Read durable session events from events.ndjson. Use since_seq to incrementally poll long-running sessions.",
|
|
1036
|
+
inputSchema: z.object({
|
|
1037
|
+
session_id: SessionIdSchema,
|
|
1038
|
+
since_seq: z.number().int().min(0).default(0),
|
|
1039
|
+
response_format: ResponseFormatSchema,
|
|
1040
|
+
}),
|
|
1041
|
+
annotations: {
|
|
1042
|
+
readOnlyHint: true,
|
|
1043
|
+
destructiveHint: false,
|
|
1044
|
+
idempotentHint: true,
|
|
1045
|
+
openWorldHint: false,
|
|
1046
|
+
},
|
|
1047
|
+
}, async ({ session_id, since_seq, response_format }) => textResult({
|
|
1048
|
+
session_id,
|
|
1049
|
+
events: runtime.orchestrator.store.readEvents(session_id, since_seq),
|
|
1050
|
+
}, response_format));
|
|
1051
|
+
server.registerTool("session_report", {
|
|
1052
|
+
title: "Session Report",
|
|
1053
|
+
description: "Generate and save a Markdown report with convergence, peer decisions, failures, costs and latest events.",
|
|
1054
|
+
inputSchema: z.object({
|
|
1055
|
+
session_id: SessionIdSchema,
|
|
1056
|
+
response_format: ResponseFormatSchema,
|
|
1057
|
+
}),
|
|
1058
|
+
annotations: {
|
|
1059
|
+
readOnlyHint: true,
|
|
1060
|
+
destructiveHint: false,
|
|
1061
|
+
idempotentHint: true,
|
|
1062
|
+
openWorldHint: false,
|
|
1063
|
+
},
|
|
1064
|
+
}, async ({ session_id, response_format }) => {
|
|
1065
|
+
const session = runtime.orchestrator.store.read(session_id);
|
|
1066
|
+
const markdown = sessionReportMarkdown(session, runtime.orchestrator.store.readEvents(session_id));
|
|
1067
|
+
const path = runtime.orchestrator.store.saveReport(session_id, markdown);
|
|
1068
|
+
return response_format === "markdown"
|
|
1069
|
+
? textResult(markdown, "markdown")
|
|
1070
|
+
: textResult({ session_id, path, markdown }, response_format);
|
|
1071
|
+
});
|
|
1072
|
+
server.registerTool("session_check_convergence", {
|
|
1073
|
+
title: "Check Convergence",
|
|
1074
|
+
description: "Return the latest durable convergence state, health and scope for a saved session without calling providers.",
|
|
1075
|
+
inputSchema: z.object({
|
|
1076
|
+
session_id: SessionIdSchema,
|
|
1077
|
+
response_format: ResponseFormatSchema,
|
|
1078
|
+
}),
|
|
1079
|
+
annotations: {
|
|
1080
|
+
readOnlyHint: true,
|
|
1081
|
+
destructiveHint: false,
|
|
1082
|
+
idempotentHint: true,
|
|
1083
|
+
openWorldHint: false,
|
|
1084
|
+
},
|
|
1085
|
+
}, async ({ session_id, response_format }) => {
|
|
1086
|
+
const session = runtime.orchestrator.store.read(session_id);
|
|
1087
|
+
const latestRound = session.rounds.at(-1);
|
|
1088
|
+
return textResult({
|
|
1089
|
+
session_id: session.session_id,
|
|
1090
|
+
outcome: session.outcome,
|
|
1091
|
+
outcome_reason: session.outcome_reason,
|
|
1092
|
+
convergence: latestRound?.convergence ?? null,
|
|
1093
|
+
convergence_health: session.convergence_health,
|
|
1094
|
+
convergence_scope: session.convergence_scope,
|
|
1095
|
+
in_flight: session.in_flight,
|
|
1096
|
+
failed_attempts: session.failed_attempts ?? [],
|
|
1097
|
+
}, response_format);
|
|
1098
|
+
});
|
|
1099
|
+
server.registerTool("session_attach_evidence", {
|
|
1100
|
+
title: "Attach Evidence",
|
|
1101
|
+
description: "Persist a text evidence artifact under a durable session evidence directory and register it in session metadata.",
|
|
1102
|
+
inputSchema: z.object({
|
|
1103
|
+
session_id: SessionIdSchema,
|
|
1104
|
+
label: z.string().min(1).max(120),
|
|
1105
|
+
content: z.string().min(1).max(2_000_000),
|
|
1106
|
+
content_type: z.string().min(1).max(120).default("text/plain"),
|
|
1107
|
+
extension: z.string().min(1).max(16).default("txt"),
|
|
1108
|
+
response_format: ResponseFormatSchema,
|
|
1109
|
+
}),
|
|
1110
|
+
annotations: {
|
|
1111
|
+
readOnlyHint: false,
|
|
1112
|
+
destructiveHint: false,
|
|
1113
|
+
idempotentHint: false,
|
|
1114
|
+
openWorldHint: false,
|
|
1115
|
+
},
|
|
1116
|
+
}, async ({ session_id, label, content, content_type, extension, response_format }) => textResult(runtime.orchestrator.store.attachEvidence(session_id, {
|
|
1117
|
+
label,
|
|
1118
|
+
content,
|
|
1119
|
+
content_type,
|
|
1120
|
+
extension,
|
|
1121
|
+
}), response_format));
|
|
1122
|
+
server.registerTool("session_evidence_checklist_update", {
|
|
1123
|
+
title: "Update Evidence Checklist Item Status",
|
|
1124
|
+
description: "Operator workflow for the v2.7.0 Evidence Broker. Mark a checklist item as 'satisfied' (operator confirms the ask was answered), 'deferred' (out of scope for this session), 'rejected' (ask itself is unfounded), or 'open' (retract a prior terminal status). The 'addressed' status is reserved for runtime auto-promotion (resurfacing inference) and cannot be set via this tool. Every transition is appended to evidence_status_history with the operator's optional note.",
|
|
1125
|
+
inputSchema: z.object({
|
|
1126
|
+
session_id: SessionIdSchema,
|
|
1127
|
+
item_id: z
|
|
1128
|
+
.string()
|
|
1129
|
+
.min(1)
|
|
1130
|
+
.max(64)
|
|
1131
|
+
.regex(/^[a-f0-9]+$/i, "item_id must be a hex string"),
|
|
1132
|
+
status: z.enum(["open", "satisfied", "deferred", "rejected"]),
|
|
1133
|
+
note: z.string().min(1).max(2000).optional(),
|
|
1134
|
+
response_format: ResponseFormatSchema,
|
|
1135
|
+
}),
|
|
1136
|
+
annotations: {
|
|
1137
|
+
readOnlyHint: false,
|
|
1138
|
+
destructiveHint: false,
|
|
1139
|
+
idempotentHint: false,
|
|
1140
|
+
openWorldHint: false,
|
|
1141
|
+
},
|
|
1142
|
+
}, async ({ session_id, item_id, status, note, response_format }) => textResult(runtime.orchestrator.store.setEvidenceChecklistItemStatus(session_id, item_id, status, {
|
|
1143
|
+
note,
|
|
1144
|
+
by: "operator",
|
|
1145
|
+
}), response_format));
|
|
1146
|
+
server.registerTool("session_evidence_judge_pass", {
|
|
1147
|
+
title: "Run Evidence Judge Pass",
|
|
1148
|
+
description: "v2.9.0 LLM-based satisfied detection for the Evidence Broker. The configured judge peer reads each currently-open checklist item against the supplied draft and returns a structured judgment (satisfied + confidence + rationale). The runtime promotes only items where satisfied=true AND confidence='verified'; everything else stays open. Terminal operator statuses (satisfied/deferred/rejected) and items already addressed by resurfacing-inference are NEVER touched. Items per pass are capped via CROSS_REVIEW_EVIDENCE_JUDGE_MAX_ITEMS_PER_PASS (default 8). Optional item_ids filter narrows the pass to specific items; omit for all-open. The judge_peer is the LLM that performs the judgment — choose any peer with a configured API key. v2.10.0: optional shadow_mode (default false) routes the pass through a non-mutating path that emits session.evidence_judge_pass.shadow_decision events without touching checklist state — operators use it to collect empirical judgment-quality data before relying on active mutation.",
|
|
1149
|
+
inputSchema: z.object({
|
|
1150
|
+
session_id: SessionIdSchema,
|
|
1151
|
+
judge_peer: PeerSchema,
|
|
1152
|
+
draft: z.string().min(1).max(200_000),
|
|
1153
|
+
item_ids: z
|
|
1154
|
+
.array(z
|
|
1155
|
+
.string()
|
|
1156
|
+
.min(1)
|
|
1157
|
+
.max(64)
|
|
1158
|
+
.regex(/^[a-f0-9]+$/i, "item_id must be a hex string"))
|
|
1159
|
+
.max(64)
|
|
1160
|
+
.optional(),
|
|
1161
|
+
round: z.number().int().min(1).max(10_000).optional(),
|
|
1162
|
+
review_focus: z.string().min(1).max(4000).optional(),
|
|
1163
|
+
shadow_mode: z.boolean().optional(),
|
|
1164
|
+
response_format: ResponseFormatSchema,
|
|
1165
|
+
}),
|
|
1166
|
+
annotations: {
|
|
1167
|
+
readOnlyHint: false,
|
|
1168
|
+
destructiveHint: false,
|
|
1169
|
+
idempotentHint: false,
|
|
1170
|
+
openWorldHint: false,
|
|
1171
|
+
},
|
|
1172
|
+
}, async ({ session_id, judge_peer, draft, item_ids, round, review_focus, shadow_mode, response_format, }) => textResult(await runtime.orchestrator.runEvidenceChecklistJudgePass({
|
|
1173
|
+
session_id,
|
|
1174
|
+
judge_peer,
|
|
1175
|
+
draft,
|
|
1176
|
+
item_ids,
|
|
1177
|
+
round,
|
|
1178
|
+
review_focus,
|
|
1179
|
+
mode: shadow_mode ? "shadow" : "active",
|
|
1180
|
+
}), response_format));
|
|
1181
|
+
// v2.14.0 (item 3): multi-peer judge consensus pass. Fires the judge
|
|
1182
|
+
// call against MULTIPLE peers in parallel for each open evidence
|
|
1183
|
+
// checklist item; promotes the item ONLY when all configured judge
|
|
1184
|
+
// peers agree (unanimous verified-satisfied + non-empty rationale +
|
|
1185
|
+
// zero parser_warnings). Reduces single-judge bias risk before
|
|
1186
|
+
// operator-wide active-mode autowire is enabled in high-stakes
|
|
1187
|
+
// scenarios. Cost-aware: each item costs N peer calls in parallel.
|
|
1188
|
+
server.registerTool("session_evidence_judge_consensus_pass", {
|
|
1189
|
+
title: "Run Evidence Judge Consensus Pass",
|
|
1190
|
+
description: "v2.14.0 — multi-peer consensus judge pass. Fires `judgeEvidenceAsk` against ALL `judge_peers` in parallel for each open checklist item; promotes (active mode) ONLY when all peers return verified-satisfied with non-empty rationale and zero parser_warnings. Disagreement leaves the item open with `reason=consensus_disagreement` and `per_peer` details. Shadow mode emits `session.evidence_judge_pass.shadow_decision` events with `consensus_peers` so the precision report tool sees consensus runs in its corpus. Requires at least 2 judge_peers; single-peer callers should use `session_evidence_judge_pass`. All judge_peers must be enabled (CROSS_REVIEW_PEER_<NAME>=on).",
|
|
1191
|
+
inputSchema: z.object({
|
|
1192
|
+
session_id: SessionIdSchema,
|
|
1193
|
+
// v3.7.0 (AUDIT-3): .max(PEERS.length) — same stale-`.max(5)`
|
|
1194
|
+
// regression as the `peers` panel; the 6-peer roster (Perplexity
|
|
1195
|
+
// since v3.0.0) must be representable in a judge consensus.
|
|
1196
|
+
judge_peers: z.array(PeerSchema).min(2).max(PEERS.length),
|
|
1197
|
+
draft: z.string().min(1).max(200_000),
|
|
1198
|
+
item_ids: z
|
|
1199
|
+
.array(z
|
|
1200
|
+
.string()
|
|
1201
|
+
.min(1)
|
|
1202
|
+
.max(64)
|
|
1203
|
+
.regex(/^[a-f0-9]+$/i, "item_id must be a hex string"))
|
|
1204
|
+
.max(64)
|
|
1205
|
+
.optional(),
|
|
1206
|
+
round: z.number().int().min(1).max(10_000).optional(),
|
|
1207
|
+
review_focus: z.string().min(1).max(4_000).optional(),
|
|
1208
|
+
shadow_mode: z.boolean().optional(),
|
|
1209
|
+
response_format: ResponseFormatSchema,
|
|
1210
|
+
}),
|
|
1211
|
+
annotations: {
|
|
1212
|
+
readOnlyHint: false,
|
|
1213
|
+
destructiveHint: false,
|
|
1214
|
+
idempotentHint: false,
|
|
1215
|
+
openWorldHint: false,
|
|
1216
|
+
},
|
|
1217
|
+
}, async ({ session_id, judge_peers, draft, item_ids, round, review_focus, shadow_mode, response_format, }) => textResult(await runtime.orchestrator.runEvidenceChecklistJudgeConsensusPass({
|
|
1218
|
+
session_id,
|
|
1219
|
+
judge_peers,
|
|
1220
|
+
draft,
|
|
1221
|
+
item_ids,
|
|
1222
|
+
round,
|
|
1223
|
+
review_focus,
|
|
1224
|
+
mode: shadow_mode ? "shadow" : "active",
|
|
1225
|
+
}), response_format));
|
|
1226
|
+
// v2.14.0 (item 1): precision/recall/F1 of the shadow judge against
|
|
1227
|
+
// empirical ground truth (whether peers raised the same ask in a
|
|
1228
|
+
// subsequent round). Walks events.ndjson per session, correlates
|
|
1229
|
+
// each `session.evidence_judge_pass.shadow_decision` event with the
|
|
1230
|
+
// matching evidence_checklist item by id, and rolls up per
|
|
1231
|
+
// judge_peer. Operator-triggered observability — DOES NOT mutate
|
|
1232
|
+
// session state; safe to run on any session.
|
|
1233
|
+
server.registerTool("session_judgment_precision_report", {
|
|
1234
|
+
title: "Judgment Precision Report",
|
|
1235
|
+
description: "v2.14.0 — compute precision/recall/F1 of the shadow judge against the empirical ground truth (whether peers raised the same ask in a subsequent round). Walks `session.evidence_judge_pass.shadow_decision` events across all sessions (or a single session via session_id, or filtered by judge peer / since timestamp), correlates each decision with the subsequent evidence_checklist resurfacing behavior, and returns per-peer TP/FP/TN/FN counts plus precision/recall/F1. Decisions whose item.last_round equals the judge round AND no later round exists are excluded as 'no ground truth' (we cannot tell if the ask would have come back). Operator uses this to decide whether to flip a peer from shadow to active mode (item 2 / v2.13).",
|
|
1236
|
+
inputSchema: z.object({
|
|
1237
|
+
peer: PeerSchema.optional(),
|
|
1238
|
+
since: z.string().min(1).max(64).optional(),
|
|
1239
|
+
session_id: SessionIdSchema.optional(),
|
|
1240
|
+
response_format: ResponseFormatSchema,
|
|
1241
|
+
}),
|
|
1242
|
+
annotations: {
|
|
1243
|
+
readOnlyHint: true,
|
|
1244
|
+
destructiveHint: false,
|
|
1245
|
+
idempotentHint: true,
|
|
1246
|
+
openWorldHint: false,
|
|
1247
|
+
},
|
|
1248
|
+
}, async ({ peer, since, session_id, response_format }) => textResult(runtime.orchestrator.store.computeJudgmentPrecisionReport({
|
|
1249
|
+
peer,
|
|
1250
|
+
since,
|
|
1251
|
+
session_id,
|
|
1252
|
+
}), response_format));
|
|
1253
|
+
// v2.14.0 (item 4): tribunal-colegiado contestation. Per the memory
|
|
1254
|
+
// `project_cross_review_v2_tribunal_colegiado_model.md`, caller can
|
|
1255
|
+
// formally contest a final verdict, opening a new deliberation cycle
|
|
1256
|
+
// within the same autos. The original session is preserved (append-
|
|
1257
|
+
// only); a new session is initialized with a structural reference
|
|
1258
|
+
// back. Caller NOT_READY (contesta) → use this tool. Caller READY
|
|
1259
|
+
// (acata) → use session_finalize as before.
|
|
1260
|
+
server.registerTool("contest_verdict", {
|
|
1261
|
+
title: "Contest Verdict",
|
|
1262
|
+
description: "v2.14.0 — formally contest a final verdict and open a new deliberation cycle. Per the cross-review tribunal-colegiado model: caller READY (acata) → session_finalize as usual; caller NOT_READY (contesta) → contest_verdict. Stamps the original session's meta with a `contestation` record (timestamp + reason + original_outcome + new_session_id) and initializes a NEW session whose `contests_session_id` points back to the contested session, preserving the chain of custody append-only across sessions. The original session must be in a final state (converged/aborted/max-rounds); contesting an in-flight session throws cannot_contest_in_flight_session. Once contested, a session cannot be contested again (chain-of-custody invariant) — contest the LATEST session in the chain.",
|
|
1263
|
+
inputSchema: z.object({
|
|
1264
|
+
session_id: SessionIdSchema,
|
|
1265
|
+
reason: z.string().min(1).max(4_000),
|
|
1266
|
+
new_task: z.string().min(1).max(SCHEMA_TASK_MAX_CHARS),
|
|
1267
|
+
new_initial_draft: z.string().max(SCHEMA_INITIAL_DRAFT_MAX_CHARS).optional(),
|
|
1268
|
+
new_caller: CallerSchema.optional(),
|
|
1269
|
+
response_format: ResponseFormatSchema,
|
|
1270
|
+
}),
|
|
1271
|
+
annotations: {
|
|
1272
|
+
readOnlyHint: false,
|
|
1273
|
+
destructiveHint: false,
|
|
1274
|
+
idempotentHint: false,
|
|
1275
|
+
openWorldHint: false,
|
|
1276
|
+
},
|
|
1277
|
+
}, async ({ session_id, reason, new_task, new_initial_draft, new_caller, response_format }) => {
|
|
1278
|
+
// v2.17.0: identity forgery rejection (operator directive 2026-05-05).
|
|
1279
|
+
// Skip when new_caller is undefined (orchestrator falls back to a
|
|
1280
|
+
// sensible default); otherwise verify like the other handlers.
|
|
1281
|
+
if (new_caller !== undefined) {
|
|
1282
|
+
verifyCallerIdentity(new_caller, server.server.getClientVersion());
|
|
1283
|
+
}
|
|
1284
|
+
return textResult(runtime.orchestrator.store.contestVerdict({
|
|
1285
|
+
session_id,
|
|
1286
|
+
reason,
|
|
1287
|
+
new_task,
|
|
1288
|
+
new_initial_draft,
|
|
1289
|
+
new_caller,
|
|
1290
|
+
}), response_format);
|
|
1291
|
+
});
|
|
1292
|
+
server.registerTool("regenerate_caller_tokens", {
|
|
1293
|
+
title: "Regenerate Caller Tokens (F1)",
|
|
1294
|
+
description: "v2.18.0 / F1 (caller capability tokens). Rotate the per-host secret tokens used by the F1 identity gate. OVERWRITES the existing host-tokens.json file (default location: <data_dir>/host-tokens.json; override via CROSS_REVIEW_TOKENS_FILE env var) with freshly generated 256-bit hex secrets — one per agent (codex, claude, gemini, deepseek, grok). Returns the new map so the operator can copy each per-agent secret into the corresponding MCP host config as CROSS_REVIEW_CALLER_TOKEN. AFTER calling this tool, every MCP host carrying a stale token will start being rejected with identity_forgery_blocked: token does not match any known agent. The operator MUST redistribute the secrets and reload the affected hosts. Use cases: (a) initial deployment after first-boot generation; (b) suspected token leak; (c) periodic rotation. The tool has no input parameters and no auth gate — local filesystem access already implies the ability to read or rewrite host-tokens.json directly, so the MCP surface adds no new exposure.",
|
|
1295
|
+
inputSchema: z.object({ response_format: ResponseFormatSchema }),
|
|
1296
|
+
annotations: {
|
|
1297
|
+
readOnlyHint: false,
|
|
1298
|
+
destructiveHint: true,
|
|
1299
|
+
idempotentHint: false,
|
|
1300
|
+
openWorldHint: false,
|
|
1301
|
+
},
|
|
1302
|
+
}, async ({ response_format }) => {
|
|
1303
|
+
const generated = f1GenerateHostTokens(runtime.config.data_dir, {
|
|
1304
|
+
overwrite: true,
|
|
1305
|
+
});
|
|
1306
|
+
if (!generated) {
|
|
1307
|
+
throw new Error("regenerate_caller_tokens: failed to write host-tokens.json (no record returned); check data_dir / CROSS_REVIEW_TOKENS_FILE permissions.");
|
|
1308
|
+
}
|
|
1309
|
+
setHostTokensRecord({
|
|
1310
|
+
filePath: generated.filePath,
|
|
1311
|
+
map: generated.map,
|
|
1312
|
+
generated_at: generated.generated_at,
|
|
1313
|
+
});
|
|
1314
|
+
return textResult({
|
|
1315
|
+
ok: true,
|
|
1316
|
+
file_path: generated.filePath,
|
|
1317
|
+
generated_at: generated.generated_at,
|
|
1318
|
+
tokens: generated.map,
|
|
1319
|
+
next_steps: [
|
|
1320
|
+
"Copy each per-agent secret into the corresponding MCP host config as CROSS_REVIEW_CALLER_TOKEN.",
|
|
1321
|
+
"Reload the affected MCP hosts so the new env value is picked up.",
|
|
1322
|
+
"Stale tokens will start being rejected with identity_forgery_blocked: token does not match any known agent.",
|
|
1323
|
+
],
|
|
1324
|
+
}, response_format);
|
|
1325
|
+
});
|
|
1326
|
+
server.registerTool("escalate_to_operator", {
|
|
1327
|
+
title: "Escalate To Operator",
|
|
1328
|
+
description: "Record a durable operator escalation for sessions that require human judgment or external intervention.",
|
|
1329
|
+
inputSchema: z.object({
|
|
1330
|
+
session_id: SessionIdSchema,
|
|
1331
|
+
reason: z.string().min(1).max(1000),
|
|
1332
|
+
severity: z.enum(["info", "warning", "critical"]).default("warning"),
|
|
1333
|
+
response_format: ResponseFormatSchema,
|
|
1334
|
+
}),
|
|
1335
|
+
annotations: {
|
|
1336
|
+
readOnlyHint: false,
|
|
1337
|
+
destructiveHint: false,
|
|
1338
|
+
idempotentHint: false,
|
|
1339
|
+
openWorldHint: false,
|
|
1340
|
+
},
|
|
1341
|
+
}, async ({ session_id, reason, severity, response_format }) => textResult(runtime.orchestrator.store.escalateToOperator(session_id, { reason, severity }), response_format));
|
|
1342
|
+
server.registerTool("session_sweep", {
|
|
1343
|
+
title: "Sweep Idle Sessions",
|
|
1344
|
+
description: "Finalize unfinished sessions whose metadata has been idle for at least 24 hours. v3.7.5 (B1): opt-in `prune_corrupt` also removes stale entries from the corrupt_sessions/ quarantine directory.",
|
|
1345
|
+
inputSchema: z.object({
|
|
1346
|
+
idle_minutes: z.number().min(1440).max(100_000).default(1440),
|
|
1347
|
+
outcome: z.enum(["aborted", "max-rounds"]).default("aborted"),
|
|
1348
|
+
reason: z.string().min(1).max(200).default("stale"),
|
|
1349
|
+
// v3.7.5 (B1, logs+sessions study 2026-05-15): opt-in
|
|
1350
|
+
// quarantine cleanup. Default false → behavior identical to
|
|
1351
|
+
// v3.7.4 (returns the SessionMeta[] array). When true, the
|
|
1352
|
+
// response wraps the array in `{ swept, pruned_corrupt }` and
|
|
1353
|
+
// additionally removes corrupt_sessions/* entries older than
|
|
1354
|
+
// `corrupt_min_age_days` (default 30 days).
|
|
1355
|
+
prune_corrupt: z.boolean().default(false),
|
|
1356
|
+
corrupt_min_age_days: z.number().int().min(1).max(365).default(30),
|
|
1357
|
+
response_format: ResponseFormatSchema,
|
|
1358
|
+
}),
|
|
1359
|
+
annotations: {
|
|
1360
|
+
readOnlyHint: false,
|
|
1361
|
+
destructiveHint: false,
|
|
1362
|
+
idempotentHint: true,
|
|
1363
|
+
openWorldHint: false,
|
|
1364
|
+
},
|
|
1365
|
+
}, async ({ idle_minutes, outcome, reason, prune_corrupt, corrupt_min_age_days, response_format, }) => {
|
|
1366
|
+
const swept = runtime.orchestrator.store.sweepIdle(idle_minutes * 60_000, outcome, reason);
|
|
1367
|
+
if (!prune_corrupt) {
|
|
1368
|
+
return textResult(swept, response_format);
|
|
1369
|
+
}
|
|
1370
|
+
const pruneReport = runtime.orchestrator.store.pruneCorruptSessions(corrupt_min_age_days * 24 * 60 * 60 * 1000);
|
|
1371
|
+
return textResult({
|
|
1372
|
+
swept,
|
|
1373
|
+
pruned_corrupt: {
|
|
1374
|
+
threshold_days: corrupt_min_age_days,
|
|
1375
|
+
...pruneReport,
|
|
1376
|
+
},
|
|
1377
|
+
}, response_format);
|
|
1378
|
+
});
|
|
1379
|
+
server.registerTool("session_finalize", {
|
|
1380
|
+
title: "Finalize Session",
|
|
1381
|
+
description: "Mark a durable session as converged, aborted or max-rounds with an optional reason.",
|
|
1382
|
+
inputSchema: z.object({
|
|
1383
|
+
session_id: SessionIdSchema,
|
|
1384
|
+
outcome: z.enum(["converged", "aborted", "max-rounds"]),
|
|
1385
|
+
reason: z.string().max(200).optional(),
|
|
1386
|
+
response_format: ResponseFormatSchema,
|
|
1387
|
+
}),
|
|
1388
|
+
annotations: {
|
|
1389
|
+
readOnlyHint: false,
|
|
1390
|
+
destructiveHint: false,
|
|
1391
|
+
idempotentHint: true,
|
|
1392
|
+
openWorldHint: false,
|
|
1393
|
+
},
|
|
1394
|
+
}, async ({ session_id, outcome, reason, response_format }) => textResult(runtime.orchestrator.store.finalize(session_id, outcome, reason), response_format));
|
|
1395
|
+
await server.connect(new StdioServerTransport());
|
|
1396
|
+
console.error("cross-review running on stdio");
|
|
1397
|
+
// v2.27.1 (cold-start hardening): boot-time sweeps + notices are
|
|
1398
|
+
// deferred 30s instead of running on `setImmediate`. The Claude Code
|
|
1399
|
+
// MCP host has a stricter spawn-to-initialize timeout than other hosts;
|
|
1400
|
+
// pre-v2.27.1 the FS walks (4 sweeps × up to 209 session dirs each on
|
|
1401
|
+
// a busy operator) plus the boot notices ran on the same event-loop
|
|
1402
|
+
// tick as the initialize handshake response, pushing it past Claude
|
|
1403
|
+
// Code's threshold while remaining tolerated by Codex CLI / Gemini
|
|
1404
|
+
// Code Assist / VS Code / Antigravity / Grok CLI / DeepSeek CLI.
|
|
1405
|
+
// Deferring 30s lets the handshake respond in <200 ms while keeping
|
|
1406
|
+
// the housekeeping work — it just runs once the operator is idle.
|
|
1407
|
+
// 0 ms would also work but a small delay leaves room for an
|
|
1408
|
+
// immediate `tools/list` follow-up to also clear before disk I/O.
|
|
1409
|
+
const STARTUP_SWEEP_DELAY_MS = 30_000;
|
|
1410
|
+
setTimeout(() => {
|
|
1411
|
+
try {
|
|
1412
|
+
const tmpSweep = runtime.orchestrator.store.sweepOrphanTmpFiles();
|
|
1413
|
+
if (tmpSweep.scanned > 0) {
|
|
1414
|
+
console.error("[cross-review] startup tmp sweep:", JSON.stringify(tmpSweep));
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
catch (err) {
|
|
1418
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1419
|
+
console.error(`[cross-review] startup tmp sweep error: ${message}`);
|
|
1420
|
+
}
|
|
1421
|
+
}, STARTUP_SWEEP_DELAY_MS);
|
|
1422
|
+
setTimeout(() => {
|
|
1423
|
+
try {
|
|
1424
|
+
const inFlightSweep = runtime.orchestrator.store.clearStaleInFlight();
|
|
1425
|
+
if (inFlightSweep.scanned > 0) {
|
|
1426
|
+
console.error("[cross-review] startup in_flight sweep:", JSON.stringify(inFlightSweep));
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
catch (err) {
|
|
1430
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1431
|
+
console.error(`[cross-review] startup in_flight sweep error: ${message}`);
|
|
1432
|
+
}
|
|
1433
|
+
}, STARTUP_SWEEP_DELAY_MS);
|
|
1434
|
+
// v2.5.0: companion to clearStaleInFlight — abort sessions that the
|
|
1435
|
+
// caller never finalized. Runs AFTER the in_flight sweep (deferred via
|
|
1436
|
+
// setTimeout, same delay so order is preserved by registration order)
|
|
1437
|
+
// so a session whose in_flight got cleared this same boot is
|
|
1438
|
+
// immediately eligible for staleness review.
|
|
1439
|
+
setTimeout(() => {
|
|
1440
|
+
try {
|
|
1441
|
+
const abortSweep = runtime.orchestrator.store.abortStaleSessions();
|
|
1442
|
+
if (abortSweep.scanned > 0) {
|
|
1443
|
+
console.error("[cross-review] startup stale-session abort sweep:", JSON.stringify(abortSweep));
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
catch (err) {
|
|
1447
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1448
|
+
console.error(`[cross-review] startup stale-session abort sweep error: ${message}`);
|
|
1449
|
+
}
|
|
1450
|
+
}, STARTUP_SWEEP_DELAY_MS);
|
|
1451
|
+
// v2.27.0: prune finalized sessions older than CROSS_REVIEW_PRUNE_AFTER_DAYS
|
|
1452
|
+
// (default 60). Empirically motivated by 534 sessions accumulated by
|
|
1453
|
+
// 2026-05-12 inflating sweep + list cost. Disable with PRUNE_AFTER_DAYS=0.
|
|
1454
|
+
setTimeout(() => {
|
|
1455
|
+
try {
|
|
1456
|
+
const envDisable = (process.env.CROSS_REVIEW_PRUNE_AFTER_DAYS ?? "").trim() === "0";
|
|
1457
|
+
if (envDisable)
|
|
1458
|
+
return;
|
|
1459
|
+
const pruneSweep = runtime.orchestrator.store.pruneOldSessions();
|
|
1460
|
+
if (pruneSweep.pruned > 0) {
|
|
1461
|
+
console.error("[cross-review] startup prune sweep:", JSON.stringify(pruneSweep));
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
catch (err) {
|
|
1465
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1466
|
+
console.error(`[cross-review] startup prune sweep error: ${message}`);
|
|
1467
|
+
}
|
|
1468
|
+
}, STARTUP_SWEEP_DELAY_MS);
|
|
1469
|
+
// v2.10.0 / v2.12.0: surface judge auto-wire misconfiguration at boot.
|
|
1470
|
+
// Per operator request the runtime never throws on a stray env value (a
|
|
1471
|
+
// typo must not break a paying review-host); we log a single notice so
|
|
1472
|
+
// the operator notices the dead-letter case during real runs. Source of
|
|
1473
|
+
// truth is `runtime.config.evidence_judge_autowire` (parsed by
|
|
1474
|
+
// loadConfig); this notice no longer re-reads env vars.
|
|
1475
|
+
setTimeout(() => {
|
|
1476
|
+
const autowire = runtime.config.evidence_judge_autowire;
|
|
1477
|
+
if (autowire.mode === "off" && autowire.configured_mode_raw === "")
|
|
1478
|
+
return;
|
|
1479
|
+
if (autowire.mode !== "off" && autowire.mode !== "shadow" && autowire.mode !== "active") {
|
|
1480
|
+
console.error(`[cross-review] notice: CROSS_REVIEW_EVIDENCE_JUDGE_AUTOWIRE_MODE="${autowire.configured_mode_raw}" is not recognized; valid values are "off", "shadow" and "active". Auto-wire will be skipped.`);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (autowire.mode === "off")
|
|
1484
|
+
return;
|
|
1485
|
+
if (!autowire.active) {
|
|
1486
|
+
console.error(`[cross-review] notice: CROSS_REVIEW_EVIDENCE_JUDGE_AUTOWIRE_MODE=${autowire.mode} is set but CROSS_REVIEW_EVIDENCE_JUDGE_AUTOWIRE_PEER ("${autowire.configured_peer_raw}") is missing or not one of codex|claude|gemini|deepseek. ${autowire.mode === "active" ? "Active" : "Shadow"} auto-wire will be skipped per round; configure the peer to enable it.`);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (autowire.mode === "active") {
|
|
1490
|
+
// v2.14.0 item 2: WARN loudly when active mode is on. Active
|
|
1491
|
+
// mutates session state; operator must have validated the
|
|
1492
|
+
// judge_peer's precision via session_judgment_precision_report
|
|
1493
|
+
// before flipping. Surface the WARN every boot so an inadvertent
|
|
1494
|
+
// env carry-over from a test run is visible.
|
|
1495
|
+
console.error(`[cross-review] WARN: judge auto-wire active in ACTIVE mode via peer "${autowire.peer}" — verified-satisfied judgments WILL mutate evidence checklist state (markEvidenceItemAddressedByJudge). Run session_judgment_precision_report and confirm the judge's F1 is acceptable before relying on this in production. Set MODE=shadow to revert to non-mutating data collection.`);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
console.error(`[cross-review] notice: judge auto-wire active in SHADOW mode via peer "${autowire.peer}" (max_items_per_pass=${autowire.max_items_per_pass}). Every askPeers round will fire a non-mutating judge pass; events session.evidence_judge_pass.shadow_decision are emitted per item.`);
|
|
1499
|
+
}, STARTUP_SWEEP_DELAY_MS);
|
|
1500
|
+
// v2.15.0 (item 4A boot warning): when operator configured a
|
|
1501
|
+
// CROSS_REVIEW_GROK_REASONING_EFFORT but the chosen model is NOT in
|
|
1502
|
+
// the allowlist (grok-4.20-multi-agent and grok-4.3 accept the field
|
|
1503
|
+
// per xAI docs — see GROK_REASONING_EFFORT_MODELS_BOOT_NOTICE below),
|
|
1504
|
+
// inform that the value will be ignored at the wire level.
|
|
1505
|
+
// Catches misconfigurations early instead of letting the operator
|
|
1506
|
+
// assume reasoning intensity is being applied when xAI silently
|
|
1507
|
+
// ignores it (or when a future model would reject with 400).
|
|
1508
|
+
setTimeout(() => {
|
|
1509
|
+
if (!runtime.config.peer_enabled.grok)
|
|
1510
|
+
return;
|
|
1511
|
+
const grokModel = runtime.config.models.grok;
|
|
1512
|
+
const reasoningSetExplicitly = Boolean(process.env.CROSS_REVIEW_GROK_REASONING_EFFORT);
|
|
1513
|
+
if (!reasoningSetExplicitly)
|
|
1514
|
+
return;
|
|
1515
|
+
if (GROK_REASONING_EFFORT_MODELS_BOOT_NOTICE.has(grokModel))
|
|
1516
|
+
return;
|
|
1517
|
+
console.error(`[cross-review] notice: GrokAdapter — model="${grokModel}" does NOT accept reasoning.effort per xAI docs (only grok-4.20-multi-agent and grok-4.3 do). CROSS_REVIEW_GROK_REASONING_EFFORT="${process.env.CROSS_REVIEW_GROK_REASONING_EFFORT}" will be IGNORED at the wire level for this model. xAI auto-applies reasoning internally for the Grok-4 lineup. Set CROSS_REVIEW_GROK_MODEL=grok-4.20-multi-agent (or grok-4.3) to enable explicit reasoning.effort control.`);
|
|
1518
|
+
}, STARTUP_SWEEP_DELAY_MS);
|
|
1519
|
+
// v3.0.0: Perplexity sixth peer — boot notice for reasoning_effort
|
|
1520
|
+
// capability. Only `sonar-reasoning-pro` and `sonar-deep-research`
|
|
1521
|
+
// accept `reasoning_effort` per Perplexity docs (sonar / sonar-pro
|
|
1522
|
+
// ignore the field — no chain-of-thought stage). When the operator
|
|
1523
|
+
// configures CROSS_REVIEW_PERPLEXITY_REASONING_EFFORT but the chosen
|
|
1524
|
+
// model lacks the capability, surface a stderr notice so the operator
|
|
1525
|
+
// sees the dead-letter case during real runs.
|
|
1526
|
+
setTimeout(() => {
|
|
1527
|
+
if (!runtime.config.peer_enabled.perplexity)
|
|
1528
|
+
return;
|
|
1529
|
+
const perplexityModel = runtime.config.models.perplexity;
|
|
1530
|
+
const reasoningSetExplicitly = Boolean(process.env.CROSS_REVIEW_PERPLEXITY_REASONING_EFFORT);
|
|
1531
|
+
if (!reasoningSetExplicitly)
|
|
1532
|
+
return;
|
|
1533
|
+
if (PERPLEXITY_REASONING_EFFORT_MODELS_BOOT_NOTICE.has(perplexityModel))
|
|
1534
|
+
return;
|
|
1535
|
+
console.error(`[cross-review] notice: PerplexityAdapter — model="${perplexityModel}" does NOT accept reasoning_effort per Perplexity docs (only sonar-reasoning-pro and sonar-deep-research do). CROSS_REVIEW_PERPLEXITY_REASONING_EFFORT="${process.env.CROSS_REVIEW_PERPLEXITY_REASONING_EFFORT}" will be IGNORED at the wire level for this model. Set CROSS_REVIEW_PERPLEXITY_MODEL=sonar-reasoning-pro (default) to enable explicit reasoning_effort control.`);
|
|
1536
|
+
}, STARTUP_SWEEP_DELAY_MS);
|
|
1537
|
+
}
|
|
1538
|
+
// v2.15.0: shadow copy of `peers/grok.ts:GROK_REASONING_EFFORT_MODELS`
|
|
1539
|
+
// for the boot notice. Avoids creating a hard import dependency from
|
|
1540
|
+
// the server boot path into a peer adapter module. If xAI adds models
|
|
1541
|
+
// to the reasoning-capable set, both lists must update together.
|
|
1542
|
+
const GROK_REASONING_EFFORT_MODELS_BOOT_NOTICE = new Set([
|
|
1543
|
+
"grok-4.20-multi-agent",
|
|
1544
|
+
// v3.7.3 (Codex v3.7.2 parecer, AUDIT-2): this shadow set had drifted
|
|
1545
|
+
// from `peers/grok.ts:GROK_REASONING_EFFORT_MODELS`, which has accepted
|
|
1546
|
+
// grok-4.3 since v2.18.4 (xAI docs WebFetch-verified 2026-05-07). Kept
|
|
1547
|
+
// in sync per the "both lists must update together" contract above.
|
|
1548
|
+
"grok-4.3",
|
|
1549
|
+
]);
|
|
1550
|
+
// v3.0.0: shadow copy of `peers/perplexity.ts:PERPLEXITY_REASONING_EFFORT_MODELS`
|
|
1551
|
+
// for the boot notice (same rationale as the GROK_* shadow above — no
|
|
1552
|
+
// hard import dependency from server boot path into the adapter).
|
|
1553
|
+
// When Perplexity adds new reasoning-capable models, both lists must
|
|
1554
|
+
// update together.
|
|
1555
|
+
const PERPLEXITY_REASONING_EFFORT_MODELS_BOOT_NOTICE = new Set([
|
|
1556
|
+
"sonar-reasoning-pro",
|
|
1557
|
+
"sonar-deep-research",
|
|
1558
|
+
]);
|
|
1559
|
+
// v2.4.0 / cross-review R6 follow-up (CI failure 25199679588): guard
|
|
1560
|
+
// main() so it only runs when this module is invoked as the entry point
|
|
1561
|
+
// (e.g. `bin/cross-review` or `node dist/src/mcp/server.js`). Without
|
|
1562
|
+
// the guard, any module that imports a named export from here (the smoke
|
|
1563
|
+
// suite imports `SessionIdSchema` and `pruneCompletedJobs`) triggers a
|
|
1564
|
+
// full server boot at import time — and in CI that boot ran with the
|
|
1565
|
+
// stub flag set but without confirmation, tripping the v2.4.0 P1.1
|
|
1566
|
+
// fail-fast gate before scripts/smoke.ts could write the confirmation
|
|
1567
|
+
// env var. Comparing `import.meta.url` to `process.argv[1]` is the
|
|
1568
|
+
// canonical ESM "is main module" check; a side benefit is that bin
|
|
1569
|
+
// installs (which resolve through symlinks) still match because we
|
|
1570
|
+
// compare resolved paths.
|
|
1571
|
+
const __isMainModule = (() => {
|
|
1572
|
+
if (!process.argv[1])
|
|
1573
|
+
return false;
|
|
1574
|
+
const moduleFile = fileURLToPath(import.meta.url);
|
|
1575
|
+
const argvFile = path.resolve(process.argv[1]);
|
|
1576
|
+
return moduleFile === argvFile;
|
|
1577
|
+
})();
|
|
1578
|
+
if (__isMainModule) {
|
|
1579
|
+
main().catch((error) => {
|
|
1580
|
+
console.error(error);
|
|
1581
|
+
process.exit(1);
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
//# sourceMappingURL=server.js.map
|