specvector 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specvector",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Context-aware AI code review using Model Context Protocol (MCP)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -280,19 +280,19 @@ export function getStrictnessModifier(strictness: SpecVectorConfig["strictness"]
280
280
  switch (strictness) {
281
281
  case "strict":
282
282
  return `Be VERY strict. Flag any potential issue, no matter how minor.
283
- Focus on: security vulnerabilities, performance issues, error handling, edge cases.
283
+ Focus on: security vulnerabilities, logic errors, boundary conditions, data flow issues, performance problems, error handling, edge cases.
284
284
  Do not approve code that could be improved.`;
285
285
 
286
286
  case "lenient":
287
287
  return `Be lenient and focus only on critical issues.
288
- Only flag: bugs that would cause runtime errors, security vulnerabilities, obvious mistakes.
289
- Skip: style issues, minor improvements, suggestions.
288
+ Only flag: bugs that would cause runtime errors, logic errors causing crashes or data corruption, security vulnerabilities, obvious mistakes.
289
+ Skip: style issues, minor improvements, suggestions, theoretical problems.
290
290
  Approve unless there are blocking issues.`;
291
291
 
292
292
  case "normal":
293
293
  default:
294
294
  return `Use balanced judgement. Flag important issues but don't be nitpicky.
295
- Focus on: bugs, security, performance, maintainability.
296
- Skip: purely stylistic preferences.`;
295
+ Focus on: logic errors that would cause real bugs in production, security, performance, maintainability.
296
+ Skip: purely stylistic preferences, theoretical issues you haven't verified.`;
297
297
  }
298
298
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { postPRComment } from "./github/comment";
9
9
  import { parseDiff, getDiffSummary } from "./review/diff-parser";
10
10
  import { formatReviewComment, formatReviewSummary } from "./review/formatter";
11
11
  import { runReview } from "./review/engine";
12
+ import { redactError } from "./utils/redact";
12
13
 
13
14
  const VERSION = "0.1.0";
14
15
 
@@ -132,7 +133,6 @@ async function handleReview(args: string[]): Promise<void> {
132
133
  displayAndPostReview(mockReview, prNumber, dryRun);
133
134
  } else {
134
135
  console.log("🤖 Running AI review...");
135
- console.log(" (this may take 15-30 seconds)");
136
136
  console.log("");
137
137
 
138
138
  // Get branch name for Linear context
@@ -242,6 +242,6 @@ function generateMockReview(filesReviewed: number): import("./types/review").Rev
242
242
 
243
243
  // Run the CLI
244
244
  main().catch((error) => {
245
- console.error("Fatal error:", error);
245
+ console.error("Fatal error:", redactError(error));
246
246
  process.exit(1);
247
247
  });
@@ -17,6 +17,7 @@ import type {
17
17
  MCPToolsListResult,
18
18
  MCPToolCallResult,
19
19
  } from "./types";
20
+ import { redactSecrets, buildSafeEnv } from "../utils/redact";
20
21
 
21
22
  // ============================================================================
22
23
  // Constants
@@ -73,7 +74,7 @@ export async function createMCPClient(
73
74
  let proc: Subprocess;
74
75
  try {
75
76
  proc = spawn([config.command, ...config.args], {
76
- env: { ...process.env, ...config.env },
77
+ env: buildSafeEnv(config.env as Record<string, string> | undefined),
77
78
  stdin: "pipe",
78
79
  stdout: "pipe",
79
80
  stderr: "pipe",
@@ -351,7 +352,7 @@ function startReadingStderr(state: MCPClientState): void {
351
352
  const text = decoder.decode(value, { stream: true });
352
353
  // Log stderr for debugging (could be made configurable)
353
354
  if (text.trim()) {
354
- console.error(`[MCP:${state.config.name}] ${text.trim()}`);
355
+ console.error(`[MCP:${state.config.name}] ${redactSecrets(text.trim())}`);
355
356
  }
356
357
  }
357
358
  } catch {
@@ -172,7 +172,7 @@ export async function runReview(
172
172
  /**
173
173
  * System prompt for the code review agent.
174
174
  */
175
- const REVIEW_SYSTEM_PROMPT = `You are a pragmatic code reviewer. Your job is to catch REAL problems, not nitpick.
175
+ export const REVIEW_SYSTEM_PROMPT = `You are a pragmatic code reviewer. Your job is to catch REAL problems, not nitpick.
176
176
 
177
177
  ## Tools Available
178
178
  - read_file: Read source files to understand context
@@ -181,21 +181,43 @@ const REVIEW_SYSTEM_PROMPT = `You are a pragmatic code reviewer. Your job is to
181
181
  - get_outline: Get functions/classes in a file (fast overview)
182
182
  - find_symbol: Find where a function or class is defined
183
183
 
184
+ ## Tool Use Strategy
185
+ Before flagging any issue, you MUST verify your understanding:
186
+ 1. **Read the full file** for any function being changed — don't judge from diff alone
187
+ 2. **Use find_symbol** to trace how changed functions are called by other code
188
+ 3. **Use grep** to find other usages of modified functions or variables
189
+ 4. **Only flag an issue if you have verified it** by reading the surrounding context
190
+
184
191
  ## What to Look For (in priority order)
185
192
  1. **CRITICAL**: Security vulnerabilities, data loss, crashes
186
193
  2. **HIGH**: Bugs that WILL break functionality in production
187
194
  3. **MEDIUM**: Significant code quality issues (not style nits)
188
195
 
196
+ ## Business Logic Patterns to Detect
197
+ Focus on real logic errors that cause incorrect behavior:
198
+ - **Off-by-one errors**: Wrong boundary conditions, < vs <=, array index issues
199
+ - **Null/undefined handling**: Missing null checks on values that can be null
200
+ - **Race conditions**: Shared state without synchronization, async ordering bugs
201
+ - **Incorrect boolean logic**: Inverted conditions, wrong operator (AND vs OR)
202
+ - **Missing error paths**: Happy-path-only code that ignores failure cases in data flows
203
+ - **Wrong operator**: Using = instead of ==, + instead of -, incorrect comparisons
204
+ - **State management bugs**: Mutating shared state, stale closures, incorrect resets
205
+ - **Type coercion issues**: Implicit conversions causing unexpected behavior
206
+
189
207
  ## What NOT to Flag
190
208
  - Style preferences or "I would do it differently"
191
209
  - Theoretical performance issues without evidence
192
210
  - Missing edge case tests for working code
193
211
  - "Could be refactored" suggestions
194
212
  - Code that works but isn't perfect
213
+ - Naming convention preferences
214
+ - Comment formatting or missing comments
215
+ - Import ordering or grouping
195
216
 
196
217
  ## Key Principle
197
218
  Most PRs should have 0-2 findings. If you're finding 5+ issues, you're being too picky.
198
219
  Only flag issues you'd actually block a PR for in a real code review.
220
+ Verify every finding with tool use before reporting it.
199
221
 
200
222
  ## Response Format
201
223
  SUMMARY: [1-2 sentences - is this code ready to merge?]
@@ -228,15 +250,15 @@ ${diff.length > 15000 ? "\n(diff truncated, use tools to read full files if need
228
250
 
229
251
  ## Instructions
230
252
  1. First, understand what the changes are doing
231
- 2. Use tools to explore related code if needed (find usages, read implementations)
232
- 3. Identify any issues with the changes
253
+ 2. Use tools to explore related code read full files, trace call chains, check usages
254
+ 3. For each potential issue, verify it by reading surrounding context before flagging
233
255
  4. Provide your review in the specified format`;
234
256
  }
235
257
 
236
258
  /**
237
259
  * Parse the agent's response into structured findings.
238
260
  */
239
- function parseReviewResponse(response: string, diffSummary: string, contextSources: ContextSource[] = []): ReviewResult {
261
+ export function parseReviewResponse(response: string, diffSummary: string, contextSources: ContextSource[] = []): ReviewResult {
240
262
  const findings: ReviewFinding[] = [];
241
263
 
242
264
  // Extract summary
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Secret redaction utilities.
3
+ *
4
+ * Prevents API keys, tokens, and other sensitive values from appearing
5
+ * in logs, error messages, or PR comments.
6
+ *
7
+ * Story 7.4: Security Enforcement (NFR4, NFR5)
8
+ */
9
+
10
+ /**
11
+ * Environment variable names that contain secrets.
12
+ * Values from these variables will be redacted from any output.
13
+ */
14
+ const SECRET_ENV_VARS = [
15
+ "OPENROUTER_API_KEY",
16
+ "LINEAR_API_TOKEN",
17
+ "LINEAR_API_KEY",
18
+ "GITHUB_TOKEN",
19
+ "GH_TOKEN",
20
+ ] as const;
21
+
22
+ /**
23
+ * Environment variable names that are safe to pass to MCP subprocesses.
24
+ * Only these variables (plus explicitly provided config.env) are forwarded.
25
+ */
26
+ export const MCP_SAFE_ENV_VARS = [
27
+ "PATH",
28
+ "HOME",
29
+ "USER",
30
+ "SHELL",
31
+ "TERM",
32
+ "LANG",
33
+ "LC_ALL",
34
+ "NODE_ENV",
35
+ "TMPDIR",
36
+ "XDG_RUNTIME_DIR",
37
+ // Node/Bun runtime
38
+ "NODE_PATH",
39
+ "BUN_INSTALL",
40
+ // npm/npx needs these to find packages
41
+ "npm_config_prefix",
42
+ "npm_config_cache",
43
+ "NVM_DIR",
44
+ "NVM_BIN",
45
+ "VOLTA_HOME",
46
+ ] as const;
47
+
48
+ /**
49
+ * Redact known secret values from a string.
50
+ *
51
+ * Scans the string for any values that match current environment variable
52
+ * secrets and replaces them with "[REDACTED]".
53
+ *
54
+ * @param text - The string to redact secrets from
55
+ * @returns The redacted string
56
+ */
57
+ export function redactSecrets(text: string): string {
58
+ let redacted = text;
59
+
60
+ for (const envVar of SECRET_ENV_VARS) {
61
+ const value = process.env[envVar];
62
+ if (value && value.length >= 4) {
63
+ // Replace all occurrences of the secret value
64
+ redacted = redacted.replaceAll(value, "[REDACTED]");
65
+ }
66
+ }
67
+
68
+ return redacted;
69
+ }
70
+
71
+ /**
72
+ * Safely stringify an error, redacting any secrets.
73
+ *
74
+ * Handles unknown error types (Error, string, object, etc.)
75
+ * and ensures no secret values leak through error messages.
76
+ *
77
+ * @param error - The error to stringify
78
+ * @returns A safe, redacted error string
79
+ */
80
+ export function redactError(error: unknown): string {
81
+ let message: string;
82
+
83
+ if (error instanceof Error) {
84
+ // Only use message, not stack (stack can contain env var values)
85
+ message = error.message;
86
+ } else if (typeof error === "string") {
87
+ message = error;
88
+ } else {
89
+ try {
90
+ message = JSON.stringify(error) ?? String(error);
91
+ } catch {
92
+ message = String(error);
93
+ }
94
+ }
95
+
96
+ return redactSecrets(message);
97
+ }
98
+
99
+ /**
100
+ * Build a filtered environment for MCP subprocesses.
101
+ *
102
+ * Only includes safe, non-secret environment variables from process.env,
103
+ * merged with any explicitly provided config env vars.
104
+ *
105
+ * @param configEnv - Additional env vars from MCP config (may include tokens intentionally)
106
+ * @returns A filtered environment object
107
+ */
108
+ export function buildSafeEnv(configEnv?: Record<string, string>): Record<string, string> {
109
+ const safeEnv: Record<string, string> = {};
110
+
111
+ // Only copy allowed env vars from process.env
112
+ for (const key of MCP_SAFE_ENV_VARS) {
113
+ const value = process.env[key];
114
+ if (value !== undefined) {
115
+ safeEnv[key] = value;
116
+ }
117
+ }
118
+
119
+ // Merge explicitly provided config env (these are intentional, e.g., LINEAR_API_KEY)
120
+ if (configEnv) {
121
+ Object.assign(safeEnv, configEnv);
122
+ }
123
+
124
+ return safeEnv;
125
+ }