š§ Upcoming Feature
This design document describes a feature currently in planning. CLI Local LLM Processing will enable checks without sending diffs to the Threadline web application, with optional syncing of results for analytics and collaboration.
CLI Local LLM Processing
Summary
CLI calls LLM directly, so code diffs don't have to go to Threadline. You can still optionally send diffs and check results to Threadline, thereby benefiting from convenience and analysis capabilities. When syncing, the full request (diffs + threadlines + results) is sent to the web app, which stores everything but skips LLM processing since it's already done.
Problem It Solves
This creates greater flexibility on which LLM provider and models to use, makes the user directly responsible for LLM costs, and allows you to opt out of sending code diffs up to Threadline's servers.
How It Works
Current Flow
CLI (check.ts) ā Collects: git context, diffs, threadlines ā POST /api/threadline-check ā Web App (route.ts) ā processThreadlines() ā calls OpenAI API ā storeCheck() ā saves to database ā Return results ā CLI ā displayResults()
Currently, the CLI sends all threadline data, diffs, and context files to the web app. The web app processes everything using OpenAI, stores results, and returns them to the CLI.
New Flow
CLI (check.ts) ā Collects: git context, diffs, threadlines ā processThreadlines() ā calls OpenAI API locally ā displayResults() ā [STOP HERE - Local only] OR ā [Optional] POST /api/threadline-check-results ā Send: diffs + threadlines + results ā Web App (new route.ts) ā storeCheck() ā saves to database (Skips LLM processing - already done)
With local processing, the CLI handles all LLM calls directly. Results are displayed immediately. The process can stop here for local-only usage. Optionally, the full request (diffs + threadlines + results) can be synced to the web app for storage and analytics. The web app stores everything but skips LLM processing since it's already complete.
Detailed Analysis of Current Implementation
1. Request Object Structure
The CLI sends a ReviewRequest object via POST /api/threadline-check:
ReviewRequest {
threadlines: Array<{
id: string; // Threadline identifier
version: string; // Version string from file
patterns: string[]; // File patterns (e.g., ["**/*.ts"])
content: string; // Threadline guidelines text
filePath: string; // Path to threadline file
contextFiles?: string[]; // Optional: paths to context files
contextContent?: { // Optional: full content of context files
[filePath: string]: string;
};
}>;
diff: string; // Full git diff (unified format)
files: string[]; // List of changed file paths
apiKey: string; // Threadline API key (for auth)
account: string; // Account identifier (email)
repoName?: string; // Git remote URL
branchName?: string; // Branch name
commitSha?: string; // Commit SHA
commitMessage?: string; // Commit message
commitAuthorName?: string; // Author name
commitAuthorEmail?: string;// Author email
prTitle?: string; // PR/MR title
environment?: string; // 'github' | 'gitlab' | 'vercel' | 'local'
cliVersion?: string; // CLI version
reviewContext: 'local' | 'commit' | 'pr' | 'file' | 'folder' | 'files';
}Key Points:
- CLI reads threadline files and context files from disk, includes full content
- CLI generates git diff using git commands (varies by context: commit, PR, file, etc.)
- All metadata (repo, branch, commit, author) is collected by CLI before sending
- Full diff is sent (can be large with -U200 context lines)
2. Endpoint Processing Flow
POST /api/threadline-check (route.ts)
ā
āā Step 1: Parse & Validate Request
ā āā Validate threadlines array (required, non-empty)
ā āā Validate filePath on each threadline (required)
ā āā Validate diff (must be string, empty allowed)
ā āā Validate reviewContext (must be valid enum)
ā āā Validate apiKey & account (required)
ā
āā Step 2: Calculate Statistics (for audit)
ā āā countLinesInDiff(diff) ā {added, removed, total}
ā āā calculateContextStats(threadlines) ā {fileCount, totalLines, files}
ā āā Log audit statistics
ā
āā Step 3: Early Return for Zero Diffs
ā āā If diff.trim() === '':
ā āā Return all threadlines as 'not_relevant' (no LLM calls)
ā
āā Step 4: Authentication
ā āā Look up account in database by identifier
ā āā Compare apiKey (plaintext comparison)
ā āā Get accountId and userId
ā āā Fallback to env vars (backward compatibility)
ā
āā Step 5: Get OpenAI API Key
ā āā Read OPENAI_API_KEY from server environment
ā
āā Step 6: Process Threadlines (LLM Calls)
ā āā processThreadlines({...request, apiKey: openaiApiKey})
ā ā
ā āā For each threadline (parallel):
ā ā āā processThreadline(threadline, diff, files, apiKey)
ā ā ā
ā ā āā Filter files matching patterns
ā ā ā āā If no matches ā return 'not_relevant'
ā ā ā
ā ā āā Filter diff to relevant files
ā ā ā āā filterDiffByFiles(diff, relevantFiles)
ā ā ā
ā ā āā Extract files from filtered diff
ā ā ā āā extractFilesFromDiff(filteredDiff)
ā ā ā
ā ā āā Trim diff for LLM (reduce tokens)
ā ā ā āā createSlimDiff(filteredDiff, contextLines)
ā ā ā (Default: 10 context lines, configurable)
ā ā ā
ā ā āā Build prompt
ā ā ā āā buildPrompt(threadline, trimmedDiff, filesInDiff)
ā ā ā āā Includes threadline content
ā ā ā āā Includes context files content
ā ā ā āā Includes trimmed diff
ā ā ā āā Includes changed files list
ā ā ā
ā ā āā Call OpenAI API
ā ā ā āā openai.chat.completions.create({
ā ā ā model: 'gpt-5.2',
ā ā ā messages: [system, user],
ā ā ā response_format: {type: 'json_object'},
ā ā ā temperature: 0.1
ā ā ā })
ā ā ā
ā ā āā Return ProcessThreadlineResult
ā ā āā status: 'compliant' | 'attention' | 'not_relevant' | 'error'
ā ā āā reasoning: string
ā ā āā fileReferences: string[]
ā ā āā relevantFiles: string[]
ā ā āā filteredDiff: string (full filtered diff, not trimmed)
ā ā āā filesInFilteredDiff: string[]
ā ā āā llmCallMetrics: {...}
ā ā
ā āā Return ProcessThreadlinesResponse
ā āā results: ProcessThreadlineResult[]
ā āā metadata: {totalThreadlines, completed, timedOut, errors, llmModel}
ā
āā Step 7: Store Check in Database
ā āā storeCheck({request, result, diffStats, contextStats, ...})
ā ā
ā āā Begin Transaction
ā ā
ā āā Insert check record
ā ā āā INSERT INTO checks (repo_name, branch_name, commit_sha, ...)
ā ā
ā āā Insert diff content
ā ā āā INSERT INTO check_diffs (check_id, diff_content, diff_format)
ā ā
ā āā For each threadline:
ā ā āā Generate hashes
ā ā ā āā versionHash = generateVersionHash({
ā ā ā ā threadlineId, filePath, patterns, content,
ā ā ā ā version, repoName, accountId
ā ā ā ā })
ā ā ā āā identityHash = generateIdentityHash({
ā ā ā threadlineId, filePath, repoName, accountId
ā ā ā })
ā ā ā
ā ā āā Check if version_hash exists
ā ā ā āā If yes ā reuse threadline_definition_id
ā ā ā āā If no ā check identity_hash for predecessor
ā ā ā āā Insert new threadline_definition
ā ā ā
ā ā āā Process context files
ā ā ā āā For each context file:
ā ā ā ā āā contextHash = generateContextHash({
ā ā ā ā ā accountId, repoName, filePath, content
ā ā ā ā ā })
ā ā ā ā āā Check if content_hash exists
ā ā ā ā āā Reuse or create context_file_snapshot
ā ā ā āā Collect snapshot IDs
ā ā ā
ā ā āā Insert check_threadlines
ā ā āā INSERT INTO check_threadlines (
ā ā threadline_definition_id,
ā ā context_snapshot_ids,
ā ā relevant_files,
ā ā filtered_diff,
ā ā files_in_filtered_diff
ā ā )
ā ā
ā āā Insert check_results
ā ā āā INSERT INTO check_results (
ā ā status, reasoning, file_references
ā ā )
ā ā
ā āā Commit Transaction
ā
āā Step 8: Log Metrics (non-blocking)
ā āā Log LLM call metrics for each threadline
ā āā Log check summary metrics
ā
āā Step 9: Return Response
āā NextResponse.json(result)3. What Needs to Move vs. Stay
ā Move to CLI (Required for Local Processing)
processThreadlines()- Main orchestration with parallel executionprocessThreadline()- Single threadline processing with OpenAI APIbuildPrompt()- Prompt constructionfilterDiffByFiles()- Filter diff by threadline patternsextractFilesFromDiff()- Extract file list from diffcreateSlimDiff()- Trim diff for LLM (token reduction)- OpenAI SDK integration
- Timeout handling (40s per threadline)
- Parallel processing logic
ā Stay in Web App (Storage & Analytics Only)
storeCheck()- Database storage logic- Hash calculations (
generateVersionHash,generateIdentityHash,generateContextHash) - Only done on server for analysis purposes, after LLM results are received - Authentication logic (apiKey validation, account lookup)
- Metrics logging (LLM call metrics, check summary metrics)
- Audit statistics calculation (for logging, not needed for processing)
4. Data Flow Summary
CLI ā Web App (Current): āā Sends: threadlines[], diff, files[], metadata āā Web App: Processes with LLM āā Web App: Stores in DB (with hashes) āā Returns: results[], metadata CLI ā Web App (New - Sync): āā Sends: threadlines[], diff, files[], metadata, results[], metadata āā Web App: Skips LLM (already processed) āā Web App: Stores in DB (with hashes) āā Returns: success/error Key Insight: - Web app still needs full diff for UI diff viewer - Web app still needs results for storage - Web app still generates hashes for deduplication - Only LLM processing moves to CLI
Implementation Plan
Prerequisite: Storage function storeCheckAndMetrics() exists at app/lib/audit/store-check-and-metrics.ts. Both paths (web app LLM, CLI sync) use this function with the same ProcessThreadlinesResponse interface.
Next: Add Sync Endpoint
Create POST /api/threadline-check-results ā same as current endpoint minus LLM processing.
// CLI sends: ReviewRequest + results + metadata
{
...reviewRequest, // Same as today
results: ProcessThreadlineResult[], // Already processed
metadata: { totalThreadlines, completed, timedOut, errors, llmModel }
}
// Endpoint: validate ā auth ā storeCheckAndMetrics() ā return successThen: Port Processing to CLI
Port processThreadlines(), buildPrompt(), diff filtering to CLI. Returns same ProcessThreadlinesResponse.
// CLI: process locally, optionally sync
const result = await processThreadlines({...});
displayResults(result);
if (shouldSync) await client.syncResults({...reviewRequest, ...result});Changes
CLI Changes
New Files:
src/api/openai.ts- OpenAI client wrappersrc/processors/expert.ts- PortprocessThreadlinesfrom web appsrc/processors/single-expert.ts- PortprocessThreadlinefrom web appsrc/llm/prompt-builder.ts- PortbuildPromptfrom web app
Modified Files:
src/commands/check.ts- ReplaceReviewAPIClient.review()call with local processingsrc/api/client.ts- Add newsyncResults()method for optional web app sync
Key Implementation Details:
- CLI currently calls
ReviewAPIClient.review()at line 283 incheck.ts, sending full request including diffs - Replace with local
processThreadlines()that calls OpenAI directly - Port timeout logic (40s per threadline) and parallel processing from web app
- Port prompt building logic - includes threadline content, context files, diff, and changed files
- After local processing, optionally call new sync endpoint with full request (diffs + threadlines + results) - web app stores everything but skips LLM processing
Web App Changes
New Endpoint:
POST /api/threadline-check-results - Accepts full request (diffs + threadlines + results). Stores in database using existing storeCheck() function, but skips LLM processing since results are already provided.
Request Format:
{
// Full request (same as current /api/threadline-check)
threadlines: [...], // Threadline definitions
diff: string, // Full git diff
files: string[], // Changed files
results: ExpertResult[]; // Already processed results
metadata: {
totalThreadlines: number;
completed: number;
timedOut: number;
errors: number;
llmModel: string;
};
// Same metadata as current: repoName, branchName, commitSha, etc.
apiKey: string;
account: string;
}The web app needs the full diff for the UI diff viewer, analytics, and fix detection. It just skips calling the LLM since results are already provided.
Existing Endpoint:
Keep POST /api/threadline-check for backward compatibility. Can be deprecated later.
Configuration
Environment Variables:
OPENAI_API_KEY- Required for local processingTHREADLINE_SYNC-true|false- Control whether to sync results to web app
CLI Flags:
--no-sync- Skip syncing results to web app--sync- Explicitly enable syncing (default: enabled for backward compatibility)
Code to Port
From Web App to CLI:
app/lib/processors/expert.tsāsrc/processors/expert.ts- Main processing logic with parallel execution and timeout handlingapp/lib/processors/single-expert.tsāsrc/processors/single-expert.ts- Single threadline processing with OpenAI API callsapp/lib/llm/prompt-builder.tsāsrc/llm/prompt-builder.ts- Prompt construction logicapp/lib/utils/diff-filter.tsāsrc/utils/diff-filter.ts- Filter diffs by threadline patterns
Dependencies to Add:
openai- OpenAI SDK for Node.js