Skip to content

feat(agent): redesign agent mode with deterministic replay runtime#76

Closed
oshorefueled wants to merge 9 commits intomainfrom
feat/agentic-review
Closed

feat(agent): redesign agent mode with deterministic replay runtime#76
oshorefueled wants to merge 9 commits intomainfrom
feat/agentic-review

Conversation

@oshorefueled
Copy link
Copy Markdown
Contributor

@oshorefueled oshorefueled commented Mar 31, 2026

Why

Agent mode behavior had drifted and produced fragile completion/reporting semantics. This redesign establishes deterministic, replay-backed findings with explicit finalization so agent runs are reliable, machine-safe across output modes, and easier to reason about operationally.

What

  • Added a clean agent runtime surface on this branch, including --mode agent and --print CLI wiring.
  • Introduced explicit agent contracts: lint(file, ruleSource, context?), top-level report_finding, and mandatory finalize_review.
  • Added collision-resilient file-backed review sessions at ~/.vectorlint/reviews/*.jsonl with typed lifecycle/finding events.
  • Made lint output authoritative for inline findings, persisted as finding_recorded_inline events.
  • Enforced hard failure when finalize_review is missing and propagated this to orchestrator operational error handling.
  • Added deterministic session replay to derive final findings/scores instead of free-form in-memory/model text.
  • Added agent progress reporting for interactive line mode and locked behavior around --print suppression and JSON-family machine-safe output.
  • Added comprehensive agent-mode test coverage (tests/agent/*, tests/orchestrator-agent-output.test.ts).

Scope

In scope

  • New src/agent/* runtime components and tools.
  • Agent-mode orchestration integration in CLI/types/orchestrator.
  • Rule identity mapping and replay-based output derivation.
  • Progress UX contract behavior and tests.

Out of scope

  • Legacy branch code reuse (feat/agent) or backport strategy.
  • Non-agent lint/judge evaluator algorithm changes.
  • Provider/model transport changes.

Behavior impact

  • Agent mode now requires an explicit finalize_review signal to be considered successful.
  • Inline findings are sourced from lint tool outputs and persisted immediately.
  • Final agent JSON-family payloads are replay-derived and deterministic by session events.
  • Interactive progress output is TTY-gated in line mode; --print suppresses progress UI.

Risk

  • Medium: this is a substantial new runtime slice (src/agent/*) and orchestration wiring.
  • Main mitigations: focused unit/integration tests for contracts, finalize enforcement, replay output, and output-mode UX invariants.
  • Residual risk: current replay scoring is intentionally simple and may need iteration as richer scoring parity requirements evolve.

How to test / verify

Checks run

  • npm run lint
  • npm run build
  • npm run test:run -- tests/agent tests/orchestrator-agent-output.test.ts
  • LLM_PROVIDER=openai OPENAI_API_KEY=dummy OPENAI_MODEL=gpt-4o npm run dev -- README.md --mode=agent --config tests/evaluations/.vectorlint.ini
  • LLM_PROVIDER=openai OPENAI_API_KEY=dummy OPENAI_MODEL=gpt-4o npm run dev -- README.md --mode=agent --print --config tests/evaluations/.vectorlint.ini

Manual verification

  • Run vectorlint <file> --mode=agent in a TTY and confirm progress lines follow the expected format and done markers.
  • Run with --print and confirm interactive progress does not appear.
  • Run with --output json and confirm stdout is machine-parseable JSON with replay-derived findings/scores.
  • Simulate a custom agent execution path that omits finalize_review and confirm non-zero / operational error behavior.

Rollback

  • Revert this PR to restore pre-agent-runtime behavior in this branch.
  • If partial rollback is needed, disable agent mode routing in orchestrator and remove --mode agent usage paths while retaining existing lint-mode flows.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added --mode CLI option to select "lint" (default) or "agent" runtime
    • Added --print flag to suppress output
    • Agent mode persists evaluation sessions with deterministic event replay
    • Agent mode reports progress with structured tool call and file completion tracking
    • Agent-mode output includes finding scores computed per rule
  • Tests

    • Added comprehensive test suite for agent runtime components and output validation

@oshorefueled
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

This change introduces a complete agent-mode runtime for conducting code reviews via tooling. It wires CLI options (--mode, --print), creates an agent executor with session persistence, implements rule registry/lookup, defines type contracts for findings and events, and adds output reporting alongside comprehensive test coverage.

Changes

Cohort / File(s) Summary
Agent Core Runtime
src/agent/agent-executor.ts, src/agent/types.ts, src/agent/index.ts
Implements agent executor pipeline with tooling orchestration, session lifecycle management, rule source registry building, and findings accumulation. Adds type schemas for lint inputs, findings, and deterministic session events (session_started, tool_call_started/finished, finding_recorded_inline/top_level, session_finalized). Re-exports agent APIs through main index.
Session Management
src/agent/review-session-store.ts, src/agent/session-replay.ts, src/agent/merger.ts
Introduces collision-resilient JSONL-backed session store with exclusive file creation, event append/replay, and finalization detection. Adds session replay utilities to reconstruct findings and build summary reports with per-rule scoring. Implements deduplication logic for merging finding arrays by computed fingerprint.
Lint Tool Implementation
src/agent/tools/lint-tool.ts, src/agent/tools/index.ts
Defines lint tool factory with data contracts for violations and rule results. Looks up canonical rule IDs from rule-source registry at runtime and normalizes violations from rule execution.
CLI Integration
src/cli/commands.ts, src/cli/orchestrator.ts, src/cli/types.ts, src/schemas/cli-schemas.ts
Adds CLI options (--mode, --print) to command registration and extends evaluation options/results. Branches evaluateFiles to delegate to agent executor when mode is "agent" and conditionally outputs agent report as JSON. Extends schemas and result types to support agent reporting.
Output & Rule Identity
src/output/agent-progress-reporter.ts, src/prompts/rule-identity.ts
Introduces progress reporter interface for stderr-based lifecycle callbacks (file review, tool calls, completion timing). Adds utilities to derive canonical rule IDs and rule sources from prompts with PascalCase normalization.
Documentation & Tests
docs/logs/2026-03-31-agent-mode-implementation-plan.log.md, tests/agent/*, tests/orchestrator-agent-output.test.ts
Adds execution log documenting completed tasks. Covers executor registry building, inline finding recording, session persistence, finalization enforcement, progress reporting, and TTY-gated output behavior via six new test suites.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant CLI as CLI Orchestrator
    participant Executor as Agent Executor
    participant Store as Session Store
    participant Tool as Lint Tool
    participant Model as Agent Model

    User->>CLI: evaluateFiles(targets, {mode: 'agent'})
    CLI->>Executor: runAgentExecutor({targets, prompts})
    
    Executor->>Store: createReviewSessionStore()
    Store-->>Executor: ReviewSessionStore
    
    Executor->>Store: append(session_started)
    Store-->>Executor: ✓
    
    Executor->>Executor: buildRuleRegistry(targets, prompts)
    Executor->>Executor: createLintTool(registry)
    
    Executor->>Model: executeAgent({lint, report_finding, finalize_review})
    
    rect rgba(100, 200, 255, 0.5)
    Note over Model,Tool: Tool Execution Loop
    Model->>Tool: lint({file, ruleSource})
    Tool->>Tool: registry.lookup(ruleSource)
    Tool-->>Model: LintToolResult
    Executor->>Store: append(finding_recorded_inline)
    Store-->>Executor: ✓
    end
    
    rect rgba(100, 255, 200, 0.5)
    Note over Model,Executor: Finalization
    Model->>Executor: finalize_review({totalFindings})
    Executor->>Store: append(session_finalized)
    Store-->>Executor: ✓
    end
    
    Executor->>Store: replay()
    Store-->>Executor: SessionEvent[]
    
    Executor->>Executor: buildAgentReplayReport()
    Executor-->>CLI: RunAgentExecutorResult
    
    CLI->>CLI: print report (if JSON format)
    CLI-->>User: EvaluationResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hops through sessions with glee,
Events logged in JSONL spree!
Lint tools spring, findings take flight,
Sessions replay through the night,
Agent mode hops into the light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: a redesign of agent mode with deterministic replay runtime capabilities.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agentic-review

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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 (10)
src/agent/session-replay.ts (2)

18-22: Duplicate implementation of replaySessionEvents.

This function is nearly identical to the replay() method in src/agent/review-session-store.ts (lines 57-61). Consider either:

  1. Exporting replay from the store and reusing it here, or
  2. Extracting a shared parsing utility.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/session-replay.ts` around lines 18 - 22, The function
replaySessionEvents duplicates logic already present in
ReviewSessionStore.replay; to fix, either export and reuse the existing replay
method from src/agent/review-session-store.ts (call ReviewSessionStore.replay or
export a standalone replay function) or extract the JSON-lines-to-SessionEvent
parsing into a shared utility (e.g., parseSessionEventsFromFile) and call that
from both replaySessionEvents and ReviewSessionStore.replay; update imports and
remove the duplicate implementation so parsing lives in one place and both
callers delegate to it.

28-35: Unnecessary O(n²) complexity from per-event merge.

Calling mergeAgentFindings(findings, [event.payload.finding]) for each event recreates the fingerprint Set from scratch on every iteration. Since events in a single session file are already recorded in order and should not contain duplicates (the executor de-duplicates before appending), you could simplify to a direct push, or build the Set once outside the loop.

♻️ Simplified accumulation
 export async function replaySessionFindings(sessionFilePath: string): Promise<AgentFinding[]> {
   const events = await replaySessionEvents(sessionFilePath);
-  let findings: AgentFinding[] = [];
+  const findings: AgentFinding[] = [];
+  const seen = new Set<string>();

   for (const event of events) {
     if (
       event.eventType === "finding_recorded_inline" ||
       event.eventType === "finding_recorded_top_level"
     ) {
-      findings = mergeAgentFindings(findings, [event.payload.finding]);
+      const finding = event.payload.finding;
+      const key = JSON.stringify([finding.kind, finding.ruleId, finding.ruleSource, finding.file ?? "", finding.line ?? null, finding.message]);
+      if (!seen.has(key)) {
+        seen.add(key);
+        findings.push(finding);
+      }
     }
   }

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

In `@src/agent/session-replay.ts` around lines 28 - 35, The loop is causing O(n²)
work by calling mergeAgentFindings(findings, [event.payload.finding]) for every
event; instead, accumulate findings directly (e.g., push each
event.payload.finding into the findings array or collect into a local array or
Set) and call mergeAgentFindings once (or build the fingerprint Set once) after
the loop; update the loop over events to reference event.payload.finding and
stop calling mergeAgentFindings per-iteration to avoid reconstructing the
fingerprint Set repeatedly.
src/agent/review-session-store.ts (2)

82-85: hasFinalizedEvent() re-reads entire file on each call.

This method replays all events just to check if any is session_finalized. For long sessions with many events, this becomes increasingly expensive. If this is called frequently (e.g., in a polling loop), consider caching the finalized state after the first true result, since finalization is a terminal state.

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

In `@src/agent/review-session-store.ts` around lines 82 - 85, hasFinalizedEvent
currently calls replay() and scans all events each time; change it to memoize
the terminal finalized state so repeated calls don't re-read the whole event
stream. Add a private boolean (e.g., _isFinalized) initialized false on the
store, update it to true whenever replay() finds event.eventType ===
'session_finalized' or when any code path that emits finalization runs, and have
hasFinalizedEvent return the cached _isFinalized immediately if true; only call
replay() if _isFinalized is false and then set the cache when a finalized event
is observed. Ensure you reference hasFinalizedEvent and replay in the change and
keep cache updates atomic/consistent with any write/finalize methods.

53-56: Minor TOCTOU window between file creation and first append.

Opening with 'wx' then immediately closing creates a brief window where another process could theoretically write to the file before the first append(). In practice, the unique sessionId makes this extremely unlikely. If stronger guarantees are needed, consider keeping the file handle open for the session lifetime.

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

In `@src/agent/review-session-store.ts` around lines 53 - 56, The current pattern
opens the file with open(sessionFilePath, 'wx') then immediately calls
handle.close(), creating a TOCTOU window before the first append; instead, keep
the file handle open for the session lifetime and use it for subsequent writes
to avoid the race. Update the code around open(sessionFilePath,
'wx')/handle.close() in review-session-store.ts to store the returned handle
(e.g., in a map keyed by sessionId or as part of the session object), perform
writes against that handle (using handle.write or handle.appendFile) for
subsequent append() operations, and ensure the handle is closed when the session
ends or is cleaned up. Ensure append() uses the kept handle rather than
re-opening the file so there is no window between creation and first write.
src/agent/merger.ts (1)

3-11: Consider using a delimiter-free serialization for fingerprint.

The :: delimiter in findingFingerprint could cause false-positive collisions if any field (particularly message) contains the literal string ::. For example, two different findings could produce the same fingerprint if one field's value contains :: that aligns with adjacent field boundaries.

A safer approach would use JSON serialization or a delimiter that's escaped/unlikely in content.

♻️ Optional: Use JSON for collision-safe fingerprinting
 function findingFingerprint(finding: AgentFinding): string {
-  return [
-    finding.kind,
-    finding.ruleId,
-    finding.ruleSource,
-    finding.file ?? "",
-    String(finding.line ?? ""),
-    finding.message,
-  ].join("::");
+  return JSON.stringify([
+    finding.kind,
+    finding.ruleId,
+    finding.ruleSource,
+    finding.file ?? "",
+    finding.line ?? null,
+    finding.message,
+  ]);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/merger.ts` around lines 3 - 11, The current findingFingerprint
function uses "::" delimiter which can collide when field values contain that
sequence; change findingFingerprint to produce a collision-safe serialization
(e.g., use JSON.stringify on an object with stable keys or encode/escape each
field) so the fingerprint is unambiguous; update the function findingFingerprint
to serialize { kind, ruleId, ruleSource, file, line, message } with
JSON.stringify (or an escape scheme) to ensure deterministic ordering and avoid
delimiter collisions while keeping the same return type (string).
src/agent/types.ts (1)

102-109: Duplicate toPascalCase implementation.

This toPascalCase function is identical to the one in src/prompts/rule-identity.ts (lines 4-11). Consider extracting it to a shared utility module to avoid duplication and ensure consistent behavior across the codebase.

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

In `@src/agent/types.ts` around lines 102 - 109, The toPascalCase function is
duplicated; extract it into a single shared utility (e.g., export a function
named toPascalCase from a new/shared utils module), replace the local
implementations in the agent types and the rule-identity usage with an import of
that exported toPascalCase, remove the duplicate definitions, and run/typecheck
to ensure no signature or behavior changes; reference the existing symbol name
toPascalCase and update any imports that previously referenced the local
implementations.
tests/orchestrator-agent-output.test.ts (1)

232-237: Consider extracting progress format constants to reduce test brittleness.

The exact substring assertions (e.g., '⠋ ◈ reviewing', '└ calling tool lint tool lint') are tightly coupled to the progress reporter's output format. If the progress messages change, these tests will fail even though the functionality is correct.

Consider either:

  1. Extracting progress format strings to shared constants used by both the reporter and tests
  2. Using regex patterns that match the semantic structure rather than exact formatting

This is a minor maintainability concern and can be addressed later.

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

In `@tests/orchestrator-agent-output.test.ts` around lines 232 - 237, Tests assert
exact progress strings from stderrOutput (e.g., the REVIEW and LINT messages
using README_PATH), which is brittle; change the test to either reference shared
progress format constants defined in the progress reporter module or relax
assertions to semantic regexes: replace the exact substring checks on
stderrOutput with patterns that match the semantic pieces (e.g., presence of
"reviewing", the README_PATH, "calling tool lint", and "done" timestamps) or
import constants exported by the progress reporter (progress symbols/format
strings) and use those constants in the assertions so the test uses the
canonical format instead of hardcoded literals.
src/agent/agent-executor.ts (1)

84-91: Redundant initialization check for fileRuleMap entries.

Lines 65-67 already initialize fileRuleMap[file] = [] for all targets, so the existence check at lines 85-89 will never create new entries. The code works correctly but the conditional is dead logic.

♻️ Simplified version
     for (const file of targets) {
-      let rulesForFile = fileRuleMap[file];
-      if (!rulesForFile) {
-        rulesForFile = [];
-        fileRuleMap[file] = rulesForFile;
-      }
-      rulesForFile.push(ruleSource);
+      fileRuleMap[file].push(ruleSource);
     }
🤖 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 84 - 91, The loop in
agent-executor.ts redundantly checks/initializes entries in fileRuleMap even
though earlier code already set fileRuleMap[file] = [] for all targets; remove
the dead conditional in the loop and directly push ruleSource into the existing
array. Locate the for (const file of targets) loop that references fileRuleMap,
rulesForFile and ruleSource, and replace the conditional block that creates
rulesForFile with a straight rulesForFile = fileRuleMap[file];
rulesForFile.push(ruleSource) usage (or directly
fileRuleMap[file].push(ruleSource)) to simplify and eliminate dead logic.
src/agent/index.ts (1)

40-45: Unsafe type assertions bypass type checking.

The as RunAgentExecutorParams['runRule'] and as RunAgentExecutorParams['executeAgent'] casts silence type mismatches between AgentExecutionOptions (which uses unknown parameters in src/cli/types.ts) and RunAgentExecutorParams (which expects specific typed parameters). If the signatures diverge, these casts will mask runtime errors.

Consider either:

  1. Aligning the types in AgentExecutionOptions with RunAgentExecutorParams
  2. Adding runtime validation before the cast
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/index.ts` around lines 40 - 45, The current unsafe casts on
options.agent?.runRule and options.agent?.execute bypass TypeScript checks
(using as RunAgentExecutorParams['runRule'] / ['executeAgent']); either align
AgentExecutionOptions to use the exact RunAgentExecutorParams types or add
runtime validation before converting: implement validation helpers (e.g.,
validateRunRuleParams and validateExecuteAgentParams) that assert the
shape/types expected by RunAgentExecutorParams, call them where
options.agent?.runRule and options.agent?.execute are read, and only include the
properties in the spread if validation passes (otherwise log/throw);
alternatively, update the AgentExecutionOptions definitions to use the exact
RunAgentExecutorParams types so the casts are unnecessary.
docs/logs/2026-03-31-agent-mode-implementation-plan.log.md (1)

19-19: Minor grammar: hyphenate compound adjective.

"rule-source based" should be "rule-source-based" when used as a compound adjective before "tool schemas".

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

In `@docs/logs/2026-03-31-agent-mode-implementation-plan.log.md` at line 19,
Update the compound adjective "rule-source based" to the hyphenated form
"rule-source-based" in the sentence that reads "Introduced rule-source based
tool schemas, canonical `Pack.Rule` normalization, and a typed session event
union including explicit finalization events for deterministic replay
contracts."; replace "rule-source based" with "rule-source-based" to correct the
grammar while leaving the rest of the sentence and the `Pack.Rule` reference
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/index.ts`:
- Around line 49-61: The returned report currently hardcodes hadSeverityErrors
to false, preventing agent mode from exiting with a non-zero code when replay
findings have error severity; update the value of hadSeverityErrors in the
object returned by the function to derive from the replayReport (for example set
hadSeverityErrors to a boolean based on replayReport.summary.errors > 0 or the
appropriate severity count) so it reflects actual error-severity findings from
replayReport.summary; keep existing fields (totalFiles, totalErrors, etc.)
unchanged and only replace the hardcoded false with the computed boolean.

In `@src/agent/types.ts`:
- Around line 111-117: Extract the duplicated toPascalCase implementation into a
single shared utility and update callers: create a new exported function (e.g.,
toPascalCase) in a common module and import it into the files that currently
define it, then remove the duplicate implementations from both the file that
defines canonicalRuleIdFromSource and from rule-identity usage; ensure
canonicalRuleIdFromSource continues to call the shared toPascalCase and
run/update its tests, and while here evaluate whether canonicalRuleIdFromSource
is referenced anywhere in production code and, if not needed, consider
consolidating callers to use canonicalRuleIdFromPackRule instead and remove the
unused function.

In `@src/output/agent-progress-reporter.ts`:
- Around line 13-16: The compactRulePreview function always appends "..." even
when ruleSource is shorter than or equal to 48 characters; update
compactRulePreview so it only appends "..." when truncation occurs (i.e., when
ruleSource.length > 48). Locate the compactRulePreview function and change the
return logic to return the original ruleSource unchanged for short inputs, and
return the truncated slice plus "..." only when the input was actually
truncated.
- Line 53: The output string in the progress reporter duplicates the word "tool"
(see the call to options.write in agent-progress-reporter.ts); update the
message in that write call to remove the repeated word so it reads like "└
calling tool lint..." (or "└ calling lint tool..." depending on intended
phrasing) and keep the existing suffix handling intact; modify the string passed
to options.write in the function that writes the "calling tool" line to use a
single "tool" occurrence.

---

Nitpick comments:
In `@docs/logs/2026-03-31-agent-mode-implementation-plan.log.md`:
- Line 19: Update the compound adjective "rule-source based" to the hyphenated
form "rule-source-based" in the sentence that reads "Introduced rule-source
based tool schemas, canonical `Pack.Rule` normalization, and a typed session
event union including explicit finalization events for deterministic replay
contracts."; replace "rule-source based" with "rule-source-based" to correct the
grammar while leaving the rest of the sentence and the `Pack.Rule` reference
unchanged.

In `@src/agent/agent-executor.ts`:
- Around line 84-91: The loop in agent-executor.ts redundantly
checks/initializes entries in fileRuleMap even though earlier code already set
fileRuleMap[file] = [] for all targets; remove the dead conditional in the loop
and directly push ruleSource into the existing array. Locate the for (const file
of targets) loop that references fileRuleMap, rulesForFile and ruleSource, and
replace the conditional block that creates rulesForFile with a straight
rulesForFile = fileRuleMap[file]; rulesForFile.push(ruleSource) usage (or
directly fileRuleMap[file].push(ruleSource)) to simplify and eliminate dead
logic.

In `@src/agent/index.ts`:
- Around line 40-45: The current unsafe casts on options.agent?.runRule and
options.agent?.execute bypass TypeScript checks (using as
RunAgentExecutorParams['runRule'] / ['executeAgent']); either align
AgentExecutionOptions to use the exact RunAgentExecutorParams types or add
runtime validation before converting: implement validation helpers (e.g.,
validateRunRuleParams and validateExecuteAgentParams) that assert the
shape/types expected by RunAgentExecutorParams, call them where
options.agent?.runRule and options.agent?.execute are read, and only include the
properties in the spread if validation passes (otherwise log/throw);
alternatively, update the AgentExecutionOptions definitions to use the exact
RunAgentExecutorParams types so the casts are unnecessary.

In `@src/agent/merger.ts`:
- Around line 3-11: The current findingFingerprint function uses "::" delimiter
which can collide when field values contain that sequence; change
findingFingerprint to produce a collision-safe serialization (e.g., use
JSON.stringify on an object with stable keys or encode/escape each field) so the
fingerprint is unambiguous; update the function findingFingerprint to serialize
{ kind, ruleId, ruleSource, file, line, message } with JSON.stringify (or an
escape scheme) to ensure deterministic ordering and avoid delimiter collisions
while keeping the same return type (string).

In `@src/agent/review-session-store.ts`:
- Around line 82-85: hasFinalizedEvent currently calls replay() and scans all
events each time; change it to memoize the terminal finalized state so repeated
calls don't re-read the whole event stream. Add a private boolean (e.g.,
_isFinalized) initialized false on the store, update it to true whenever
replay() finds event.eventType === 'session_finalized' or when any code path
that emits finalization runs, and have hasFinalizedEvent return the cached
_isFinalized immediately if true; only call replay() if _isFinalized is false
and then set the cache when a finalized event is observed. Ensure you reference
hasFinalizedEvent and replay in the change and keep cache updates
atomic/consistent with any write/finalize methods.
- Around line 53-56: The current pattern opens the file with
open(sessionFilePath, 'wx') then immediately calls handle.close(), creating a
TOCTOU window before the first append; instead, keep the file handle open for
the session lifetime and use it for subsequent writes to avoid the race. Update
the code around open(sessionFilePath, 'wx')/handle.close() in
review-session-store.ts to store the returned handle (e.g., in a map keyed by
sessionId or as part of the session object), perform writes against that handle
(using handle.write or handle.appendFile) for subsequent append() operations,
and ensure the handle is closed when the session ends or is cleaned up. Ensure
append() uses the kept handle rather than re-opening the file so there is no
window between creation and first write.

In `@src/agent/session-replay.ts`:
- Around line 18-22: The function replaySessionEvents duplicates logic already
present in ReviewSessionStore.replay; to fix, either export and reuse the
existing replay method from src/agent/review-session-store.ts (call
ReviewSessionStore.replay or export a standalone replay function) or extract the
JSON-lines-to-SessionEvent parsing into a shared utility (e.g.,
parseSessionEventsFromFile) and call that from both replaySessionEvents and
ReviewSessionStore.replay; update imports and remove the duplicate
implementation so parsing lives in one place and both callers delegate to it.
- Around line 28-35: The loop is causing O(n²) work by calling
mergeAgentFindings(findings, [event.payload.finding]) for every event; instead,
accumulate findings directly (e.g., push each event.payload.finding into the
findings array or collect into a local array or Set) and call mergeAgentFindings
once (or build the fingerprint Set once) after the loop; update the loop over
events to reference event.payload.finding and stop calling mergeAgentFindings
per-iteration to avoid reconstructing the fingerprint Set repeatedly.

In `@src/agent/types.ts`:
- Around line 102-109: The toPascalCase function is duplicated; extract it into
a single shared utility (e.g., export a function named toPascalCase from a
new/shared utils module), replace the local implementations in the agent types
and the rule-identity usage with an import of that exported toPascalCase, remove
the duplicate definitions, and run/typecheck to ensure no signature or behavior
changes; reference the existing symbol name toPascalCase and update any imports
that previously referenced the local implementations.

In `@tests/orchestrator-agent-output.test.ts`:
- Around line 232-237: Tests assert exact progress strings from stderrOutput
(e.g., the REVIEW and LINT messages using README_PATH), which is brittle; change
the test to either reference shared progress format constants defined in the
progress reporter module or relax assertions to semantic regexes: replace the
exact substring checks on stderrOutput with patterns that match the semantic
pieces (e.g., presence of "reviewing", the README_PATH, "calling tool lint", and
"done" timestamps) or import constants exported by the progress reporter
(progress symbols/format strings) and use those constants in the assertions so
the test uses the canonical format instead of hardcoded literals.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9d47c1f-8bf3-4526-9c01-c3111bd66c13

📥 Commits

Reviewing files that changed from the base of the PR and between 27097c3 and 6ec4b30.

📒 Files selected for processing (21)
  • docs/logs/2026-03-31-agent-mode-implementation-plan.log.md
  • src/agent/agent-executor.ts
  • src/agent/index.ts
  • src/agent/merger.ts
  • src/agent/review-session-store.ts
  • src/agent/session-replay.ts
  • src/agent/tools/index.ts
  • src/agent/tools/lint-tool.ts
  • src/agent/types.ts
  • src/cli/commands.ts
  • src/cli/orchestrator.ts
  • src/cli/types.ts
  • src/output/agent-progress-reporter.ts
  • src/prompts/rule-identity.ts
  • src/schemas/cli-schemas.ts
  • tests/agent/agent-executor.test.ts
  • tests/agent/bootstrap.test.ts
  • tests/agent/lint-tool.test.ts
  • tests/agent/review-session-store.test.ts
  • tests/agent/types-redesign.test.ts
  • tests/orchestrator-agent-output.test.ts

Comment on lines +49 to +61
return {
totalFiles: targets.length,
totalErrors: replayReport.summary.errors,
totalWarnings: replayReport.summary.warnings,
requestFailures: 0,
hadOperationalErrors: Boolean(runResult.error),
hadSeverityErrors: false,
tokenUsage: {
totalInputTokens: 0,
totalOutputTokens: 0,
},
agentReport: replayReport,
};
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 | 🟠 Major

hadSeverityErrors is hardcoded to false, bypassing severity-based exit codes.

Per src/cli/commands.ts:241, the CLI exits with code 1 when hadSeverityErrors is true. With this always set to false, agent mode will never exit non-zero based on finding severity, unlike lint mode. If the replay report contains error-severity findings, this should likely derive from replayReport.summary.errors > 0.

🔧 Proposed fix
   return {
     totalFiles: targets.length,
     totalErrors: replayReport.summary.errors,
     totalWarnings: replayReport.summary.warnings,
     requestFailures: 0,
     hadOperationalErrors: Boolean(runResult.error),
-    hadSeverityErrors: false,
+    hadSeverityErrors: replayReport.summary.errors > 0,
     tokenUsage: {
       totalInputTokens: 0,
       totalOutputTokens: 0,
     },
     agentReport: replayReport,
   };
📝 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
return {
totalFiles: targets.length,
totalErrors: replayReport.summary.errors,
totalWarnings: replayReport.summary.warnings,
requestFailures: 0,
hadOperationalErrors: Boolean(runResult.error),
hadSeverityErrors: false,
tokenUsage: {
totalInputTokens: 0,
totalOutputTokens: 0,
},
agentReport: replayReport,
};
return {
totalFiles: targets.length,
totalErrors: replayReport.summary.errors,
totalWarnings: replayReport.summary.warnings,
requestFailures: 0,
hadOperationalErrors: Boolean(runResult.error),
hadSeverityErrors: replayReport.summary.errors > 0,
tokenUsage: {
totalInputTokens: 0,
totalOutputTokens: 0,
},
agentReport: replayReport,
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/index.ts` around lines 49 - 61, The returned report currently
hardcodes hadSeverityErrors to false, preventing agent mode from exiting with a
non-zero code when replay findings have error severity; update the value of
hadSeverityErrors in the object returned by the function to derive from the
replayReport (for example set hadSeverityErrors to a boolean based on
replayReport.summary.errors > 0 or the appropriate severity count) so it
reflects actual error-severity findings from replayReport.summary; keep existing
fields (totalFiles, totalErrors, etc.) unchanged and only replace the hardcoded
false with the computed boolean.

Comment on lines +111 to +117
export function canonicalRuleIdFromSource(ruleSource: string): string {
const normalized = ruleSource.replace(/\\/g, "/");
const parts = normalized.split("/").filter(Boolean);
const ruleName = toPascalCase(parts.at(-1) ?? "rule");
const packName = toPascalCase(parts.at(-2) ?? "default");
return `${packName}.${ruleName}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usages of both functions to verify consistent application
echo "=== canonicalRuleIdFromSource usages ==="
rg -n 'canonicalRuleIdFromSource' --type=ts

echo -e "\n=== canonicalRuleIdFromPackRule usages ==="
rg -n 'canonicalRuleIdFromPackRule' --type=ts

Repository: TRocket-Labs/vectorlint

Length of output: 769


🏁 Script executed:

#!/bin/bash
# Get the implementation of canonicalRuleIdFromPackRule
echo "=== canonicalRuleIdFromPackRule implementation ===" 
sed -n '13,25p' src/prompts/rule-identity.ts

echo -e "\n=== toPascalCase function ===" 
rg -A 5 'function toPascalCase\|export.*toPascalCase' --type=ts

echo -e "\n=== canonicalRuleIdFromPackRule call site ===" 
sed -n '65,80p' src/agent/agent-executor.ts

echo -e "\n=== Test case for canonicalRuleIdFromSource ===" 
sed -n '15,20p' tests/agent/types-redesign.test.ts

Repository: TRocket-Labs/vectorlint

Length of output: 1358


🏁 Script executed:

#!/bin/bash
# Search for toPascalCase definition
echo "=== Finding toPascalCase definition ===" 
rg -B 2 -A 10 'toPascalCase' --type=ts | head -50

echo -e "\n=== Check where toPascalCase is imported from in types.ts ===" 
head -20 src/agent/types.ts

echo -e "\n=== Check where toPascalCase is imported from in rule-identity.ts ===" 
head -20 src/prompts/rule-identity.ts

Repository: TRocket-Labs/vectorlint

Length of output: 3801


Extract the duplicated toPascalCase function into a shared module.

The same toPascalCase implementation appears in both src/agent/types.ts and src/prompts/rule-identity.ts. This violates the DRY principle and creates maintenance risk if the implementations diverge. Move this utility to a shared location (e.g., src/utils/pascal-case.ts or similar) and import it in both files.

Additionally, canonicalRuleIdFromSource is defined and tested but not used anywhere in production code—consider whether it is actually needed or if canonicalRuleIdFromPackRule should be the canonical approach throughout the codebase.

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

In `@src/agent/types.ts` around lines 111 - 117, Extract the duplicated
toPascalCase implementation into a single shared utility and update callers:
create a new exported function (e.g., toPascalCase) in a common module and
import it into the files that currently define it, then remove the duplicate
implementations from both the file that defines canonicalRuleIdFromSource and
from rule-identity usage; ensure canonicalRuleIdFromSource continues to call the
shared toPascalCase and run/update its tests, and while here evaluate whether
canonicalRuleIdFromSource is referenced anywhere in production code and, if not
needed, consider consolidating callers to use canonicalRuleIdFromPackRule
instead and remove the unused function.

Comment on lines +13 to +16
function compactRulePreview(ruleSource: string): string {
const compact = ruleSource.length > 48 ? `${ruleSource.slice(0, 48)}` : ruleSource;
return `${compact}...`;
}
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

compactRulePreview unconditionally appends "..." even for short strings.

When ruleSource.length <= 48, the string is not truncated, yet "..." is still appended, which incorrectly implies truncation occurred.

🔧 Proposed fix
 function compactRulePreview(ruleSource: string): string {
-  const compact = ruleSource.length > 48 ? `${ruleSource.slice(0, 48)}` : ruleSource;
-  return `${compact}...`;
+  if (ruleSource.length > 48) {
+    return `${ruleSource.slice(0, 48)}...`;
+  }
+  return ruleSource;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/output/agent-progress-reporter.ts` around lines 13 - 16, The
compactRulePreview function always appends "..." even when ruleSource is shorter
than or equal to 48 characters; update compactRulePreview so it only appends
"..." when truncation occurs (i.e., when ruleSource.length > 48). Locate the
compactRulePreview function and change the return logic to return the original
ruleSource unchanged for short inputs, and return the truncated slice plus "..."
only when the input was actually truncated.

) {
suffix = detail ? ` ${detail}` : "";
}
options.write(`└ calling tool ${toolName} tool${suffix}\n`);
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

Typo: "tool" appears twice in output string.

The output reads "calling tool lint tool..." which is grammatically incorrect.

🐛 Proposed fix
-      options.write(`└ calling tool ${toolName} tool${suffix}\n`);
+      options.write(`└ calling tool ${toolName}${suffix}\n`);
📝 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
options.write(`└ calling tool ${toolName} tool${suffix}\n`);
options.write(`└ calling tool ${toolName}${suffix}\n`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/output/agent-progress-reporter.ts` at line 53, The output string in the
progress reporter duplicates the word "tool" (see the call to options.write in
agent-progress-reporter.ts); update the message in that write call to remove the
repeated word so it reads like "└ calling tool lint..." (or "└ calling lint
tool..." depending on intended phrasing) and keep the existing suffix handling
intact; modify the string passed to options.write in the function that writes
the "calling tool" line to use a single "tool" occurrence.

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