@probelabs/visor
Version:
AI-powered code review tool for GitHub Pull Requests - CLI and GitHub Action
1,408 lines (1,387 loc) • 505 kB
JavaScript
import {
init_tracer_init,
initializeTracer
} from "./chunk-TUTOLSFV.mjs";
import {
MemoryStore,
createExtendedLiquid,
createPermissionHelpers,
detectLocalMode,
init_logger,
logger,
logger_exports,
resolveAssociationFromEvent
} from "./chunk-B5QBV2QJ.mjs";
import {
addEvent,
addFailIfTriggered,
context,
emitNdjsonFallback,
emitNdjsonSpanWithEvents,
fallback_ndjson_exports,
init_fallback_ndjson,
trace,
withActiveSpan
} from "./chunk-33QVZ2D4.mjs";
import {
__esm,
__export,
__require,
__toCommonJS
} from "./chunk-WMJKH4XE.mjs";
// src/session-registry.ts
var session_registry_exports = {};
__export(session_registry_exports, {
SessionRegistry: () => SessionRegistry
});
var SessionRegistry;
var init_session_registry = __esm({
"src/session-registry.ts"() {
"use strict";
SessionRegistry = class _SessionRegistry {
static instance;
sessions = /* @__PURE__ */ new Map();
exitHandlerRegistered = false;
constructor() {
this.registerExitHandlers();
}
/**
* Get the singleton instance of SessionRegistry
*/
static getInstance() {
if (!_SessionRegistry.instance) {
_SessionRegistry.instance = new _SessionRegistry();
}
return _SessionRegistry.instance;
}
/**
* Register a ProbeAgent session
*/
registerSession(sessionId, agent) {
console.error(`\u{1F504} Registering AI session: ${sessionId}`);
this.sessions.set(sessionId, agent);
}
/**
* Get an existing ProbeAgent session
*/
getSession(sessionId) {
const agent = this.sessions.get(sessionId);
if (agent) {
console.error(`\u267B\uFE0F Reusing AI session: ${sessionId}`);
}
return agent;
}
/**
* Remove a session from the registry
*/
unregisterSession(sessionId) {
if (this.sessions.has(sessionId)) {
console.error(`\u{1F5D1}\uFE0F Unregistering AI session: ${sessionId}`);
const agent = this.sessions.get(sessionId);
this.sessions.delete(sessionId);
if (agent && typeof agent.cleanup === "function") {
try {
agent.cleanup();
} catch (error) {
console.error(`\u26A0\uFE0F Warning: Failed to cleanup ProbeAgent: ${error}`);
}
}
}
}
/**
* Clear all sessions (useful for cleanup)
*/
clearAllSessions() {
console.error(`\u{1F9F9} Clearing all AI sessions (${this.sessions.size} sessions)`);
for (const [, agent] of this.sessions.entries()) {
if (agent && typeof agent.cleanup === "function") {
try {
agent.cleanup();
} catch {
}
}
}
this.sessions.clear();
}
/**
* Get all active session IDs
*/
getActiveSessionIds() {
return Array.from(this.sessions.keys());
}
/**
* Check if a session exists
*/
hasSession(sessionId) {
return this.sessions.has(sessionId);
}
/**
* Clone a session with a new session ID using ProbeAgent's official clone() method
* This uses ProbeAgent's built-in cloning which automatically handles:
* - Intelligent filtering of internal messages (schema reminders, tool prompts, etc.)
* - Preserving system message for cache efficiency
* - Deep copying conversation history
* - Copying agent configuration
*/
async cloneSession(sourceSessionId, newSessionId, checkName) {
const sourceAgent = this.sessions.get(sourceSessionId);
if (!sourceAgent) {
console.error(`\u26A0\uFE0F Cannot clone session: ${sourceSessionId} not found`);
return void 0;
}
try {
const clonedAgent = sourceAgent.clone({
sessionId: newSessionId,
stripInternalMessages: true,
// Remove schema reminders, tool prompts, etc.
keepSystemMessage: true,
// Keep for cache efficiency
deepCopy: true
// Safe deep copy of history
});
if (sourceAgent.debug && checkName) {
try {
const { initializeTracer: initializeTracer2 } = await import("./tracer-init-WC75N5NW.mjs");
const tracerResult = await initializeTracer2(newSessionId, checkName);
if (tracerResult) {
clonedAgent.tracer = tracerResult.tracer;
clonedAgent._telemetryConfig = tracerResult.telemetryConfig;
clonedAgent._traceFilePath = tracerResult.filePath;
}
} catch (traceError) {
console.error(
"\u26A0\uFE0F Warning: Failed to initialize tracing for cloned session:",
traceError
);
}
}
if (sourceAgent._mcpInitialized && typeof clonedAgent.initialize === "function") {
try {
await clonedAgent.initialize();
console.error(`\u{1F527} Initialized MCP tools for cloned session`);
} catch (initError) {
console.error(`\u26A0\uFE0F Warning: Failed to initialize cloned agent: ${initError}`);
}
}
const historyLength = clonedAgent.history?.length || 0;
console.error(
`\u{1F4CB} Cloned session ${sourceSessionId} \u2192 ${newSessionId} using ProbeAgent.clone() (${historyLength} messages, internal messages filtered)`
);
this.registerSession(newSessionId, clonedAgent);
return clonedAgent;
} catch (error) {
console.error(`\u26A0\uFE0F Failed to clone session ${sourceSessionId}:`, error);
return void 0;
}
}
/**
* Register process exit handlers to cleanup sessions on exit
*/
registerExitHandlers() {
if (this.exitHandlerRegistered) {
return;
}
const cleanupAndExit = (signal) => {
if (this.sessions.size > 0) {
console.error(`
\u{1F9F9} [${signal}] Cleaning up ${this.sessions.size} active AI sessions...`);
this.clearAllSessions();
}
};
process.on("exit", () => {
if (this.sessions.size > 0) {
console.error(`\u{1F9F9} [exit] Cleaning up ${this.sessions.size} active AI sessions...`);
for (const [, agent] of this.sessions.entries()) {
if (agent && typeof agent.cleanup === "function") {
try {
agent.cleanup();
} catch {
}
}
}
this.sessions.clear();
}
});
process.on("SIGINT", () => {
cleanupAndExit("SIGINT");
process.exit(0);
});
process.on("SIGTERM", () => {
cleanupAndExit("SIGTERM");
process.exit(0);
});
this.exitHandlerRegistered = true;
}
};
}
});
// src/github-comments.ts
init_logger();
import { v4 as uuidv4 } from "uuid";
// src/footer.ts
function generateFooter(options = {}) {
const { includeMetadata, includeSeparator = true } = options;
const parts = [];
if (includeSeparator) {
parts.push("---");
parts.push("");
}
parts.push(
"*Powered by [Visor](https://probelabs.com/visor) from [Probelabs](https://probelabs.com)*"
);
if (includeMetadata) {
const { lastUpdated, triggeredBy, commitSha } = includeMetadata;
const commitInfo = commitSha ? ` | Commit: ${commitSha.substring(0, 7)}` : "";
parts.push("");
parts.push(`*Last updated: ${lastUpdated} | Triggered by: ${triggeredBy}${commitInfo}*`);
}
parts.push("");
parts.push("\u{1F4A1} **TIP:** You can chat with Visor using `/visor ask <your question>`");
return parts.join("\n");
}
// src/github-comments.ts
var CommentManager = class {
octokit;
retryConfig;
constructor(octokit, retryConfig) {
this.octokit = octokit;
this.retryConfig = {
maxRetries: 3,
baseDelay: 1e3,
maxDelay: 1e4,
backoffFactor: 2,
...retryConfig
};
}
/**
* Find existing Visor comment by comment ID marker
*/
async findVisorComment(owner, repo, prNumber, commentId) {
try {
const comments = await this.octokit.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100
// GitHub default max
});
for (const comment of comments.data) {
if (comment.body && this.isVisorComment(comment.body, commentId)) {
return comment;
}
}
return null;
} catch (error) {
if (this.isRateLimitError(
error
)) {
await this.handleRateLimit(error);
return this.findVisorComment(owner, repo, prNumber, commentId);
}
throw error;
}
}
/**
* Update existing comment or create new one with collision detection
*/
async updateOrCreateComment(owner, repo, prNumber, content, options = {}) {
const {
commentId = this.generateCommentId(),
triggeredBy = "unknown",
allowConcurrentUpdates = false,
commitSha
} = options;
return this.withRetry(async () => {
const existingComment = await this.findVisorComment(owner, repo, prNumber, commentId);
const formattedContent = this.formatCommentWithMetadata(content, {
commentId,
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
triggeredBy,
commitSha
});
if (existingComment) {
if (!allowConcurrentUpdates) {
const currentComment = await this.octokit.rest.issues.getComment({
owner,
repo,
comment_id: existingComment.id
});
if (currentComment.data.updated_at !== existingComment.updated_at) {
throw new Error(
`Comment collision detected for comment ${commentId}. Another process may have updated it.`
);
}
}
const updatedComment = await this.octokit.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body: formattedContent
});
logger.info(
`\u2705 Successfully updated comment (ID: ${commentId}, GitHub ID: ${existingComment.id}) on PR #${prNumber} in ${owner}/${repo}`
);
return updatedComment.data;
} else {
const newComment = await this.octokit.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: formattedContent
});
logger.info(
`\u2705 Successfully created comment (ID: ${commentId}, GitHub ID: ${newComment.data.id}) on PR #${prNumber} in ${owner}/${repo}`
);
return newComment.data;
}
});
}
/**
* Format comment content with metadata markers
*/
formatCommentWithMetadata(content, metadata) {
const { commentId, lastUpdated, triggeredBy, commitSha } = metadata;
const footer = generateFooter({
includeMetadata: {
lastUpdated,
triggeredBy,
commitSha
}
});
return `<!-- visor-comment-id:${commentId} -->
${content}
${footer}
<!-- /visor-comment-id:${commentId} -->`;
}
/**
* Create collapsible sections for comment content
*/
createCollapsibleSection(title, content, isExpanded = false) {
const openAttribute = isExpanded ? " open" : "";
return `<details${openAttribute}>
<summary>${title}</summary>
${content}
</details>`;
}
/**
* Group review results by check type with collapsible sections
*/
formatGroupedResults(results, groupBy = "check") {
const grouped = this.groupResults(results, groupBy);
const sections = [];
for (const [groupKey, items] of Object.entries(grouped)) {
const totalScore = items.reduce((sum, item) => sum + (item.score || 0), 0) / items.length;
const totalIssues = items.reduce((sum, item) => sum + (item.issuesFound || 0), 0);
const emoji = this.getCheckTypeEmoji(groupKey);
const title = `${emoji} ${this.formatGroupTitle(groupKey, totalScore, totalIssues)}`;
const sectionContent = items.map((item) => item.content).join("\n\n");
sections.push(this.createCollapsibleSection(title, sectionContent, totalIssues > 0));
}
return sections.join("\n\n");
}
/**
* Generate unique comment ID
*/
generateCommentId() {
return uuidv4().substring(0, 8);
}
/**
* Check if comment is a Visor comment
*/
isVisorComment(body, commentId) {
if (commentId) {
if (body.includes(`visor-comment-id:${commentId} `) || body.includes(`visor-comment-id:${commentId} -->`)) {
return true;
}
if (commentId.startsWith("pr-review-") && body.includes("visor-review-")) {
return true;
}
return false;
}
return body.includes("visor-comment-id:") && body.includes("<!-- /visor-comment-id:") || body.includes("visor-review-");
}
/**
* Extract comment ID from comment body
*/
extractCommentId(body) {
const match = body.match(/visor-comment-id:([a-f0-9-]+)/);
return match ? match[1] : null;
}
/**
* Handle rate limiting with exponential backoff
*/
async handleRateLimit(error) {
const resetTime = error.response?.headers?.["x-ratelimit-reset"];
if (resetTime) {
const resetDate = new Date(parseInt(resetTime) * 1e3);
const waitTime = Math.max(resetDate.getTime() - Date.now(), this.retryConfig.baseDelay);
console.log(`Rate limit exceeded. Waiting ${Math.round(waitTime / 1e3)}s until reset...`);
await this.sleep(Math.min(waitTime, this.retryConfig.maxDelay));
} else {
await this.sleep(this.retryConfig.baseDelay);
}
}
/**
* Check if error is a rate limit error
*/
isRateLimitError(error) {
return error.status === 403 && (error.response?.data?.message?.includes("rate limit") ?? false);
}
/**
* Check if error should not be retried (auth errors, not found, etc.)
*/
isNonRetryableError(error) {
const nonRetryableStatuses = [401, 404, 422];
const status = error.status || error.response?.status;
if (status === 403) {
return !this.isRateLimitError(error);
}
return status !== void 0 && nonRetryableStatuses.includes(status);
}
/**
* Retry wrapper with exponential backoff
*/
async withRetry(operation) {
let lastError = new Error("Unknown error");
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === this.retryConfig.maxRetries) {
break;
}
if (this.isRateLimitError(
error
)) {
await this.handleRateLimit(error);
} else if (this.isNonRetryableError(error)) {
throw error;
} else {
const delay = Math.min(
this.retryConfig.baseDelay * Math.pow(this.retryConfig.backoffFactor, attempt),
this.retryConfig.maxDelay
);
await this.sleep(delay);
}
}
}
throw lastError;
}
/**
* Sleep utility
*/
sleep(ms) {
return new Promise((resolve4) => setTimeout(resolve4, ms));
}
/**
* Group results by specified criteria
*/
groupResults(results, groupBy) {
const grouped = {};
for (const result of results) {
const key = groupBy === "check" ? result.checkType : this.getSeverityGroup(result.score);
if (!grouped[key]) {
grouped[key] = [];
}
grouped[key].push(result);
}
return grouped;
}
/**
* Get severity group based on score
*/
getSeverityGroup(score) {
if (!score) return "Unknown";
if (score >= 90) return "Excellent";
if (score >= 75) return "Good";
if (score >= 50) return "Needs Improvement";
return "Critical Issues";
}
/**
* Get emoji for check type
*/
getCheckTypeEmoji(checkType) {
const emojiMap = {
performance: "\u{1F4C8}",
security: "\u{1F512}",
architecture: "\u{1F3D7}\uFE0F",
style: "\u{1F3A8}",
all: "\u{1F50D}",
Excellent: "\u2705",
Good: "\u{1F44D}",
"Needs Improvement": "\u26A0\uFE0F",
"Critical Issues": "\u{1F6A8}",
Unknown: "\u2753"
};
return emojiMap[checkType] || "\u{1F4DD}";
}
/**
* Format group title with score and issue count
*/
formatGroupTitle(groupKey, score, issuesFound) {
const formattedScore = Math.round(score);
return `${groupKey} Review (Score: ${formattedScore}/100)${issuesFound > 0 ? ` - ${issuesFound} issues found` : ""}`;
}
};
// src/ai-review-service.ts
init_session_registry();
init_logger();
init_tracer_init();
import { ProbeAgent } from "@probelabs/probe";
// src/utils/diff-processor.ts
import { extract } from "@probelabs/probe";
import * as path from "path";
async function processDiffWithOutline(diffContent) {
if (!diffContent || diffContent.trim().length === 0) {
return diffContent;
}
try {
const originalProbePath = process.env.PROBE_PATH;
const fs7 = __require("fs");
const possiblePaths = [
// Relative to current working directory (most common in production)
path.join(process.cwd(), "node_modules/@probelabs/probe/bin/probe-binary"),
// Relative to __dirname (for unbundled development)
path.join(__dirname, "../..", "node_modules/@probelabs/probe/bin/probe-binary"),
// Relative to dist directory (for bundled CLI)
path.join(__dirname, "node_modules/@probelabs/probe/bin/probe-binary")
];
let probeBinaryPath;
for (const candidatePath of possiblePaths) {
if (fs7.existsSync(candidatePath)) {
probeBinaryPath = candidatePath;
break;
}
}
if (!probeBinaryPath) {
if (process.env.DEBUG === "1" || process.env.VERBOSE === "1") {
console.error("Probe binary not found. Tried:", possiblePaths);
}
return diffContent;
}
process.env.PROBE_PATH = probeBinaryPath;
const extractPromise = extract({
content: diffContent,
format: "outline-diff",
allowTests: true
// Allow test files and test code blocks in extraction results
});
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Extract timeout after 30s")), 3e4);
});
const result = await Promise.race([extractPromise, timeoutPromise]);
if (originalProbePath !== void 0) {
process.env.PROBE_PATH = originalProbePath;
} else {
delete process.env.PROBE_PATH;
}
return typeof result === "string" ? result : JSON.stringify(result);
} catch (error) {
if (process.env.DEBUG === "1" || process.env.VERBOSE === "1") {
console.error("Failed to process diff with outline-diff format:", error);
}
return diffContent;
}
}
// src/ai-review-service.ts
function log(...args) {
logger.debug(args.join(" "));
}
var AIReviewService = class {
config;
sessionRegistry;
constructor(config = {}) {
this.config = {
timeout: 6e5,
// Increased timeout to 10 minutes for AI responses
...config
};
this.sessionRegistry = SessionRegistry.getInstance();
if (!this.config.apiKey) {
if (process.env.CLAUDE_CODE_API_KEY) {
this.config.apiKey = process.env.CLAUDE_CODE_API_KEY;
this.config.provider = "claude-code";
} else if (process.env.GOOGLE_API_KEY) {
this.config.apiKey = process.env.GOOGLE_API_KEY;
this.config.provider = "google";
} else if (process.env.ANTHROPIC_API_KEY) {
this.config.apiKey = process.env.ANTHROPIC_API_KEY;
this.config.provider = "anthropic";
} else if (process.env.OPENAI_API_KEY) {
this.config.apiKey = process.env.OPENAI_API_KEY;
this.config.provider = "openai";
} else if (
// Check for AWS Bedrock credentials
process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY || process.env.AWS_BEDROCK_API_KEY
) {
this.config.provider = "bedrock";
this.config.apiKey = "AWS_CREDENTIALS";
}
}
if (!this.config.model && process.env.MODEL_NAME) {
this.config.model = process.env.MODEL_NAME;
}
}
// NOTE: per request, no additional redaction/encryption helpers are used.
/**
* Execute AI review using probe agent
*/
async executeReview(prInfo, customPrompt, schema, checkName, sessionId) {
const startTime = Date.now();
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const prompt = await this.buildCustomPrompt(prInfo, customPrompt, schema);
log(`Executing AI review with ${this.config.provider} provider...`);
log(`\u{1F527} Debug: Raw schema parameter: ${JSON.stringify(schema)} (type: ${typeof schema})`);
log(`Schema type: ${schema || "none (no schema)"}`);
let debugInfo;
if (this.config.debug) {
debugInfo = {
prompt,
rawResponse: "",
provider: this.config.provider || "unknown",
model: this.config.model || "default",
apiKeySource: this.getApiKeySource(),
processingTime: 0,
promptLength: prompt.length,
responseLength: 0,
errors: [],
jsonParseSuccess: false,
timestamp,
schemaName: typeof schema === "object" ? "custom" : schema,
schema: void 0
// Will be populated when schema is loaded
};
}
if (this.config.model === "mock" || this.config.provider === "mock") {
log("\u{1F3AD} Using mock AI model/provider for testing - skipping API key validation");
} else {
if (!this.config.apiKey) {
const errorMessage = "No API key configured. Please set GOOGLE_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY environment variable, or configure AWS credentials for Bedrock (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY).";
if (debugInfo) {
debugInfo.errors = [errorMessage];
debugInfo.processingTime = Date.now() - startTime;
debugInfo.rawResponse = "API call not attempted - no API key configured";
return {
issues: [
{
file: "system",
line: 0,
ruleId: "system/api-key-missing",
message: errorMessage,
severity: "error",
category: "logic"
}
],
debug: debugInfo
};
}
throw new Error(errorMessage);
}
}
try {
const { response, effectiveSchema } = await this.callProbeAgent(
prompt,
schema,
debugInfo,
checkName,
sessionId
);
const processingTime = Date.now() - startTime;
if (debugInfo) {
debugInfo.rawResponse = response;
debugInfo.responseLength = response.length;
debugInfo.processingTime = processingTime;
}
const result = this.parseAIResponse(response, debugInfo, effectiveSchema);
if (debugInfo) {
result.debug = debugInfo;
}
return result;
} catch (error) {
if (debugInfo) {
debugInfo.errors = [error instanceof Error ? error.message : String(error)];
debugInfo.processingTime = Date.now() - startTime;
return {
issues: [
{
file: "system",
line: 0,
ruleId: "system/ai-execution-error",
message: error instanceof Error ? error.message : String(error),
severity: "error",
category: "logic"
}
],
debug: debugInfo
};
}
throw error;
}
}
/**
* Execute AI review using session reuse - reuses an existing ProbeAgent session
* @param sessionMode - 'clone' (default) clones history, 'append' shares history
*/
async executeReviewWithSessionReuse(prInfo, customPrompt, parentSessionId, schema, checkName, sessionMode = "clone") {
const startTime = Date.now();
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const existingAgent = this.sessionRegistry.getSession(parentSessionId);
if (!existingAgent) {
throw new Error(
`Session not found for reuse: ${parentSessionId}. Ensure the parent check completed successfully.`
);
}
const prompt = await this.buildCustomPrompt(prInfo, customPrompt, schema, {
skipPRContext: true
});
let agentToUse;
let currentSessionId;
if (sessionMode === "clone") {
currentSessionId = `${checkName}-session-${Date.now()}`;
log(
`\u{1F4CB} Cloning AI session ${parentSessionId} \u2192 ${currentSessionId} for ${checkName} check...`
);
const clonedAgent = await this.sessionRegistry.cloneSession(
parentSessionId,
currentSessionId,
checkName
// Pass checkName for tracing
);
if (!clonedAgent) {
throw new Error(`Failed to clone session ${parentSessionId}`);
}
agentToUse = clonedAgent;
} else {
log(`\u{1F504} Appending to AI session ${parentSessionId} (shared history)...`);
agentToUse = existingAgent;
currentSessionId = parentSessionId;
}
log(`\u{1F527} Debug: Raw schema parameter: ${JSON.stringify(schema)} (type: ${typeof schema})`);
log(`\u{1F4CB} Schema for this check: ${schema || "none (no schema)"}`);
if (sessionMode === "clone") {
log(`\u2705 Cloned agent will use NEW schema (${schema}) - parent schema does not persist`);
log(`\u{1F504} Clone operation ensures fresh agent with copied history but new configuration`);
} else {
log(`\u{1F504} Append mode - using existing agent instance with shared history and configuration`);
}
let debugInfo;
if (this.config.debug) {
debugInfo = {
prompt,
rawResponse: "",
provider: this.config.provider || "unknown",
model: this.config.model || "default",
apiKeySource: this.getApiKeySource(),
processingTime: 0,
promptLength: prompt.length,
responseLength: 0,
errors: [],
jsonParseSuccess: false,
timestamp,
schemaName: typeof schema === "object" ? "custom" : schema,
schema: void 0
// Will be populated when schema is loaded
};
}
try {
const { response, effectiveSchema } = await this.callProbeAgentWithExistingSession(
agentToUse,
prompt,
schema,
debugInfo,
checkName
);
const processingTime = Date.now() - startTime;
if (debugInfo) {
debugInfo.rawResponse = response;
debugInfo.responseLength = response.length;
debugInfo.processingTime = processingTime;
}
const result = this.parseAIResponse(response, debugInfo, effectiveSchema);
if (debugInfo) {
result.debug = debugInfo;
}
if (sessionMode === "clone" && currentSessionId !== parentSessionId) {
result.sessionId = currentSessionId;
}
return result;
} catch (error) {
if (debugInfo) {
debugInfo.errors = [error instanceof Error ? error.message : String(error)];
debugInfo.processingTime = Date.now() - startTime;
return {
issues: [
{
file: "system",
line: 0,
ruleId: "system/ai-session-reuse-error",
message: error instanceof Error ? error.message : String(error),
severity: "error",
category: "logic"
}
],
debug: debugInfo
};
}
throw error;
}
}
/**
* Register a new AI session in the session registry
*/
registerSession(sessionId, agent) {
this.sessionRegistry.registerSession(sessionId, agent);
}
/**
* Cleanup a session from the registry
*/
cleanupSession(sessionId) {
this.sessionRegistry.unregisterSession(sessionId);
}
/**
* Build a custom prompt for AI review with XML-formatted data
*/
async buildCustomPrompt(prInfo, customInstructions, schema, options) {
const skipPRContext = options?.skipPRContext === true;
const isCodeReviewSchema = schema === "code-review";
const prContext = skipPRContext ? "" : await this.formatPRContext(prInfo, isCodeReviewSchema);
const isIssue = prInfo.isIssue === true;
if (isIssue) {
if (skipPRContext) {
return `<instructions>
${customInstructions}
</instructions>`;
}
return `<review_request>
<instructions>
${customInstructions}
</instructions>
<context>
${prContext}
</context>
<rules>
<rule>Understand the issue context and requirements from the XML data structure</rule>
<rule>Provide helpful, actionable guidance based on the issue details</rule>
<rule>Be constructive and supportive in your analysis</rule>
<rule>Consider project conventions and patterns when making recommendations</rule>
<rule>Suggest practical solutions or next steps that address the specific concern</rule>
<rule>Focus on addressing the specific concern raised in the issue</rule>
<rule>Reference relevant XML elements like metadata, description, labels, assignees when providing context</rule>
</rules>
</review_request>`;
}
if (isCodeReviewSchema) {
const analysisType = prInfo.isIncremental ? "INCREMENTAL" : "FULL";
if (skipPRContext) {
return `<instructions>
${customInstructions}
</instructions>
<reminder>
<rule>The code context and diff were provided in the previous message</rule>
<rule>Focus on the new analysis instructions above</rule>
<rule>Only analyze code that appears with + (additions) or - (deletions) in the diff sections</rule>
<rule>STRICT OUTPUT POLICY: Report only actual problems, risks, or deficiencies</rule>
<rule>SEVERITY ASSIGNMENT: Assign severity ONLY to problems introduced or left unresolved by this change</rule>
</reminder>`;
}
return `<review_request>
<analysis_type>${analysisType}</analysis_type>
<analysis_focus>
${analysisType === "INCREMENTAL" ? "You are analyzing a NEW COMMIT added to an existing PR. Focus on the changes in the commit_diff section for this specific commit." : "You are analyzing the COMPLETE PR. Review all changes in the full_diff section."}
</analysis_focus>
<instructions>
${customInstructions}
</instructions>
<context>
${prContext}
</context>
<rules>
<rule>Only analyze code that appears with + (additions) or - (deletions) in the diff sections</rule>
<rule>Ignore unchanged code unless directly relevant to understanding a change</rule>
<rule>Line numbers in your response should match actual file line numbers from the diff</rule>
<rule>Focus on real issues, not nitpicks or cosmetic concerns</rule>
<rule>Provide actionable, specific feedback with clear remediation steps</rule>
<rule>For INCREMENTAL analysis, ONLY review changes in commit_diff section</rule>
<rule>For FULL analysis, review all changes in full_diff section</rule>
<rule>Reference specific XML elements like files_summary, metadata when providing context</rule>
<rule>STRICT OUTPUT POLICY: Report only actual problems, risks, or deficiencies. Do not write praise, congratulations, or celebratory text. Do not create issues that merely restate improvements or say "no action needed".</rule>
<rule>SEVERITY ASSIGNMENT: Assign severity ONLY to problems introduced or left unresolved by this change (critical/error/warning/info as appropriate). Do NOT create issue entries solely to acknowledge improvements; if no problems exist, return zero issues.</rule>
</rules>
</review_request>`;
}
if (skipPRContext) {
return `<instructions>
${customInstructions}
</instructions>`;
}
return `<instructions>
${customInstructions}
</instructions>
<context>
${prContext}
</context>`;
}
// REMOVED: Built-in prompts - only use custom prompts from .visor.yaml
// REMOVED: getFocusInstructions - only use custom prompts from .visor.yaml
/**
* Format PR or Issue context for the AI using XML structure
*/
async formatPRContext(prInfo, isCodeReviewSchema) {
const prContextInfo = prInfo;
const isIssue = prContextInfo.isIssue === true;
const isPRContext = prContextInfo.isPRContext === true;
const includeCodeContext = isPRContext || prContextInfo.includeCodeContext !== false;
if (isPRContext) {
log("\u{1F50D} Including full code diffs in AI context (PR mode)");
} else if (!includeCodeContext) {
log("\u{1F4CA} Including only file summary in AI context (no diffs)");
} else {
log("\u{1F50D} Including code diffs in AI context");
}
if (isIssue) {
let context3 = `<issue>
<!-- Core issue metadata including identification, status, and timeline information -->
<metadata>
<number>${prInfo.number}</number>
<title>${this.escapeXml(prInfo.title)}</title>
<author>${prInfo.author}</author>
<state>${prInfo.eventContext?.issue?.state || "open"}</state>
<created_at>${prInfo.eventContext?.issue?.created_at || ""}</created_at>
<updated_at>${prInfo.eventContext?.issue?.updated_at || ""}</updated_at>
<comments_count>${prInfo.eventContext?.issue?.comments || 0}</comments_count>
</metadata>`;
if (prInfo.body) {
context3 += `
<!-- Full issue description and body text provided by the issue author -->
<description>
${this.escapeXml(prInfo.body)}
</description>`;
}
const eventContext = prInfo;
const labels = eventContext.eventContext?.issue?.labels;
if (labels && labels.length > 0) {
context3 += `
<!-- Applied labels for issue categorization and organization -->
<labels>`;
labels.forEach((label) => {
const labelName = typeof label === "string" ? label : label.name || "unknown";
context3 += `
<label>${this.escapeXml(labelName)}</label>`;
});
context3 += `
</labels>`;
}
const assignees = prInfo.eventContext?.issue?.assignees;
if (assignees && assignees.length > 0) {
context3 += `
<!-- Users assigned to work on this issue -->
<assignees>`;
assignees.forEach((assignee) => {
const assigneeName = typeof assignee === "string" ? assignee : assignee.login || "unknown";
context3 += `
<assignee>${this.escapeXml(assigneeName)}</assignee>`;
});
context3 += `
</assignees>`;
}
const milestone = prInfo.eventContext?.issue?.milestone;
if (milestone) {
context3 += `
<!-- Associated project milestone information -->
<milestone>
<title>${this.escapeXml(milestone.title || "")}</title>
<state>${milestone.state || "open"}</state>
<due_on>${milestone.due_on || ""}</due_on>
</milestone>`;
}
const triggeringComment2 = prInfo.eventContext?.comment;
if (triggeringComment2) {
context3 += `
<!-- The comment that triggered this analysis -->
<triggering_comment>
<author>${this.escapeXml(triggeringComment2.user?.login || "unknown")}</author>
<created_at>${triggeringComment2.created_at || ""}</created_at>
<body>${this.escapeXml(triggeringComment2.body || "")}</body>
</triggering_comment>`;
}
const issueComments = prInfo.comments;
if (issueComments && issueComments.length > 0) {
let historicalComments = triggeringComment2 ? issueComments.filter((c) => c.id !== triggeringComment2.id) : issueComments;
if (isCodeReviewSchema) {
historicalComments = historicalComments.filter(
(c) => !c.body || !c.body.includes("visor-comment-id:pr-review-")
);
}
if (historicalComments.length > 0) {
context3 += `
<!-- Previous comments in chronological order (excluding triggering comment) -->
<comment_history>`;
historicalComments.forEach((comment) => {
context3 += `
<comment>
<author>${this.escapeXml(comment.author || "unknown")}</author>
<created_at>${comment.createdAt || ""}</created_at>
<body>${this.escapeXml(comment.body || "")}</body>
</comment>`;
});
context3 += `
</comment_history>`;
}
}
context3 += `
</issue>`;
return context3;
}
let context2 = `<pull_request>
<!-- Core pull request metadata including identification, branches, and change statistics -->
<metadata>
<number>${prInfo.number}</number>
<title>${this.escapeXml(prInfo.title)}</title>
<author>${prInfo.author}</author>
<base_branch>${prInfo.base}</base_branch>
<target_branch>${prInfo.head}</target_branch>
<total_additions>${prInfo.totalAdditions}</total_additions>
<total_deletions>${prInfo.totalDeletions}</total_deletions>
<files_changed_count>${prInfo.files.length}</files_changed_count>
</metadata>`;
if (prInfo.body) {
context2 += `
<!-- Full pull request description provided by the author -->
<description>
${this.escapeXml(prInfo.body)}
</description>`;
}
if (includeCodeContext) {
if (prInfo.fullDiff) {
const processedFullDiff = await processDiffWithOutline(prInfo.fullDiff);
context2 += `
<!-- Complete unified diff showing all changes in the pull request (processed with outline-diff) -->
<full_diff>
${this.escapeXml(processedFullDiff)}
</full_diff>`;
}
if (prInfo.isIncremental) {
if (prInfo.commitDiff && prInfo.commitDiff.length > 0) {
const processedCommitDiff = await processDiffWithOutline(prInfo.commitDiff);
context2 += `
<!-- Diff of only the latest commit for incremental analysis (processed with outline-diff) -->
<commit_diff>
${this.escapeXml(processedCommitDiff)}
</commit_diff>`;
} else {
const processedFallbackDiff = prInfo.fullDiff ? await processDiffWithOutline(prInfo.fullDiff) : "";
context2 += `
<!-- Commit diff could not be retrieved - falling back to full diff analysis (processed with outline-diff) -->
<commit_diff>
${this.escapeXml(processedFallbackDiff)}
</commit_diff>`;
}
}
} else {
context2 += `
<!-- Code diffs excluded to reduce token usage (no code-review schema detected or disabled by flag) -->`;
}
if (prInfo.files.length > 0) {
context2 += `
<!-- Summary of all files changed with statistics -->
<files_summary>`;
prInfo.files.forEach((file) => {
context2 += `
<file>
<filename>${this.escapeXml(file.filename)}</filename>
<status>${file.status}</status>
<additions>${file.additions}</additions>
<deletions>${file.deletions}</deletions>
</file>`;
});
context2 += `
</files_summary>`;
}
const triggeringComment = prInfo.eventContext?.comment;
if (triggeringComment) {
context2 += `
<!-- The comment that triggered this analysis -->
<triggering_comment>
<author>${this.escapeXml(triggeringComment.user?.login || "unknown")}</author>
<created_at>${triggeringComment.created_at || ""}</created_at>
<body>${this.escapeXml(triggeringComment.body || "")}</body>
</triggering_comment>`;
}
const prComments = prInfo.comments;
if (prComments && prComments.length > 0) {
let historicalComments = triggeringComment ? prComments.filter((c) => c.id !== triggeringComment.id) : prComments;
if (isCodeReviewSchema) {
historicalComments = historicalComments.filter(
(c) => !c.body || !c.body.includes("visor-comment-id:pr-review-")
);
}
if (historicalComments.length > 0) {
context2 += `
<!-- Previous PR comments in chronological order (excluding triggering comment) -->
<comment_history>`;
historicalComments.forEach((comment) => {
context2 += `
<comment>
<author>${this.escapeXml(comment.author || "unknown")}</author>
<created_at>${comment.createdAt || ""}</created_at>
<body>${this.escapeXml(comment.body || "")}</body>
</comment>`;
});
context2 += `
</comment_history>`;
}
}
context2 += `
</pull_request>`;
return context2;
}
/**
* No longer escaping XML - returning text as-is
*/
escapeXml(text) {
return text;
}
/**
* Call ProbeAgent with an existing session
*/
async callProbeAgentWithExistingSession(agent, prompt, schema, debugInfo, _checkName) {
if (this.config.model === "mock" || this.config.provider === "mock") {
log("\u{1F3AD} Using mock AI model/provider for testing (session reuse)");
const response = await this.generateMockResponse(prompt);
return { response, effectiveSchema: typeof schema === "object" ? "custom" : schema };
}
log("\u{1F504} Reusing existing ProbeAgent session for AI review...");
log(`\u{1F4DD} Prompt length: ${prompt.length} characters`);
log(`\u2699\uFE0F Model: ${this.config.model || "default"}, Provider: ${this.config.provider || "auto"}`);
try {
log("\u{1F680} Calling existing ProbeAgent with answer()...");
let schemaString = void 0;
let effectiveSchema = typeof schema === "object" ? "custom" : schema;
if (schema && schema !== "plain") {
try {
schemaString = await this.loadSchemaContent(schema);
log(`\u{1F4CB} Loaded schema content for: ${schema}`);
log(`\u{1F4C4} Raw schema JSON:
${schemaString}`);
} catch (error) {
log(`\u26A0\uFE0F Failed to load schema ${schema}, proceeding without schema:`, error);
schemaString = void 0;
effectiveSchema = void 0;
if (debugInfo && debugInfo.errors) {
debugInfo.errors.push(`Failed to load schema: ${error}`);
}
}
} else if (schema === "plain") {
log(`\u{1F4CB} Using plain schema - no JSON validation will be applied`);
}
const schemaOptions = schemaString ? { schema: schemaString } : void 0;
if (debugInfo && schemaOptions) {
debugInfo.schema = JSON.stringify(schemaOptions, null, 2);
}
if (schemaOptions) {
log(`\u{1F3AF} Schema options passed to ProbeAgent.answer() (session reuse):`);
log(JSON.stringify(schemaOptions, null, 2));
}
if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
try {
const fs7 = __require("fs");
const path9 = __require("path");
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
const provider = this.config.provider || "auto";
const model = this.config.model || "default";
let conversationHistory = [];
try {
const agentAny2 = agent;
if (agentAny2.history) {
conversationHistory = agentAny2.history;
} else if (agentAny2.messages) {
conversationHistory = agentAny2.messages;
} else if (agentAny2._messages) {
conversationHistory = agentAny2._messages;
}
} catch {
}
const debugData = {
timestamp,
checkName: _checkName || "unknown",
provider,
model,
schema: effectiveSchema,
schemaOptions: schemaOptions || "none",
sessionInfo: {
isSessionReuse: true,
historyMessageCount: conversationHistory.length
},
currentPromptLength: prompt.length,
currentPrompt: prompt,
conversationHistory
};
const debugJson = JSON.stringify(debugData, null, 2);
let readableVersion = `=============================================================
`;
readableVersion += `VISOR DEBUG REPORT - SESSION REUSE
`;
readableVersion += `=============================================================
`;
readableVersion += `Timestamp: ${timestamp}
`;
readableVersion += `Check Name: ${_checkName || "unknown"}
`;
readableVersion += `Provider: ${provider}
`;
readableVersion += `Model: ${model}
`;
readableVersion += `Schema: ${effectiveSchema}
`;
readableVersion += `Schema Options: ${schemaOptions ? "provided" : "none"}
`;
readableVersion += `History Messages: ${conversationHistory.length}
`;
readableVersion += `=============================================================
`;
if (schemaOptions) {
readableVersion += `
${"=".repeat(60)}
`;
readableVersion += `SCHEMA CONFIGURATION
`;
readableVersion += `${"=".repeat(60)}
`;
readableVersion += JSON.stringify(schemaOptions, null, 2);
readableVersion += `
`;
}
if (conversationHistory.length > 0) {
readableVersion += `
${"=".repeat(60)}
`;
readableVersion += `CONVERSATION HISTORY (${conversationHistory.length} messages)
`;
readableVersion += `${"=".repeat(60)}
`;
conversationHistory.forEach((msg, index) => {
readableVersion += `
${"-".repeat(60)}
`;
readableVersion += `MESSAGE #${index + 1}
`;
readableVersion += `Role: ${msg.role || "unknown"}
`;
if (msg.content) {
const contentStr = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content, null, 2);
readableVersion += `Length: ${contentStr.length} characters
`;
readableVersion += `${"-".repeat(60)}
`;
readableVersion += `${contentStr}
`;
}
});
}
readableVersion += `
${"=".repeat(60)}
`;
readableVersion += `CURRENT PROMPT (NEW MESSAGE)
`;
readableVersion += `${"=".repeat(60)}
`;
readableVersion += `Length: ${prompt.length} characters
`;
readableVersion += `${"-".repeat(60)}
`;
readableVersion += `${prompt}
`;
readableVersion += `
${"=".repeat(60)}
`;
readableVersion += `END OF DEBUG REPORT
`;
readableVersion += `${"=".repeat(60)}
`;
const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path9.join(process.cwd(), "debug-artifacts");
if (!fs7.existsSync(debugArtifactsDir)) {
fs7.mkdirSync(debugArtifactsDir, { recursive: true });
}
const debugFile = path9.join(
debugArtifactsDir,
`prompt-${_checkName || "unknown"}-${timestamp}.json`
);
fs7.writeFileSync(debugFile, debugJson, "utf-8");
const readableFile = path9.join(
debugArtifactsDir,
`prompt-${_checkName || "unknown"}-${timestamp}.txt`
);
fs7.writeFileSync(readableFile, readableVersion, "utf-8");
log(`
\u{1F4BE} Full debug info saved to:`);
log(` JSON: ${debugFile}`);
log(` TXT: ${readableFile}`);
log(` - Includes: full conversation history, schema, current prompt`);
} catch (error) {
log(`\u26A0\uFE0F Could not save debug file: ${error}`);
}
}
const agentAny = agent;
let response;
if (agentAny.tracer && typeof agentAny.tracer.withSpan === "function") {
response = await agentAny.tracer.withSpan(
"visor.ai_check_reuse",
async () => {
return await agent.answer(prompt, void 0, schemaOptions);
},
{
"check.name": _checkName || "unknown",
"check.mode": "session_reuse",
"prompt.length": prompt.length,
"schema.type": effectiveSchema || "none"
}
);
} else {
response = await agent.answer(prompt, void 0, schemaOptions);
}
log("\u2705 ProbeAgent session reuse completed successfully");
log(`\u{1F4E4} Response length: ${response.length} characters`);
if (process.env.VISOR_DEBUG_AI_SESSIONS === "true") {
try {
const fs7 = __require("fs");
const path9 = __require("path");
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
const agentAny2 = agent;
let fullHistory = [];
if (agentAny2.history) {
fullHistory = agentAny2.history;
} else if (agentAny2.messages) {
fullHistory = agentAny2.messages;
} else if (agentAny2._messages) {
fullHistory = agentAny2._messages;
}
const debugArtifactsDir = process.env.VISOR_DEBUG_ARTIFACTS || path9.join(process.cwd(), "debug-artifacts");
const sessionBase = path9.join(
debugArtifactsDir,
`session-${_checkName || "unknown"}-${timestamp}`
);
const sessionData = {
timestamp,
checkName: _checkName || "unknown",
provider: this.config.provider || "auto",
model: this.config.model || "default",
schema: effectiveSchema,
totalMessages: fullHistory.length
};
fs7.writeFileSync(sessionBase + ".json", JSON.stringify(sessionData, null, 2), "utf-8");
let readable = `=============================================================
`;
readable += `COMPLETE AI SESSION HISTORY (AFTER RESPONSE)
`;
readable += `=============================================================
`;
readable += `Timestamp: ${timestamp}
`;
readable += `Check: ${_checkName || "unknown"}
`;
readable += `Total Messages: ${fullHistory.length}
`;
readable += `=============================================================
`;
fullHistory.forEach((msg, idx) => {