Skip to content

feat(agent): add agent mode for cross-document evaluation#74

Closed
oshorefueled wants to merge 34 commits intomainfrom
feat/agent
Closed

feat(agent): add agent mode for cross-document evaluation#74
oshorefueled wants to merge 34 commits intomainfrom
feat/agent

Conversation

@oshorefueled
Copy link
Copy Markdown
Contributor

@oshorefueled oshorefueled commented Mar 19, 2026

Why

VectorLint needed a cross-document evaluation path that can reason across files while still producing review-friendly output (including rdjson diagnostics). Existing per-file lint mode alone could not reliably surface structural/off-page documentation issues during PR review.

What

  • Added --mode agent orchestration in the CLI, running one agent execution per active rule.
  • Added a read-only agent tool suite: read_file, search_content, search_files, list_directory, and lint.
  • Added structured agent finding schemas (inline and top-level) plus merger and reporter support.
  • Added agent output handling for line, json, rdjson, and vale-json fallback-to-json behavior.
  • Aligned agent execution with scan-path rule targeting by computing matched files per rule and passing that set into each agent run.
  • Updated lint tool contract for agent mode to file + ruleContent + optional context, with rule-scoped lint tool instances.
  • Classified off-page / top-level findings as warnings (not scored as on-page errors), including warning severity in rdjson diagnostics and summary counters.
  • Hardened tool/runtime behavior around path normalization, symlink handling, directory listing limits, and concurrency guards.
  • Added comprehensive tests for agent tools, executor behavior, orchestrator output mapping, and matched-file routing.

Scope

In scope

  • Agent-mode runtime, tooling, output formatting, and tests.
  • Scan-path aware rule-to-file matching for agent executions.
  • Warning treatment for top-level/off-page findings.

Out of scope

  • Removing judge / technical-accuracy pathways in non-agent lint flows.
  • Introducing a query_codebase tool.
  • Changing default non-agent mode behavior.

Behavior impact

  • Default behavior is unchanged unless --mode agent is used.
  • In agent mode, findings now include cross-file/structural issues through top-level findings.
  • Top-level/off-page findings are reported as warnings and do not trigger severity-error exit behavior.
  • rdjson output now carries agent findings with appropriate severity for inline comment tooling.

Risk

  • Prompt-driven behavior can vary by model/provider quality.
  • Misconfigured scan paths can reduce rule coverage in agent mode.
  • Agent mode introduces a larger execution surface than single-file lint mode.

How to test / verify

Checks run

  • npm run lint
  • npm run test:run
  • npm run test:run -- tests/agent/agent-executor.test.ts tests/orchestrator-agent-output.test.ts

Manual verification

  1. Run npm run dev -- --mode agent --output line <target-paths> and confirm both inline and top-level findings render.
  2. Run npm run dev -- --mode agent --output rdjson <target-paths> and confirm diagnostics include severity and paths suitable for inline annotations.
  3. Run npm run dev -- --mode agent --output vale-json <target-paths> and confirm fallback warning + JSON payload.

Rollback

  • Revert this PR branch commits.
  • Operationally, omit --mode agent to continue using existing lint-only behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an LLM-driven "agent" mode: new agent executor, workspace‑scoped tools (read/search/files/list/lint), Zod-validated agent finding types, CLI wiring for --mode agent, orchestrator branching for concurrent per-rule agent runs, result merger/renderer updates, tests, and documentation.

Changes

Cohort / File(s) Summary
Agent executor & orchestration
src/agent/agent-executor.ts, src/cli/orchestrator.ts
New runAgentExecutor composes prompts, exposes typed tools to the model, enforces Zod-validated structured JSON output with recovery parsing, and returns per-rule AgentRunResult; orchestrator adds runAgentMode, builds tools/diffContext, runs agents concurrently, aggregates findings, and emits agent-specific outputs (line/json/rdjson).
Agent public surface & aggregation
src/agent/index.ts, src/agent/merger.ts, src/agent/types.ts, src/agent/tools/index.ts
Barrel exports for agent APIs and tools, Zod schemas and TS types for agent findings/AgentRunResult, and collectAgentFindings to flatten per-rule results.
Tools: read / search / files / list
src/agent/tools/read-file.ts, src/agent/tools/search-content.ts, src/agent/tools/search-files.ts, src/agent/tools/list-directory.ts
Workspace-root-scoped file reading with line pagination/truncation; content search using ripgrep with JS fallback (pattern safety, context, limit, glob, ignore-case); glob-based file discovery with limits and traversal protection; sorted directory listing including dotfiles and truncation notices.
Tool: lint sub-tool
src/agent/tools/lint-tool.ts
Rule-scoped lint tool factory that resolves files, blocks traversal, runs evaluator, normalizes judge-style vs violations-style outputs, computes scores/violation entries, and returns structured results.
Path utilities & safety
src/agent/tools/path-utils.ts
expandPath, resolveToCwd, and isWithinRoot utilities using home expansion, cwd resolution, and realpath/resolve to prevent traversal and symlink escape.
CLI mode, options & schemas
src/cli/mode.ts, src/cli/commands.ts, src/cli/types.ts, src/schemas/cli-schemas.ts
Adds `lint
Provider surface
src/providers/llm-provider.ts, src/providers/vercel-ai-provider.ts
LLMProvider gains getLanguageModel(): LanguageModel; VercelAIProvider implements it to return its configured model.
Output/reporting
src/output/reporter.ts, src/output/json-formatter.ts
Adds printAgentFinding for inline/top-level console rendering; extends JSON Issue shape with optional `source?: 'lint'
Tests
tests/agent/*, tests/orchestrator-agent-output.test.ts
Vitest coverage for executor recovery/failure, tools (read/search/files/list), path utils, types, merger, and orchestrator agent-mode output formatting.
Docs / plans / logs
docs/*
New design/spec/plan documents and an execution log documenting the agentic capabilities and generated artifacts.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI
    participant Orch as Orchestrator
    participant Agent as Agent Executor
    participant LLM as Language Model
    participant Tools as Tool Set
    participant FS as File System

    CLI->>Orch: evaluateFiles(targets, { mode: "agent" })
    Orch->>Orch: build tools and diffContext
    Orch->>Orch: model = provider.getLanguageModel()
    Orch->>Agent: runAgentExecutor({ rule, cwd, model, tools, diffContext })
    Agent->>LLM: generateText(systemPrompt + toolSchemas, stepLimit)
    loop Agent-driven tool calls
        LLM->>Tools: invoke tool (read_file / search_content / list_directory / lint)
        Tools->>Tools: resolve path & isWithinRoot check
        Tools->>FS: read/list/search files
        FS-->>Tools: content/results
        Tools-->>LLM: tool response (paginated/truncated/no-match)
    end
    LLM-->>Agent: structured JSON { findings: [...] }
    Agent->>Agent: validate with AGENT_FINDING_SCHEMA
    Agent-->>Orch: { findings, ruleId, error? }
    Orch->>Orch: collectAgentFindings(allResults)
    Orch-->>CLI: print findings / emit JSON summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • ayo6706

Poem

🐰 I hopped through roots and files today,

Tools on paw, I paved the way.
I read, I searched, I kept paths tight,
Findings gathered, neat and right.
A rabbit cheers—agent mode takes flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(agent): add agent mode for cross-document evaluation' is clear, concise, and directly describes the primary feature added in the PR—a new agent mode enabling multi-file evaluation alongside the existing per-file lint workflow.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
tests/agent/list-directory.test.ts (1)

39-46: Minor redundancy: subdir is already created in beforeEach.

Line 40 re-creates subdir which is already set up by beforeEach at line 9. The recursive: true makes this harmless, but you could simplify by only writing the nested file.

♻️ Suggested simplification
   it('lists a specific subdirectory', async () => {
-    mkdirSync(path.join(TMP, 'subdir'), { recursive: true });
     writeFileSync(path.join(TMP, 'subdir', 'nested.md'), '');
     const tool = createListDirectoryTool(TMP);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/agent/list-directory.test.ts` around lines 39 - 46, The test "lists a
specific subdirectory" redundantly recreates the subdir; remove the
mkdirSync(path.join(TMP, 'subdir'), { recursive: true }) call and keep only
writeFileSync(path.join(TMP, 'subdir', 'nested.md'), '') so the test uses the
setup from beforeEach; locate the test using the it block name and the
createListDirectoryTool(TMP) / tool.execute({ path: 'subdir' }) calls to update
the snippet.
src/agent/tools/list-directory.ts (1)

18-59: Consider using async/await for consistency.

The function returns Promise<string> but wraps synchronous operations in Promise.resolve/Promise.reject. Other tools in this module (e.g., search-files.ts) use async/await. For consistency and readability, consider making this an async function.

♻️ Proposed refactor using async/await
-    execute({ path: dirPath, limit }) {
-      try {
-        const absolutePath = resolveToCwd(dirPath || '.', cwd);
-
-        if (!isWithinRoot(absolutePath, cwd)) {
-          return Promise.reject(new Error(`Path traversal blocked: ${dirPath} is outside the allowed root`));
-        }
-
-        if (!existsSync(absolutePath)) {
-          return Promise.reject(new Error(`Directory not found: ${dirPath}`));
-        }
+    async execute({ path: dirPath, limit }) {
+      const absolutePath = resolveToCwd(dirPath || '.', cwd);
+
+      if (!isWithinRoot(absolutePath, cwd)) {
+        throw new Error(`Path traversal blocked: ${dirPath} is outside the allowed root`);
+      }
+
+      if (!existsSync(absolutePath)) {
+        throw new Error(`Directory not found: ${dirPath}`);
+      }
         // ... rest of implementation using return instead of Promise.resolve
-        return Promise.resolve(output);
-      } catch (error) {
-        return Promise.reject(error instanceof Error ? error : new Error(String(error)));
-      }
+      return output;
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/tools/list-directory.ts` around lines 18 - 59, Convert the execute
method to an async function and stop wrapping sync results in
Promise.resolve/Promise.reject: change the execute signature to async execute({
path: dirPath, limit }) and inside use the existing try/catch but return plain
strings or throw Errors instead of Promise.resolve/Promise.reject; keep using
resolveToCwd, isWithinRoot, existsSync, readdirSync, statSync and path.join as
before, compute effectiveLimit from DEFAULT_LIMIT, build results, and when
errors occur throw the Error (or rethrow in catch using throw error instanceof
Error ? error : new Error(String(error))). Also remove any
Promise.resolve/Promise.reject uses and adjust the truncation message logic to
return the combined string directly.
src/agent/agent-executor.ts (1)

140-154: Consider adding fallback parsing for structured output robustness.

The Vercel AI SDK v6.x throws NoOutputGeneratedError when structured output parsing fails (commonly when finishReason is not "stop", especially with AI Gateway or certain provider configurations). While the existing try/catch handles the exception, the recommended pattern in the SDK's issue tracker is to catch NoOutputGeneratedError and fall back to manually parsing result.text as JSON when available. This improves robustness without changing the happy path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/agent-executor.ts` around lines 140 - 154, Catch the specific
NoOutputGeneratedError thrown by generateText and implement a fallback that
attempts to parse the raw text output (e.g., result.text or response.outputText)
as JSON when structured parsing fails; update the try/catch around generateText
in agent-executor (where generateText, AGENT_OUTPUT_SCHEMA, and
stepCountIs(MAX_AGENT_STEPS) are used) to detect NoOutputGeneratedError, attempt
JSON.parse on the raw result text to extract findings and ruleId, and only
rethrow if parsing is impossible so the existing happy path using
response.output.findings remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/agent/tools/path-utils.ts`:
- Around line 17-30: The containment check in isWithinRoot is unsafe because
normalizePath can use realpathSync for one input and path.resolve for the other;
fix by applying the same normalization strategy to both paths: attempt
realpathSync on both and if either call throws, fall back to using path.resolve
for both so both normalizedRoot and normalizedPath are produced by the same
method; keep the final startsWith/equals check and ensure you still append
path.sep when checking prefix so path boundary logic (normalizedRoot + path.sep)
remains correct.

In `@src/agent/tools/search-content.ts`:
- Line 159: Update the tool description string (the description property in the
search-content tool definition) to accurately state the default glob filter as
"**/*.md" instead of "*.md"; verify consistency with the other occurrences that
use "**/*.md" (seen near the uses at lines referencing the default glob in this
file) so the description matches the actual default behavior.
- Around line 104-109: The code constructs a RegExp directly from the untrusted
pattern (pattern / new RegExp(...)) which allows ReDoS; fix by validating or
replacing the engine before compiling: either validate the pattern with a safety
check (e.g., integrate safe-regex to reject dangerous patterns) or switch to a
backtracking-free engine (e.g., use re2 to instantiate the regex instead of
RegExp) and/or wrap regex execution in a short timeout guard to abort
long-running matches; update the logic around the RegExp creation in
search-content.ts where regex = new RegExp(pattern, opts.ignoreCase ? 'i' : ''),
rejecting unsafe patterns (returning the same error string) or using re2 and
ensure opts.ignoreCase behavior is preserved.

In `@src/agent/tools/search-files.ts`:
- Around line 25-30: The fast-glob results in searchFiles (function in
src/agent/tools/search-files.ts) return paths relative to searchRoot when a
subdirectory `path` is provided, but caller tools (read_file, lint) expect
repo-relative paths, so prepend the provided `path` prefix to each match before
returning; import Node's `path` module, update the function description to state
it returns repository-relative paths, and ensure both the code branches that map
`matches` (the arrays created around lines with fg(...) and the later mapping at
36-42) add `path.join(pathPrefix, match)` (or similar) only when a non-empty
`path` argument was passed.

In `@src/cli/orchestrator.ts`:
- Around line 190-196: The RdJson/ValeJson branches instantiate RdJsonFormatter
and ValeJsonFormatter but never add agent findings, causing formatter.toJson()
to emit empty output; update the branches handling OutputFormat.RdJson and
OutputFormat.ValeJson in orchestrator.ts (the sections creating RdJsonFormatter
and ValeJsonFormatter) to either (A) log a warning that RdJson/ValeJson are not
supported in agent mode and fall back to the existing JSON formatter path (e.g.,
reuse the code that populates findings for JSON output), or (B) map the
collected agent findings into the RdJsonFormatter/ValeJsonFormatter APIs before
calling formatter.toJson(); implement one of these fixes and ensure the
warning/fallback is clearly emitted when in agent mode so users don’t get empty
output.

In `@src/output/reporter.ts`:
- Line 229: The ternary that computes loc uses a truthy check that treats 0 as
absent; update the expression that sets loc to check explicitly for undefined
(reference.startLine !== undefined) so a valid startLine of 0 is preserved—loc
should remain `${reference.file}:${reference.startLine}` when startLine is any
number, and fall back to reference.file only when startLine is strictly
undefined; modify the assignment that references reference.startLine in
src/output/reporter.ts accordingly.

---

Nitpick comments:
In `@src/agent/agent-executor.ts`:
- Around line 140-154: Catch the specific NoOutputGeneratedError thrown by
generateText and implement a fallback that attempts to parse the raw text output
(e.g., result.text or response.outputText) as JSON when structured parsing
fails; update the try/catch around generateText in agent-executor (where
generateText, AGENT_OUTPUT_SCHEMA, and stepCountIs(MAX_AGENT_STEPS) are used) to
detect NoOutputGeneratedError, attempt JSON.parse on the raw result text to
extract findings and ruleId, and only rethrow if parsing is impossible so the
existing happy path using response.output.findings remains unchanged.

In `@src/agent/tools/list-directory.ts`:
- Around line 18-59: Convert the execute method to an async function and stop
wrapping sync results in Promise.resolve/Promise.reject: change the execute
signature to async execute({ path: dirPath, limit }) and inside use the existing
try/catch but return plain strings or throw Errors instead of
Promise.resolve/Promise.reject; keep using resolveToCwd, isWithinRoot,
existsSync, readdirSync, statSync and path.join as before, compute
effectiveLimit from DEFAULT_LIMIT, build results, and when errors occur throw
the Error (or rethrow in catch using throw error instanceof Error ? error : new
Error(String(error))). Also remove any Promise.resolve/Promise.reject uses and
adjust the truncation message logic to return the combined string directly.

In `@tests/agent/list-directory.test.ts`:
- Around line 39-46: The test "lists a specific subdirectory" redundantly
recreates the subdir; remove the mkdirSync(path.join(TMP, 'subdir'), {
recursive: true }) call and keep only writeFileSync(path.join(TMP, 'subdir',
'nested.md'), '') so the test uses the setup from beforeEach; locate the test
using the it block name and the createListDirectoryTool(TMP) / tool.execute({
path: 'subdir' }) calls to update the snippet.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 00616295-063b-477b-b4e0-26cada6dd396

📥 Commits

Reviewing files that changed from the base of the PR and between 27097c3 and 2a43d21.

📒 Files selected for processing (28)
  • docs/logs/2026-03-17-agentic-capabilities.log.md
  • src/agent/agent-executor.ts
  • src/agent/index.ts
  • src/agent/merger.ts
  • src/agent/tools/index.ts
  • src/agent/tools/lint-tool.ts
  • src/agent/tools/list-directory.ts
  • src/agent/tools/path-utils.ts
  • src/agent/tools/read-file.ts
  • src/agent/tools/search-content.ts
  • src/agent/tools/search-files.ts
  • src/agent/types.ts
  • src/cli/commands.ts
  • src/cli/orchestrator.ts
  • src/cli/types.ts
  • src/output/json-formatter.ts
  • src/output/reporter.ts
  • src/providers/llm-provider.ts
  • src/providers/vercel-ai-provider.ts
  • src/schemas/cli-schemas.ts
  • tests/agent/agent-executor.test.ts
  • tests/agent/list-directory.test.ts
  • tests/agent/merger.test.ts
  • tests/agent/path-utils.test.ts
  • tests/agent/read-file.test.ts
  • tests/agent/search-content.test.ts
  • tests/agent/search-files.test.ts
  • tests/agent/types.test.ts

- Return repo-root-relative search_files paths for direct tool chaining.

- Warn and fall back to JSON when rdjson/vale-json is selected in

  agent mode, avoiding empty formatter output.

- Harden path containment checks, regex validation, and agent

  structured-output fallback parsing.

- Convert list_directory to async fs/promises behavior and remove

  redundant test setup.

- Add and align agentic design/plan docs with the updated contracts.
- Map agent findings into rdjson diagnostics for inline review tooling.

- Keep vale-json unsupported in agent mode with JSON fallback warning.

- Update agentic design/plan docs to reflect rdjson compatibility.

- Add orchestrator tests for rdjson mapping and vale-json fallback.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/plans/2026-03-17-agentic-capabilities.md (1)

276-282: Plan's isWithinRoot differs from actual implementation.

The code snippet in this plan shows a simple path.resolve approach, but the actual implementation in src/agent/tools/path-utils.ts uses realpathSync to resolve symlinks before checking containment. This is an improvement over the plan—consider updating the plan to reflect the actual more secure implementation for documentation accuracy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-17-agentic-capabilities.md` around lines 276 - 282, Update
the plan's isWithinRoot example to match the real implementation: note that the
actual function uses realpathSync to resolve symlinks before containment checks
(see function isWithinRoot in path-utils.ts) rather than just path.resolve;
change the snippet and description to mention realpathSync-based resolution and
the security rationale so docs reflect the actual, safer behavior.
src/agent/agent-executor.ts (1)

184-193: Fallback parsing uses potentially empty responseText on early errors.

When generateText throws before completion, responseText remains an empty string (from line 117). The NoOutputGeneratedError fallback at line 186 will then call parseAgentOutputFromText(''), which returns null and falls through to the generic error. This is safe but could be optimized by checking responseText first.

🔧 Minor optimization
   } catch (error) {
     if (NoOutputGeneratedError.isInstance(error)) {
+      if (responseText) {
         const fallbackOutput = parseAgentOutputFromText(responseText);
         if (fallbackOutput) {
           return {
             findings: fallbackOutput.findings,
             ruleId: rule.meta.id,
           };
         }
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/agent-executor.ts` around lines 184 - 193, The catch block handling
NoOutputGeneratedError should avoid calling parseAgentOutputFromText when
responseText is empty; update the catch (error) logic around
NoOutputGeneratedError.isInstance(error) to first check that responseText (the
variable populated during generateText) is non-empty/has non-whitespace content
(e.g., responseText && responseText.trim()) before calling
parseAgentOutputFromText(responseText), and only return the fallback { findings:
..., ruleId: rule.meta.id } when parseAgentOutputFromText actually returns a
value; otherwise fall through to the existing generic error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/specs/2026-03-17-agentic-capabilities-design.md`:
- Around line 150-179: The executor is not threading the orchestrator
AbortSignal into tool calls—generateText already receives signal but the tool
wrappers call tool.execute(params) without a signal; update the Tool
interface/method signature so execute(id, params, signal?: AbortSignal) accepts
a signal, and change all invocation sites in agent-executor.ts (the wrapper
functions that call tool.execute) to forward the orchestrator signal (the same
controller.signal used for generateText) into tool.execute; also update any tool
implementations to honor the signal (entry check, event listener, and manual
checkpoints) so long-running tool operations can be aborted.

In `@src/agent/tools/search-content.ts`:
- Around line 177-182: The code mixes rejection and resolution for errors:
isWithinRoot check uses Promise.reject(new Error(...)) while isLikelyUnsafeRegex
returns Promise.resolve('Invalid regex pattern'), causing inconsistent caller
behavior; change the path traversal branch so it also resolves with a
user-facing string (e.g., return Promise.resolve('Path traversal blocked:
${searchDir} is outside the allowed root')) instead of rejecting, keeping error
reporting consistent for callers of search-content.ts and using the same
message-style as the isLikelyUnsafeRegex branch; update references in tests or
callers that expect a rejection from isWithinRoot if any.
- Around line 110-114: The glob sync call in searchWithJs returns files that can
follow symlinks, allowing traversal outside the intended searchRoot; update the
fg.sync options used in searchWithJs to include followSymbolicLinks: false
(matching search-files.ts) so that symbolic links are not followed and external
targets cannot be read despite isWithinRoot checks on the root; ensure the
option is added to the same options object that sets cwd, ignore, and absolute.

In `@src/cli/orchestrator.ts`:
- Around line 145-154: The call to runWithConcurrency uses Math.max(1,
concurrency) but concurrency can be undefined which makes Math.max return NaN;
replace the expression by computing a safe integer worker count before the call
(e.g., coerce options.concurrency to a number with a default and ensure it's at
least 1) and then pass that safe value into runWithConcurrency; update the code
around the runWithConcurrency invocation that references concurrency (and any
options.concurrency source) so you use the validated safeConcurrency variable
instead of Math.max(1, concurrency).

---

Nitpick comments:
In `@docs/plans/2026-03-17-agentic-capabilities.md`:
- Around line 276-282: Update the plan's isWithinRoot example to match the real
implementation: note that the actual function uses realpathSync to resolve
symlinks before containment checks (see function isWithinRoot in path-utils.ts)
rather than just path.resolve; change the snippet and description to mention
realpathSync-based resolution and the security rationale so docs reflect the
actual, safer behavior.

In `@src/agent/agent-executor.ts`:
- Around line 184-193: The catch block handling NoOutputGeneratedError should
avoid calling parseAgentOutputFromText when responseText is empty; update the
catch (error) logic around NoOutputGeneratedError.isInstance(error) to first
check that responseText (the variable populated during generateText) is
non-empty/has non-whitespace content (e.g., responseText && responseText.trim())
before calling parseAgentOutputFromText(responseText), and only return the
fallback { findings: ..., ruleId: rule.meta.id } when parseAgentOutputFromText
actually returns a value; otherwise fall through to the existing generic error
path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 936058df-12e0-42a1-950e-d24be12f6448

📥 Commits

Reviewing files that changed from the base of the PR and between fdb0d5a and 6f84ef7.

📒 Files selected for processing (13)
  • docs/plans/2026-03-17-agentic-capabilities.md
  • docs/specs/2026-03-17-agentic-capabilities-design.md
  • src/agent/agent-executor.ts
  • src/agent/tools/list-directory.ts
  • src/agent/tools/path-utils.ts
  • src/agent/tools/search-content.ts
  • src/agent/tools/search-files.ts
  • src/cli/orchestrator.ts
  • src/output/reporter.ts
  • tests/agent/agent-executor.test.ts
  • tests/agent/list-directory.test.ts
  • tests/agent/search-content.test.ts
  • tests/agent/search-files.test.ts
✅ Files skipped from review due to trivial changes (3)
  • tests/agent/agent-executor.test.ts
  • tests/agent/search-files.test.ts
  • src/agent/tools/path-utils.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/output/reporter.ts
  • tests/agent/search-content.test.ts
  • tests/agent/list-directory.test.ts
  • src/agent/tools/search-files.ts
  • src/agent/tools/list-directory.ts

- disable symlink following in JS content search fallback to keep traversal constraints consistent

- sanitize agent-mode concurrency input to a safe positive integer before scheduling work

- align design spec wording with current v1 cancellation behavior (model-level signal only)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/cli/orchestrator.ts (1)

203-212: ⚠️ Potential issue | 🟡 Minor

concurrency may be NaN if undefined, causing zero workers.

At line 203, Math.max(1, concurrency) is used, but if options.concurrency is undefined, Math.max(1, undefined) returns NaN. This causes runWithConcurrency to create zero workers since Math.min(NaN, items.length) evaluates to NaN, which becomes 0 in array creation.

🔧 Proposed fix with nullish coalescing
-  const agentResults = await runWithConcurrency(prompts, Math.max(1, concurrency), async (rule) =>
+  const agentResults = await runWithConcurrency(prompts, Math.max(1, concurrency ?? 1), async (rule) =>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/orchestrator.ts` around lines 203 - 212, The Math.max(1, concurrency)
call can produce NaN when concurrency is undefined; update the orchestration
call to coerce undefined/null to a numeric default before passing to
runWithConcurrency (e.g., use a nullish coalescing fallback for
options.concurrency) so the worker count is always at least 1. Locate the call
around runWithConcurrency(prompts, Math.max(1, concurrency), ...) in
orchestrator.ts and replace Math.max(1, concurrency) with Math.max(1,
(concurrency ?? 1)) or an equivalent numeric fallback; ensure you reference the
same concurrency variable derived from options.concurrency and that
runAgentExecutor and prompts usage remains unchanged. Ensure the final value
passed is a finite integer >= 1 so runWithConcurrency's internal Math.min/array
creation cannot produce NaN.
🧹 Nitpick comments (1)
docs/specs/2026-03-17-agentic-capabilities-design.md (1)

45-60: Consider adding a language specifier to the fenced code block.

The architecture diagram uses a plain fenced code block without a language specifier. While this is a minor formatting issue, adding a specifier like text or plaintext would satisfy linters.

-```
+```text
 GitHub App
     │
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/specs/2026-03-17-agentic-capabilities-design.md` around lines 45 - 60,
The fenced code block containing the architecture diagram (lines showing "GitHub
App", "VectorLint Cloud", "VectorLint Service", "VectorLint CLI", "Lint
Executor", and "Agent Executor") should include a language specifier to satisfy
linters—update the opening fence from ``` to ```text (or ```plaintext) so the
diagram is treated as plain text; ensure the rest of the block content is
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/cli/orchestrator.ts`:
- Around line 203-212: The Math.max(1, concurrency) call can produce NaN when
concurrency is undefined; update the orchestration call to coerce undefined/null
to a numeric default before passing to runWithConcurrency (e.g., use a nullish
coalescing fallback for options.concurrency) so the worker count is always at
least 1. Locate the call around runWithConcurrency(prompts, Math.max(1,
concurrency), ...) in orchestrator.ts and replace Math.max(1, concurrency) with
Math.max(1, (concurrency ?? 1)) or an equivalent numeric fallback; ensure you
reference the same concurrency variable derived from options.concurrency and
that runAgentExecutor and prompts usage remains unchanged. Ensure the final
value passed is a finite integer >= 1 so runWithConcurrency's internal
Math.min/array creation cannot produce NaN.

---

Nitpick comments:
In `@docs/specs/2026-03-17-agentic-capabilities-design.md`:
- Around line 45-60: The fenced code block containing the architecture diagram
(lines showing "GitHub App", "VectorLint Cloud", "VectorLint Service",
"VectorLint CLI", "Lint Executor", and "Agent Executor") should include a
language specifier to satisfy linters—update the opening fence from ``` to
```text (or ```plaintext) so the diagram is treated as plain text; ensure the
rest of the block content is unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cadb2400-b535-4345-9185-ccd8371ae07e

📥 Commits

Reviewing files that changed from the base of the PR and between 6f84ef7 and dac57d9.

📒 Files selected for processing (4)
  • docs/plans/2026-03-17-agentic-capabilities.md
  • docs/specs/2026-03-17-agentic-capabilities-design.md
  • src/cli/orchestrator.ts
  • tests/orchestrator-agent-output.test.ts

- stop stat'ing every directory entry once the requested limit is satisfied

- compute truncation hint from raw entry count to avoid false positives

- normalize diff-context file rendering through repo-relative path helper

- add tests for truncation hint behavior
- run each agent with rule-specific matched files derived from scan path resolution

- switch lint tool contract to file+ruleContent+context and bind lint tool per rule

- classify top-level findings as warnings (including rdjson severity and summary counts)

- add tests covering matched-file routing and warning/error aggregation
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/agent/tools/search-content.ts (1)

178-183: ⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: path traversal rejects while unsafe regex resolves.

Path traversal at line 179 uses Promise.reject(new Error(...)) while unsafe regex at line 182 uses Promise.resolve('Invalid regex pattern'). For agent tools that return strings for LLM consumption, both should resolve with error messages for consistent caller behavior.

🔧 Proposed fix
       if (!isWithinRoot(searchRoot, cwd)) {
-        return Promise.reject(new Error(`Path traversal blocked: ${searchDir} is outside the allowed root`));
+        return Promise.resolve(`Path traversal blocked: ${searchDir} is outside the allowed root`);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/tools/search-content.ts` around lines 178 - 183, The code mixes
rejection and resolution for input validation: isWithinRoot(...) currently does
Promise.reject(new Error(...)) while isLikelyUnsafeRegex(...) returns
Promise.resolve('Invalid regex pattern'); change the path-traversal branch to
return a resolved string error like 'Path traversal blocked: <searchDir> is
outside the allowed root' (i.e., replace Promise.reject with Promise.resolve
returning a plain error string) so both isWithinRoot and isLikelyUnsafeRegex
validation paths consistently resolve with human-readable error strings for the
caller; update the logic around searchRoot, cwd, searchDir, pattern,
isWithinRoot, and isLikelyUnsafeRegex accordingly.
🧹 Nitpick comments (4)
src/cli/orchestrator.ts (2)

271-310: Consider adding AbortSignal to agent executor calls for graceful cancellation.

The runAgentMode function doesn't create or pass an AbortSignal to runAgentExecutor, even though the interface supports it (line 30 in agent-executor.ts). Without this, there's no way to implement wall-clock timeouts for runaway agent loops as mentioned in the design spec (line 120).

For v1 this may be acceptable per the spec, but consider adding a timeout mechanism:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), AGENT_TIMEOUT_MS);
try {
  // ... runAgentExecutor calls with signal: controller.signal
} finally {
  clearTimeout(timeout);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/orchestrator.ts` around lines 271 - 310, Add an AbortSignal to agent
runs so callers can cancel long-running agents: in runAgentMode create an
AbortController (e.g., controller) before invoking runWithConcurrency, start a
timeout that calls controller.abort() after your configured AGENT_TIMEOUT_MS,
pass controller.signal through to each runAgentExecutor call in the tools/args
object, and ensure you clear the timeout in a finally block; update usages
around runWithConcurrency, runAgentExecutor, and agentResults to thread the
signal through.

1-3: Consider using node: protocol for Node.js built-in imports.

The agent-related files (src/agent/tools/search-content.ts, etc.) use node:fs, node:path, node:child_process. For consistency across the codebase, consider updating these imports to use the explicit node: protocol.

📝 Proposed fix
-import { readFileSync } from 'fs';
-import { randomUUID } from 'crypto';
-import * as path from 'path';
+import { readFileSync } from 'node:fs';
+import { randomUUID } from 'node:crypto';
+import * as path from 'node:path';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/orchestrator.ts` around lines 1 - 3, The import statements in
orchestrator.ts use bare specifiers for built-ins; update them to the explicit
node: protocol to match the rest of the codebase (change "fs" -> "node:fs",
"crypto" -> "node:crypto", and "path" -> "node:path") so that the existing
imports like readFileSync, randomUUID and the path namespace import remain
identical but use the node: protocol for consistency with files such as
src/agent/tools/search-content.ts.
src/agent/agent-executor.ts (2)

119-121: Redundant default value for matchedFiles in destructuring.

matchedFiles is already typed as string[] in AgentExecutorParams (not optional), so the = [] default is never reached. This could mask caller bugs if the type changes to optional later.

Consider either:

  • Remove the default if the field is always required
  • Make the field optional in the interface if defaults are intended
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/agent-executor.ts` around lines 119 - 121, The destructured default
value "matchedFiles = []" in runAgentExecutor is redundant because
AgentExecutorParams currently defines matchedFiles as a non-optional string[];
remove the "= []" default from the destructuring in runAgentExecutor so the
function relies on the declared type, or alternatively update
AgentExecutorParams to make matchedFiles optional (matchedFiles?: string[]) if
you intend a default to be applied — ensure you update any callers/tests
accordingly and keep the symbol names runAgentExecutor and AgentExecutorParams
consistent.

190-207: Add explicit unknown type to catch block parameter.

Per coding guidelines, catch blocks should use unknown type. While the current code handles errors correctly, the explicit type annotation improves clarity and enforces the guideline.

📝 Proposed fix
-  } catch (error) {
+  } catch (error: unknown) {
     if (NoOutputGeneratedError.isInstance(error)) {

As per coding guidelines: "catch blocks use unknown type".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/agent-executor.ts` around lines 190 - 207, Change the catch
parameter to use the explicit unknown type: replace catch (error) with catch
(error: unknown) in the try/catch block inside agent-executor (the block that
references NoOutputGeneratedError, parseAgentOutputFromText, responseText and
rule.meta.id); keep the existing runtime checks
(NoOutputGeneratedError.isInstance(error) and error instanceof Error) and the
fallback handling so the code still narrows the unknown to Error or string
before constructing the message and returning the error payload.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/specs/2026-03-17-agentic-capabilities-design.md`:
- Around line 45-60: The fenced ASCII diagram block in the docs (the
triple-backtick block showing "GitHub App → VectorLint Cloud → VectorLint
Service → VectorLint CLI...") lacks a language specifier and triggers MD040;
update that code fence to include a text language identifier (```text) so the
diagram is treated as plain text/ASCII art—locate the triple-backtick block
containing the diagram and change the opening fence to use "text".

---

Duplicate comments:
In `@src/agent/tools/search-content.ts`:
- Around line 178-183: The code mixes rejection and resolution for input
validation: isWithinRoot(...) currently does Promise.reject(new Error(...))
while isLikelyUnsafeRegex(...) returns Promise.resolve('Invalid regex pattern');
change the path-traversal branch to return a resolved string error like 'Path
traversal blocked: <searchDir> is outside the allowed root' (i.e., replace
Promise.reject with Promise.resolve returning a plain error string) so both
isWithinRoot and isLikelyUnsafeRegex validation paths consistently resolve with
human-readable error strings for the caller; update the logic around searchRoot,
cwd, searchDir, pattern, isWithinRoot, and isLikelyUnsafeRegex accordingly.

---

Nitpick comments:
In `@src/agent/agent-executor.ts`:
- Around line 119-121: The destructured default value "matchedFiles = []" in
runAgentExecutor is redundant because AgentExecutorParams currently defines
matchedFiles as a non-optional string[]; remove the "= []" default from the
destructuring in runAgentExecutor so the function relies on the declared type,
or alternatively update AgentExecutorParams to make matchedFiles optional
(matchedFiles?: string[]) if you intend a default to be applied — ensure you
update any callers/tests accordingly and keep the symbol names runAgentExecutor
and AgentExecutorParams consistent.
- Around line 190-207: Change the catch parameter to use the explicit unknown
type: replace catch (error) with catch (error: unknown) in the try/catch block
inside agent-executor (the block that references NoOutputGeneratedError,
parseAgentOutputFromText, responseText and rule.meta.id); keep the existing
runtime checks (NoOutputGeneratedError.isInstance(error) and error instanceof
Error) and the fallback handling so the code still narrows the unknown to Error
or string before constructing the message and returning the error payload.

In `@src/cli/orchestrator.ts`:
- Around line 271-310: Add an AbortSignal to agent runs so callers can cancel
long-running agents: in runAgentMode create an AbortController (e.g.,
controller) before invoking runWithConcurrency, start a timeout that calls
controller.abort() after your configured AGENT_TIMEOUT_MS, pass
controller.signal through to each runAgentExecutor call in the tools/args
object, and ensure you clear the timeout in a finally block; update usages
around runWithConcurrency, runAgentExecutor, and agentResults to thread the
signal through.
- Around line 1-3: The import statements in orchestrator.ts use bare specifiers
for built-ins; update them to the explicit node: protocol to match the rest of
the codebase (change "fs" -> "node:fs", "crypto" -> "node:crypto", and "path" ->
"node:path") so that the existing imports like readFileSync, randomUUID and the
path namespace import remain identical but use the node: protocol for
consistency with files such as src/agent/tools/search-content.ts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d85a6e0-16b6-492d-8ec1-d6af19bbfbe1

📥 Commits

Reviewing files that changed from the base of the PR and between dac57d9 and 62c7f8c.

📒 Files selected for processing (10)
  • docs/specs/2026-03-17-agentic-capabilities-design.md
  • src/agent/agent-executor.ts
  • src/agent/tools/lint-tool.ts
  • src/agent/tools/list-directory.ts
  • src/agent/tools/search-content.ts
  • src/cli/orchestrator.ts
  • src/output/reporter.ts
  • tests/agent/agent-executor.test.ts
  • tests/agent/list-directory.test.ts
  • tests/orchestrator-agent-output.test.ts
✅ Files skipped from review due to trivial changes (3)
  • src/agent/tools/list-directory.ts
  • tests/agent/agent-executor.test.ts
  • tests/orchestrator-agent-output.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/agent/list-directory.test.ts
  • src/output/reporter.ts
  • src/agent/tools/lint-tool.ts

Comment on lines +45 to +60
```
GitHub App
VectorLint Cloud ← receives webhook, sends credentials + PR metadata
VectorLint Service ← shallow clones repo, invokes CLI
VectorLint CLI
├── Lint Executor ← default mode: existing per-page evaluation (unchanged)
└── Agent Executor ← --mode agent: one agent per rule, parallel
└── Tools: lint, read_file, search_content, search_files, list_directory
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language specifier to fenced code block.

The architecture diagram code block lacks a language specifier. Static analysis flagged this (MD040). Use a text-based identifier for ASCII diagrams.

📝 Proposed fix
-```
+```text
 GitHub App
     │
     ▼
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
GitHub App
VectorLint Cloud ← receives webhook, sends credentials + PR metadata
VectorLint Service ← shallow clones repo, invokes CLI
VectorLint CLI
├── Lint Executor ← default mode: existing per-page evaluation (unchanged)
└── Agent Executor ← --mode agent: one agent per rule, parallel
└── Tools: lint, read_file, search_content, search_files, list_directory
```
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 45-45: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/specs/2026-03-17-agentic-capabilities-design.md` around lines 45 - 60,
The fenced ASCII diagram block in the docs (the triple-backtick block showing
"GitHub App → VectorLint Cloud → VectorLint Service → VectorLint CLI...") lacks
a language specifier and triggers MD040; update that code fence to include a
text language identifier (```text) so the diagram is treated as plain text/ASCII
art—locate the triple-backtick block containing the diagram and change the
opening fence to use "text".

- add lint-tool contract tests for ruleContent validation and context injection

- add fixture-backed agent trace replay test through real orchestrator/executor flow

- expand scan-path routing tests for rule disable overrides across overlapping patterns
- compute post-loop agent rule scores from inline findings using density scoring

- exclude top-level findings from scoring while keeping them as warning-level findings

- make search_files honor repo .gitignore patterns and keep repo-relative output

- add tests for scoring behavior and gitignore-aware file discovery
- drop plan, spec, and execution log docs from this PR scope

- keep implementation changes focused on agent runtime and tests
- switch line output progress to a single static spinner message

- render one final done line and keep status output on stderr only

- keep json, rdjson, and vale-json stdout payload behavior unchanged

- add focused reporter and orchestrator tests for tty and stdout safety
- show linting text in lint mode and reviewing text in agent mode

- stop spinner before report rows so output starts on a fresh line

- add tests covering mode-specific status text and newline behavior
- run agent-mode rules serially and limit tool execution to one at a time

- add tool-level status updates during agent runs for better visibility

- make agent retries configurable via AGENT_MAX_RETRIES in config.toml

- add and update tests for retry wiring, env parsing, and status output
@vectorlint
Copy link
Copy Markdown

vectorlint bot commented Mar 25, 2026

Content Review by VectorLint


No issues found!

Great job! Your content passed all VectorLint checks.


Powered by VectorLint

- Reuse lint row rendering for agent findings to keep line output consistent.

- Map top-level agent findings to 1:1 and group rows by file headers.

- Replace raw status tokens with human-readable agent tool actions.

- Show elapsed time in the final spinner completion line.

- Update progress and agent output tests for the new UX contract.
- Centralize agent tool-action labels in a single constant map.

- Eliminate intermediate finding normalization array in row rendering.

- Skip spinner re-render when running text is unchanged.

- Preserve output behavior while reducing redundant work.
- Switch agent execution ordering to file-first units (file + rule).

- Replace single-line spinner with TTY block progress per file, with

  live tool-call updates that preserve scrollback history.

- Surface tool args in status events and format compact tool lines

  (including concise lint preview display).

- Expand agent output tests for file-first orchestration and

  block-progress rendering behavior.
- replace per-file-per-rule executor model with one agent session per run

- define strict prompt precedence: role, operating policy, runtime context

- add global -p/--print headless mode contract for lint and agent

- clarify naming semantics to avoid git-diff terminology drift
- replace broad docs ignore with explicit docs/logs, docs/artifacts, docs/plans paths

- keep docs/specs trackable for versioned design updates
- replace per-file/per-rule executor args with requestedTargets + fileRuleMap

- add global -p/--print plumbing and suppress interactive progress in print mode

- support run-level lint routing by ruleId via rules catalog

- update agent and orchestrator tests for single-run behavior and prompt precedence
- route lint by catalog ruleKey to avoid ruleId collision ambiguity

- remove synthetic post-run file/rule progress transitions

- add parser coverage for print flag and expand lint-tool routing tests
- Increase agent-mode default max retries from 5 to 10 for better resilience under transient provider and rate-limit failures.

- Keep AGENT_MAX_RETRIES configurable and update the global config template to reflect the new default.

- Update executor tests to assert the new retry default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant