Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
node-version: '20'
cache: 'npm'

- name: Install dependencies
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Typecheck

on:
push:
branches: [main, release-docs]
pull_request:
branches: [main, release-docs]

jobs:
typecheck:
name: tsc --noEmit
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run typecheck
run: npm run typecheck
9 changes: 9 additions & 0 deletions .vectorlint.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# VectorLint Configuration
# Global settings
RulesPath=
Concurrency=4
DefaultSeverity=warning

# Default rules for all markdown files
[**/*.md]
RunRules=VectorLint
24 changes: 0 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,30 +145,6 @@ Notes:
- Prompts and outputs are recorded when Langfuse observability is enabled.
- Do not send secrets, credentials, or PII unless your policy explicitly allows observability tooling to access that data.

## Agent Mode

Agent mode is for reviews that need context from several files such as
documentation drift and cross-file accuracy.

Run VectorLint in autonomous agent mode:

```bash
vectorlint doc.md --mode agent
```

For machine-parseable output:

```bash
vectorlint doc.md --mode agent --output json
```

To suppress interactive progress in line output:

```bash
vectorlint doc.md --mode agent --print
```


## Contributing

We welcome your contributions! Whether it's adding new rules, fixing bugs, or improving documentation, please check out our [Contributing Guidelines](.github/CONTRIBUTING.md) to get started.
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import unicorn from "eslint-plugin-unicorn";

export default defineConfig([
// Ignored + housekeeping
{ ignores: ["node_modules", "coverage", "dist", "build"] },
{ ignores: ["node_modules", "coverage", "dist", "build", "docs/research/**"] },
{ linterOptions: { reportUnusedDisableDirectives: true } },

// Make resolver settings global
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsup",
"typecheck": "tsc --noEmit",
"start": "node dist/index.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest",
"test:run": "vitest run",
"test:ci": "vitest run --coverage"
"test:ci": "vitest run --coverage",
"verify": "npm run typecheck && npm run lint && npm run test:run"
},
"bin": {
"vectorlint": "./dist/index.js",
Expand Down
37 changes: 18 additions & 19 deletions src/agent/executor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readdir, readFile } from 'fs/promises';
import * as os from 'os';
import fg from 'fast-glob';
import { buildCheckLLMSchema, isJudgeResult, type PromptEvaluationResult } from '../prompts/schema';
import { buildCheckLLMSchema, isJudgeResult, type GateChecks, type PromptEvaluationResult } from '../prompts/schema';
import type { PromptFile } from '../prompts/prompt-loader';
import { Type, Severity } from '../evaluators/types';
import { computeFilterDecision } from '../evaluators/violation-filter';
Expand All @@ -14,6 +14,7 @@ import { createReviewSessionStore } from './review-session-store';
import { buildAgentSystemPrompt } from './prompt-builder';
import { AgentToolError } from '../errors';
import { ScanPathResolver } from '../boundaries/scan-path-resolver';
import type { FilePatternConfig } from '../boundaries/file-section-parser';
import {
createAgentTools,
listAvailableTools,
Expand Down Expand Up @@ -53,7 +54,7 @@ export interface RunAgentExecutorParams {
prompts: PromptFile[];
provider: LLMProvider;
workspaceRoot: string;
scanPaths: Array<{ pattern: string; runRules: string[]; overrides: Record<string, string> }>;
scanPaths: FilePatternConfig[];
outputFormat: OutputFormat;
printMode: boolean;
sessionHomeDir?: string;
Expand Down Expand Up @@ -174,11 +175,10 @@ function findingsFromEvents(events: SessionEvent[]): AgentFinding[] {
return findings;
}

// Build the concrete matched file-to-rule pairs that the agent should review for this run.
function buildFileRuleMatches(
relativeTargets: string[],
prompts: PromptFile[],
scanPaths: Array<{ pattern: string; runRules: string[]; overrides: Record<string, string> }>
scanPaths: FilePatternConfig[]
): Array<{ file: string; ruleSource: string }> {
const resolver = new ScanPathResolver();
const availablePacks = Array.from(new Set(prompts.map((p) => p.pack).filter((p): p is string => !!p)));
Expand Down Expand Up @@ -301,12 +301,13 @@ function resolveVisibleToolContext(params: {
}
const resolvedPath = tryResolveRelativePath(workspaceRoot, parsed.data.path);
const path = resolvedPath ?? parsed.data.path;
const progressFile = resolvedPath && targetFiles.has(resolvedPath)
? resolvedPath
: (currentProgressFile ?? defaultProgressFile);
return {
toolName: 'read_file',
path,
progressFile: resolvedPath && targetFiles.has(resolvedPath)
? resolvedPath
: (currentProgressFile ?? defaultProgressFile),
...(progressFile ? { progressFile } : {}),
signature: `read_file:${path}`,
};
}
Expand All @@ -317,10 +318,11 @@ function resolveVisibleToolContext(params: {
}
const resolvedPath = tryResolveRelativePath(workspaceRoot, parsed.data.path);
const path = resolvedPath ?? parsed.data.path;
const progressFile = currentProgressFile ?? defaultProgressFile;
return {
toolName: 'list_directory',
path,
progressFile: currentProgressFile ?? defaultProgressFile,
...(progressFile ? { progressFile } : {}),
signature: `list_directory:${path}`,
};
}
Expand All @@ -333,14 +335,15 @@ function resolveVisibleToolContext(params: {
const resolvedPath = tryResolveRelativePath(workspaceRoot, parsed.data.file);
const path = resolvedPath ?? parsed.data.file;
const ruleText = parsed.data.reviewInstruction?.trim() || prompt?.body || '';
const progressFile = resolvedPath && targetFiles.has(resolvedPath)
? resolvedPath
: (currentProgressFile ?? defaultProgressFile);
return {
toolName: 'lint',
path,
ruleName: String(prompt?.meta.name || prompt?.meta.id || 'Rule'),
ruleText,
progressFile: resolvedPath && targetFiles.has(resolvedPath)
? resolvedPath
: (currentProgressFile ?? defaultProgressFile),
...(progressFile ? { progressFile } : {}),
signature: `lint:${path}:${normalizeRuleSource(parsed.data.ruleSource)}:${ruleText}`,
};
}
Expand Down Expand Up @@ -390,11 +393,7 @@ type FindingLikeViolation = {
suggestion?: string;
fix?: string;
confidence?: number;
checks?: {
plausible_non_violation?: boolean;
context_supports_violation?: boolean;
rule_supports_claim?: boolean;
};
checks?: GateChecks;
};

async function appendInlineFinding(params: {
Expand Down Expand Up @@ -516,8 +515,8 @@ export async function runAgentExecutor(params: RunAgentExecutorParams): Promise<
workspaceRoot,
promptBySource,
targetFiles,
currentProgressFile,
defaultProgressFile: relativeTargets[0],
...(currentProgressFile ? { currentProgressFile } : {}),
...(relativeTargets[0] ? { defaultProgressFile: relativeTargets[0] } : {}),
});

if (visibleToolContext?.progressFile) {
Expand Down Expand Up @@ -798,7 +797,7 @@ export async function runAgentExecutor(params: RunAgentExecutorParams): Promise<
workspaceRoot,
fileRuleMatches,
availableTools,
userInstructions,
...(userInstructions ? { userInstructions } : {}),
}),
prompt: [
`Workspace root: ${workspaceRoot}`,
Expand Down
5 changes: 3 additions & 2 deletions src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export function registerMainCommand(program: Command): void {
.option('--show-prompt-trunc', 'Print truncated prompt/content previews (500 chars)')
.option('--debug-json', 'Write debug JSON artifacts (raw model output + filter decisions)')
.option('--output <format>', `Output format: ${OUTPUT_FORMATS.join(', ')}`, OUTPUT_FORMATS[0])
.option('--mode <mode>', 'Execution mode: standard (default) or agent', DEFAULT_REVIEW_MODE)
.option('-p, --print', 'Suppress interactive progress output in agent mode')
.option('--mode <mode>', 'Execution mode: standard (default).', DEFAULT_REVIEW_MODE)
.option('-p, --print', 'Suppress interactive progress output')
.option('--config <path>', `Path to custom ${DEFAULT_CONFIG_FILENAME} config file`)
.argument('[paths...]', 'files or directories to check (required)')
.action(async (paths: string[] = []) => {
Expand Down Expand Up @@ -217,6 +217,7 @@ export function registerMainCommand(program: Command): void {
...(searchProvider ? { searchProvider } : {}),
concurrency: config.concurrency,
verbose: cliOptions.verbose,
logger: runtimeLogger,
debugJson: cliOptions.debugJson,
outputFormat: cliOptions.output,
mode: cliOptions.mode,
Expand Down
11 changes: 4 additions & 7 deletions src/cli/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { handleUnknownError, MissingDependencyError } from '../errors/index';
import { createEvaluator } from '../evaluators/index';
import { Type, Severity } from '../evaluators/types';
import { USER_INSTRUCTION_FILENAME } from '../config/constants';
import { AGENT_REVIEW_MODE, DEFAULT_REVIEW_MODE, OutputFormat } from './types';
import { OutputFormat } from './types';
import { runAgentExecutor, type AgentExecutorResult, type AgentFinding } from '../agent/executor';
import { AgentProgressReporter, shouldEmitAgentProgress } from '../agent/progress';
import type {
Expand Down Expand Up @@ -1168,6 +1168,7 @@ async function buildAgentRuleScores(
return results;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function evaluateFilesInAgentMode(
targets: string[],
options: EvaluationOptions,
Expand All @@ -1194,7 +1195,7 @@ async function evaluateFilesInAgentMode(
progressReporter,
maxParallelToolCalls: 3,
maxRetries: options.agentMaxRetries ?? 10,
userInstructions: options.userInstructionContent,
...(options.userInstructionContent ? { userInstructions: options.userInstructionContent } : {}),
});

let totalErrors = 0;
Expand Down Expand Up @@ -1301,7 +1302,7 @@ export async function evaluateFiles(
targets: string[],
options: EvaluationOptions
): Promise<EvaluationResult> {
const { outputFormat = OutputFormat.Line, mode = DEFAULT_REVIEW_MODE } = options;
const { outputFormat = OutputFormat.Line } = options;

let hadOperationalErrors = false;
let hadSeverityErrors = false;
Expand All @@ -1321,10 +1322,6 @@ export async function evaluateFiles(
jsonFormatter = new ValeJsonFormatter();
}

if (mode === AGENT_REVIEW_MODE) {
return evaluateFilesInAgentMode(targets, options, outputFormat, jsonFormatter);
}

for (const file of targets) {
try {
totalFiles += 1;
Expand Down
2 changes: 2 additions & 0 deletions src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { RdJsonFormatter } from '../output/rdjson-formatter';
import type { PromptEvaluationResult, JudgeResult } from '../prompts/schema';
import { Severity } from '../evaluators/types';
import type { TokenUsageStats, PricingConfig } from '../providers/token-usage';
import type { Logger } from '../logging/logger';

export enum OutputFormat {
Line = "line",
Expand Down Expand Up @@ -48,6 +49,7 @@ export interface EvaluationOptions {
agentMaxRetries?: number;
pricing?: PricingConfig;
userInstructionContent?: string;
logger?: Logger;
}

export interface EvaluationResult {
Expand Down
1 change: 0 additions & 1 deletion src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export class ProcessingError extends VectorlintError {
}
}

// Agent tool/runtime error for agent-mode operational failures
export class AgentToolError extends VectorlintError {
constructor(message: string, code = 'AGENT_TOOL_ERROR') {
super(message, code);
Expand Down
4 changes: 2 additions & 2 deletions src/evaluators/violation-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export function computeFilterDecision(v: GateViolationLike): FilterDecision {

const hasFix = (v.fix ?? "").trim() !== "";

const hasConfidence = typeof v.confidence === "number";
const passesConfidence = hasConfidence && v.confidence >= confidenceThreshold;
const confidence = typeof v.confidence === "number" ? v.confidence : undefined;
const passesConfidence = confidence !== undefined && confidence >= confidenceThreshold;
if (!passesConfidence) reasons.push(`confidence<${confidenceThreshold}`);

const surface =
Expand Down
2 changes: 1 addition & 1 deletion src/observability/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export function createObservability(env: EnvConfig, logger?: Logger): AIObservab
publicKey: env.LANGFUSE_PUBLIC_KEY,
secretKey: env.LANGFUSE_SECRET_KEY,
...(env.LANGFUSE_BASE_URL ? { baseUrl: env.LANGFUSE_BASE_URL } : {}),
logger,
...(logger ? { logger } : {}),
});
}
4 changes: 2 additions & 2 deletions src/observability/langfuse-observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export interface LangfuseObservabilityConfig {
}

export class LangfuseObservability implements AIObservability {
private sdk?: NodeSDK;
private initPromise?: Promise<void>;
private sdk?: NodeSDK | undefined;
private initPromise?: Promise<void> | undefined;
private readonly logger: Logger;

constructor(private readonly config: LangfuseObservabilityConfig) {
Expand Down
4 changes: 2 additions & 2 deletions src/prompts/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ export type JudgeResult = {
};

export type CheckItem = {
line: number;
description: string;
line?: number;
description?: string;
analysis: string;
message?: string;
suggestion?: string;
Expand Down
7 changes: 6 additions & 1 deletion src/providers/perplexity-provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { generateText } from 'ai';
import type { LanguageModel } from 'ai';
import { z } from 'zod';
import { createPerplexity } from '@ai-sdk/perplexity';
import type { SearchProvider } from './search-provider';
Expand Down Expand Up @@ -46,7 +47,11 @@ export class PerplexitySearchProvider implements SearchProvider {

try {
const result = await generateText({
model: this.client('sonar-pro'),
// @ai-sdk/perplexity@1 exposes LanguageModelV1 models, while ai@6's
// generateText types require a LanguageModel (V2/V3). This is a
// third-party SDK version skew, not a repo type hole; resolve by
// upgrading @ai-sdk/perplexity to a V2 release in a follow-up.
model: this.client('sonar-pro') as unknown as LanguageModel,
prompt: query,
});

Expand Down
Loading
Loading