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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A 24-tool MCP server for Claude Code that catches ambiguous instructions before
[![npm](https://img.shields.io/npm/v/preflight-dev)](https://www.npmjs.com/package/preflight-dev)
[![Node 18+](https://img.shields.io/badge/node-18%2B-brightgreen?logo=node.js&logoColor=white)](https://nodejs.org/)

[Quick Start](#quick-start) · [How It Works](#how-it-works) · [Tool Reference](#tool-reference) · [Configuration](#configuration) · [Scoring](#the-12-category-scorecard)
[Quick Start](#quick-start) · [How It Works](#how-it-works) · [Tool Reference](#tool-reference) · [Configuration](#configuration) · [Scoring](#the-12-category-scorecard) · [Troubleshooting](TROUBLESHOOTING.md)

</div>

Expand Down
131 changes: 131 additions & 0 deletions TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Troubleshooting

Common issues and fixes for preflight.

---

## Installation

### `npm install` fails with LanceDB native binary errors

LanceDB uses native binaries. If you see errors like `prebuild-install WARN` or `node-gyp` failures:

```
npm ERR! @lancedb/lancedb@0.26.2: The platform "linux" is incompatible
```

**Fix:** Make sure you're on a supported platform (macOS arm64/x64, Linux x64, Windows x64) and Node >= 20:

```bash
node -v # must be >= 20
npm cache clean --force
rm -rf node_modules package-lock.json
npm install
```

If you're on an unsupported platform (e.g., Linux arm64), LanceDB won't work. The timeline/vector search tools will be unavailable, but the core preflight tools still function.

### `npx tsx` not found

```bash
npm install -g tsx
# or use npx (comes with npm 7+):
npx tsx src/index.ts
```

---

## Configuration

### Tools load but `CLAUDE_PROJECT_DIR` warnings appear

The timeline and contract search tools need `CLAUDE_PROJECT_DIR` to know which project to index. Without it, those tools will error on use.

**Fix — Claude Code CLI:**
```bash
claude mcp add preflight \
-e CLAUDE_PROJECT_DIR=/absolute/path/to/your/project \
-- npx tsx /path/to/preflight/src/index.ts
```

**Fix — `.mcp.json`:**
```json
{
"mcpServers": {
"preflight": {
"command": "npx",
"args": ["tsx", "/path/to/preflight/src/index.ts"],
"env": {
"CLAUDE_PROJECT_DIR": "/absolute/path/to/your/project"
}
}
}
}
```

Use absolute paths — relative paths resolve from the MCP server's cwd, which may not be your project.

### `.preflight/` config not being picked up

Preflight looks for `.preflight/config.yml` in `CLAUDE_PROJECT_DIR`. If it's not found, defaults are used silently.

**Check:**
1. File is named exactly `config.yml` (not `config.yaml`)
2. It's inside `.preflight/` at your project root
3. `CLAUDE_PROJECT_DIR` points to the right directory

---

## Runtime

### "Server started" but no tools appear in Claude Code

The MCP handshake might be failing silently.

**Debug steps:**
1. Test the server standalone: `npx tsx src/index.ts` — should print `preflight: server started`
2. Check Claude Code's MCP logs: `claude mcp list` to see registered servers
3. Remove and re-add: `claude mcp remove preflight && claude mcp add preflight -- npx tsx /path/to/preflight/src/index.ts`

### Vector search returns no results

Timeline search uses LanceDB to index your Claude Code session history (JSONL files).

**Common causes:**
- No session history exists yet — use Claude Code for a few sessions first
- `CLAUDE_PROJECT_DIR` not set (search doesn't know where to look)
- Session files are in a non-standard location

**Where Claude Code stores sessions:** `~/.claude/projects/` with JSONL files per session.

### `preflight_check` returns generic advice

The triage system works best with specific prompts. If you're testing with something like "do stuff", the response will be generic by design — that's it telling you the prompt is too vague.

Try a real prompt: `"add rate limiting to the /api/users endpoint"` — you'll see it route through scope analysis, contract search, and produce actionable guidance.

---

## Performance

### Slow startup (>5 seconds)

LanceDB initialization can be slow on first run as it builds the vector index.

**Fix:** Subsequent runs are faster. If consistently slow, check that `node_modules/@lancedb` isn't corrupted:
```bash
rm -rf node_modules/@lancedb
npm install
```

### High memory usage

Each indexed project maintains an in-memory vector index. If you're indexing many large projects, memory can grow.

**Fix:** Only set `CLAUDE_PROJECT_DIR` to the project you're actively working on.

---

## Still stuck?

Open an issue: https://github.com/TerminalGravity/preflight/issues
27 changes: 26 additions & 1 deletion src/lib/git.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execFileSync } from "child_process";
import { execFileSync, execSync } from "child_process";
import { PROJECT_DIR } from "./files.js";
import type { RunError } from "../types.js";

Expand Down Expand Up @@ -30,6 +30,31 @@ export function run(argsOrCmd: string | string[], opts: { timeout?: number } = {
}
}

/**
* Run a shell command string via execSync (with shell).
* Use for commands that genuinely need shell features: pipes, redirects, ||, &&.
* Prefer run() with array args for simple git commands.
*/
export function shell(cmd: string, opts: { timeout?: number } = {}): string {
try {
return execSync(cmd, {
cwd: PROJECT_DIR,
encoding: "utf-8",
timeout: opts.timeout || 10000,
maxBuffer: 1024 * 1024,
stdio: ["pipe", "pipe", "pipe"],
}).trim();
} catch (e: any) {
const timedOut = e.killed === true || e.signal === "SIGTERM";
if (timedOut) {
return `[timed out after ${opts.timeout || 10000}ms]`;
}
const output = e.stdout?.trim() || e.stderr?.trim();
if (output) return output;
return `[command failed: ${cmd} (exit ${e.status ?? "?"})]`;
}
}

/** Convenience: run a raw command string (split on spaces). Only for simple, known-safe commands. */
function gitCmd(cmdStr: string, opts?: { timeout?: number }): string {
return run(cmdStr.split(/\s+/), opts);
Expand Down
6 changes: 3 additions & 3 deletions src/tools/audit-workspace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { run } from "../lib/git.js";
import { run, shell } from "../lib/git.js";
import { readIfExists, findWorkspaceDocs } from "../lib/files.js";

/** Extract top-level work areas from file paths generically */
Expand Down Expand Up @@ -36,7 +36,7 @@ export function registerAuditWorkspace(server: McpServer): void {
{},
async () => {
const docs = findWorkspaceDocs();
const recentFiles = run("git diff --name-only HEAD~10 2>/dev/null || echo ''").split("\n").filter(Boolean);
const recentFiles = run(["diff", "--name-only", "HEAD~10"]).split("\n").filter(Boolean);
const sections: string[] = [];

// Doc freshness
Expand Down Expand Up @@ -75,7 +75,7 @@ export function registerAuditWorkspace(server: McpServer): void {
// Check for gap trackers or similar tracking docs
const trackingDocs = Object.entries(docs).filter(([n]) => /gap|track|progress/i.test(n));
if (trackingDocs.length > 0) {
const testFilesCount = parseInt(run("find tests -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | wc -l").trim()) || 0;
const testFilesCount = parseInt(shell("find tests -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | wc -l").trim()) || 0;
sections.push(`## Tracking Docs\n${trackingDocs.map(([n]) => {
const age = docStatus.find(d => d.name === n)?.ageHours ?? "?";
return `- .claude/${n} — last updated ${age}h ago`;
Expand Down
9 changes: 5 additions & 4 deletions src/tools/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { writeFileSync, existsSync, mkdirSync } from "fs";
import { join, dirname } from "path";
import { run, getBranch, getStatus, getLastCommit, getStagedFiles } from "../lib/git.js";
import { run, shell, getBranch, getStatus, getLastCommit, getStagedFiles } from "../lib/git.js";
import { PROJECT_DIR } from "../lib/files.js";
import { appendLog, now } from "../lib/state.js";

Expand Down Expand Up @@ -84,11 +84,12 @@ ${dirty || "clean"}

if (commitResult === "no uncommitted changes") {
// Stage the checkpoint file too
run(`git add "${checkpointFile}"`);
const result = run(`${addCmd} && git commit -m "${commitMsg.replace(/"/g, '\\"')}" 2>&1`);
run(["add", checkpointFile]);
shell(addCmd);
const result = run(["commit", "-m", commitMsg]);
if (result.includes("commit failed") || result.includes("nothing to commit")) {
// Rollback: unstage if commit failed
run("git reset HEAD 2>/dev/null");
run(["reset", "HEAD"]);
commitResult = `commit failed: ${result}`;
} else {
commitResult = result;
Expand Down
6 changes: 3 additions & 3 deletions src/tools/clarify-intent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { run, getBranch, getStatus, getRecentCommits, getDiffFiles, getStagedFiles } from "../lib/git.js";
import { run, shell, getBranch, getStatus, getRecentCommits, getDiffFiles, getStagedFiles } from "../lib/git.js";
import { findWorkspaceDocs, PROJECT_DIR } from "../lib/files.js";
import { searchSemantic } from "../lib/timeline-db.js";
import { getRelatedProjects } from "../lib/config.js";
Expand Down Expand Up @@ -152,10 +152,10 @@ export function registerClarifyIntent(server: McpServer): void {
let hasTestFailures = false;

if (!area || area.includes("test") || area.includes("fix") || area.includes("ui") || area.includes("api")) {
const typeErrors = run("pnpm tsc --noEmit 2>&1 | grep -c 'error TS' || echo '0'");
const typeErrors = shell("pnpm tsc --noEmit 2>&1 | grep -c 'error TS' || echo '0'");
hasTypeErrors = parseInt(typeErrors, 10) > 0;

const testFiles = run("find tests -name '*.spec.ts' -maxdepth 4 2>/dev/null | head -20");
const testFiles = shell("find tests -name '*.spec.ts' -maxdepth 4 2>/dev/null | head -20");
const failingTests = getTestFailures();
hasTestFailures = failingTests !== "all passing" && failingTests !== "no test report found";

Expand Down
31 changes: 22 additions & 9 deletions src/tools/enrich-agent-task.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { run, getDiffFiles } from "../lib/git.js";
import { run, shell, getDiffFiles } from "../lib/git.js";
import { PROJECT_DIR } from "../lib/files.js";
import { getConfig, type RelatedProject } from "../lib/config.js";
import { existsSync, readFileSync } from "fs";
Expand Down Expand Up @@ -29,31 +29,44 @@ function findAreaFiles(area: string): string {

// If area looks like a path, search directly
if (area.includes("/")) {
return run(`git ls-files -- '${safeArea}*' 2>/dev/null | head -20`);
const allFiles = run(["ls-files", "--", `${safeArea}*`]);
return allFiles.split("\n").filter(Boolean).slice(0, 20).join("\n");
}

// Search for area keyword in git-tracked file paths
const files = run(`git ls-files 2>/dev/null | grep -i '${safeArea}' | head -20`);
if (files && !files.startsWith("[command failed")) return files;
const allFiles = run(["ls-files"]);
if (allFiles.startsWith("[")) return getDiffFiles("HEAD~3");
const matched = allFiles.split("\n").filter(f => f.toLowerCase().includes(safeArea.toLowerCase())).slice(0, 20);
if (matched.length > 0) return matched.join("\n");

// Fallback to recently changed files
return getDiffFiles("HEAD~3");
}

/** Find related test files for an area */
function findRelatedTests(area: string): string {
if (!area) return run("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10");
const allFiles = run(["ls-files"]);
if (allFiles.startsWith("[")) return "";
const testPattern = /\.(spec|test)\.(ts|tsx|js|jsx)$/;
const allTests = allFiles.split("\n").filter(f => testPattern.test(f));

const safeArea = shellEscape(area.split(/\s+/)[0]);
const tests = run(`git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | grep -i '${safeArea}' | head -10`);
return tests || run("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10");
if (!area) return allTests.slice(0, 10).join("\n");

const safeArea = shellEscape(area.split(/\s+/)[0]).toLowerCase();
const areaTests = allTests.filter(f => f.toLowerCase().includes(safeArea)).slice(0, 10);
return areaTests.length > 0 ? areaTests.join("\n") : allTests.slice(0, 10).join("\n");
}

/** Get an example pattern from the first matching file */
function getExamplePattern(files: string): string {
const firstFile = files.split("\n").filter(Boolean)[0];
if (!firstFile) return "no pattern available";
return run(`head -30 '${shellEscape(firstFile)}' 2>/dev/null || echo 'could not read file'`);
try {
const content = readFileSync(join(PROJECT_DIR, firstFile), "utf-8");
return content.split("\n").slice(0, 30).join("\n");
} catch {
return "could not read file";
}
}

// ---------------------------------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions src/tools/scope-work.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// CATEGORY 1: scope_work — Plans
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { run, getBranch, getRecentCommits, getStatus } from "../lib/git.js";
import { run, shell, getBranch, getRecentCommits, getStatus } from "../lib/git.js";
import { readIfExists, findWorkspaceDocs, PROJECT_DIR } from "../lib/files.js";
import { searchSemantic } from "../lib/timeline-db.js";
import { getRelatedProjects } from "../lib/config.js";
Expand Down Expand Up @@ -93,9 +93,9 @@ export function registerScopeWork(server: McpServer): void {
const timestamp = now();
const currentBranch = branch ?? getBranch();
const recentCommits = getRecentCommits(10);
const porcelain = run("git status --porcelain");
const porcelain = run(["status", "--porcelain"]);
const dirtyFiles = parsePortelainFiles(porcelain);
const diffStat = dirtyFiles.length > 0 ? run("git diff --stat") : "(clean working tree)";
const diffStat = dirtyFiles.length > 0 ? run(["diff", "--stat"]) : "(clean working tree)";

// Scan for relevant files based on task keywords
const keywords = task.toLowerCase().split(/\s+/);
Expand Down Expand Up @@ -128,7 +128,7 @@ export function registerScopeWork(server: McpServer): void {
.slice(0, 5);
if (grepTerms.length > 0) {
const pattern = shellEscape(grepTerms.join("|"));
matchedFiles = run(`git ls-files | head -500 | grep -iE '${pattern}' | head -30`);
matchedFiles = shell(`git ls-files | head -500 | grep -iE '${pattern}' | head -30`);
}

// Check which relevant dirs actually exist (with path traversal protection)
Expand Down
5 changes: 3 additions & 2 deletions src/tools/sequence-tasks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// CATEGORY 6: sequence_tasks — Sequencing
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { run } from "../lib/git.js";
import { run, shell } from "../lib/git.js";
import { now } from "../lib/state.js";
import { PROJECT_DIR } from "../lib/files.js";
import { existsSync } from "fs";
Expand Down Expand Up @@ -90,7 +90,8 @@ export function registerSequenceTasks(server: McpServer): void {
// For locality: infer directories from path-like tokens in task text
if (strategy === "locality") {
// Use git ls-files with a depth limit instead of find for performance
const gitFiles = run("git ls-files 2>/dev/null | head -1000");
const allFiles = run(["ls-files"]);
const gitFiles = allFiles.startsWith("[") ? "" : allFiles.split("\n").slice(0, 1000).join("\n");
const knownDirs = new Set<string>();
for (const f of gitFiles.split("\n").filter(Boolean)) {
const parts = f.split("/");
Expand Down
6 changes: 3 additions & 3 deletions src/tools/session-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { run, getBranch, getRecentCommits, getStatus } from "../lib/git.js";
import { run, shell, getBranch, getRecentCommits, getStatus } from "../lib/git.js";
import { readIfExists, findWorkspaceDocs } from "../lib/files.js";
import { STATE_DIR, now } from "../lib/state.js";

/** Check if a CLI tool is available */
function hasCommand(cmd: string): boolean {
const result = run(`command -v ${cmd} 2>/dev/null`);
const result = shell(`command -v ${cmd} 2>/dev/null`);
return !!result && !result.startsWith("[command failed");
}

Expand Down Expand Up @@ -44,7 +44,7 @@ export function registerSessionHandoff(server: McpServer): void {

// Only try gh if it exists
if (hasCommand("gh")) {
const openPRs = run("gh pr list --state open --json number,title,headRefName 2>/dev/null || echo '[]'");
const openPRs = shell("gh pr list --state open --json number,title,headRefName 2>/dev/null || echo '[]'");
if (openPRs && openPRs !== "[]") {
sections.push(`## Open PRs\n\`\`\`json\n${openPRs}\n\`\`\``);
}
Expand Down
Loading
Loading