From d5f0d016aff065ead0401b10d35b29cd29378168 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Wed, 18 Mar 2026 13:41:13 -0700 Subject: [PATCH 1/5] docs: add example CLAUDE.md for automatic preflight integration Adds a ready-to-use CLAUDE.md template that makes Claude Code automatically run preflight_check on prompts. Users can copy it into their project to get preflight working without manual tool calls. Referenced from Quick Start in README and examples/README. --- README.md | 10 ++++++++++ examples/CLAUDE.md | 30 ++++++++++++++++++++++++++++++ examples/README.md | 16 ++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 examples/CLAUDE.md diff --git a/README.md b/README.md index 6d03f5d..7969d58 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,16 @@ claude mcp add preflight -- preflight-dev-serve > **Note:** `preflight-dev` runs the interactive setup wizard. `preflight-dev-serve` starts the MCP server — that's what you want in your Claude Code config. +### Make Claude use preflight automatically + +Add preflight rules to your project's `CLAUDE.md` so Claude runs `preflight_check` on every prompt without you asking: + +```bash +cp /path/to/preflight/examples/CLAUDE.md your-project/CLAUDE.md +``` + +See [`examples/CLAUDE.md`](examples/CLAUDE.md) for a ready-to-use template with recommended rules for when to preflight, session hygiene, and skip-lists. + --- ## How It Works diff --git a/examples/CLAUDE.md b/examples/CLAUDE.md new file mode 100644 index 0000000..da55f40 --- /dev/null +++ b/examples/CLAUDE.md @@ -0,0 +1,30 @@ +# CLAUDE.md — Preflight Integration Example +# +# Drop this into your project's CLAUDE.md (or .claude/CLAUDE.md) to make +# Claude Code automatically use preflight tools during your sessions. +# Customize the rules below to match your workflow. + +## Preflight Rules + +Before starting any non-trivial task, run `preflight_check` with my prompt. This catches vague instructions before they waste tokens on wrong→fix cycles. + +### When to use preflight tools: + +- **Every prompt**: `preflight_check` triages automatically — let it decide what's needed +- **Before multi-file changes**: Run `scope_work` to get a phased plan +- **Before sub-agent tasks**: Use `enrich_agent_task` to add context +- **After making a mistake**: Use `log_correction` so preflight learns the pattern +- **Before ending a session**: Run `checkpoint` to save state for next time +- **When I say "fix it" or "do the others"**: Use `sharpen_followup` to resolve what I actually mean + +### Session hygiene: + +- Run `check_session_health` if we've been going for a while without committing +- If I ask about something we did before, use `search_history` to find it +- Before declaring a task done, run `verify_completion` (type check + tests) + +### Don't preflight these: + +- Simple git commands (commit, push, status) +- Formatting / linting +- Reading files I explicitly named diff --git a/examples/README.md b/examples/README.md index 778f15d..f2fafc1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,6 +12,22 @@ The `.preflight/` directory contains example configuration files you can copy in └── api.yml # Manual contract definitions for cross-service types ``` +## `CLAUDE.md` Integration + +The `CLAUDE.md` file tells Claude Code how to behave in your project. Adding preflight rules here makes Claude automatically use preflight tools without you having to ask. + +```bash +# Copy the example into your project: +cp /path/to/preflight/examples/CLAUDE.md my-project/CLAUDE.md + +# Or append to your existing CLAUDE.md: +cat /path/to/preflight/examples/CLAUDE.md >> my-project/CLAUDE.md +``` + +This is the **recommended way** to integrate preflight — once it's in your `CLAUDE.md`, every session automatically runs `preflight_check` on your prompts. + +--- + ### Quick setup ```bash From c17f46344bf005464e2bd65b1f1631852a51e069 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 08:20:24 -0700 Subject: [PATCH 2/5] feat(cli): add --help and --version flags, fix Node badge to 20+ - CLI now responds to --help/-h with usage info, profiles, and links - CLI now responds to --version/-v with package version - Previously, any flag just launched the interactive wizard - Fixed README badge from Node 18+ to Node 20+ (matches engines field) --- README.md | 2 +- src/cli/init.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7969d58..e7a385f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A 24-tool MCP server for Claude Code that catches ambiguous instructions before [![MCP](https://img.shields.io/badge/MCP-Compatible-blueviolet)](https://modelcontextprotocol.io/) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![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/) +[![Node 20+](https://img.shields.io/badge/node-20%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) diff --git a/src/cli/init.ts b/src/cli/init.ts index dfaaa25..d1b0021 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -9,6 +9,46 @@ import { join, dirname } from "node:path"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; +// Handle --help and --version before launching interactive wizard +const args = process.argv.slice(2); + +if (args.includes("--help") || args.includes("-h")) { + console.log(` +✈️ preflight-dev — MCP server for Claude Code prompt discipline + +Usage: + preflight-dev Interactive setup wizard (creates .mcp.json) + preflight-dev --help Show this help message + preflight-dev --version Show version + +The wizard will: + 1. Ask you to choose a profile (minimal / standard / full) + 2. Optionally create a .preflight/ config directory + 3. Write an .mcp.json so Claude Code auto-connects to preflight + +After setup, restart Claude Code and preflight tools will appear. + +Profiles: + minimal 4 tools — clarify_intent, check_session_health, session_stats, prompt_score + standard 16 tools — all prompt discipline + session_stats + prompt_score + full 20 tools — everything + timeline/vector search (needs LanceDB) + +More info: https://github.com/TerminalGravity/preflight +`); + process.exit(0); +} + +if (args.includes("--version") || args.includes("-v")) { + const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json"); + try { + const pkg = JSON.parse(await readFile(pkgPath, "utf-8")); + console.log(`preflight-dev v${pkg.version}`); + } catch { + console.log("preflight-dev (version unknown)"); + } + process.exit(0); +} + const rl = createInterface({ input: process.stdin, output: process.stdout }); function ask(question: string): Promise { From b28d77c0dee19e6845fcbc84d2f4f72c1ce1c9b2 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 14:21:08 -0700 Subject: [PATCH 3/5] docs: add TROUBLESHOOTING.md with common setup and config fixes --- README.md | 10 +++ TROUBLESHOOTING.md | 165 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 TROUBLESHOOTING.md diff --git a/README.md b/README.md index e7a385f..cdd5a58 100644 --- a/README.md +++ b/README.md @@ -744,6 +744,16 @@ curl http://localhost:11434/api/embed -d '{"model":"all-minilm","input":"test"}' --- +## Troubleshooting + +Having issues? Check **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** for solutions to common problems including: +- LanceDB setup and platform issues +- Embedding provider configuration +- `.preflight/` config parsing errors +- Profile selection guide + +--- + ## Contributing This project is young and there's plenty to do. Check the [issues](https://github.com/TerminalGravity/preflight/issues) — several are tagged `good first issue`. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..77171fc --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,165 @@ +# Troubleshooting + +Common issues and fixes for preflight. + +--- + +## Installation & Setup + +### `npx preflight-dev-serve` fails with module errors + +**Symptoms:** `ERR_MODULE_NOT_FOUND` or `Cannot find module` when running via npx. + +**Fix:** Preflight requires **Node 20+**. Check your version: +```bash +node --version +``` +If you're on Node 18 or below, upgrade via [nvm](https://github.com/nvm-sh/nvm): +```bash +nvm install 20 +nvm use 20 +``` + +### Tools don't appear in Claude Code after `claude mcp add` + +**Fix:** Restart Claude Code completely after adding the MCP server. The tool list is loaded at startup. + +If tools still don't appear, verify the server starts without errors: +```bash +npx preflight-dev-serve +``` +You should see MCP protocol output (JSON on stdout). If you see errors, check the sections below. + +--- + +## LanceDB & Timeline Search + +### `Error: Failed to open LanceDB` or LanceDB crashes on startup + +**Symptoms:** Timeline tools (`search_timeline`, `index_sessions`, etc.) fail. Other tools work fine. + +**Cause:** LanceDB uses native binaries that may not be available for your platform, or the database directory has permission issues. + +**Fixes:** +1. Make sure `~/.preflight/projects/` is writable: + ```bash + mkdir -p ~/.preflight/projects + ls -la ~/.preflight/ + ``` +2. If on an unsupported platform, use the **minimal** or **standard** profile (no LanceDB required): + ```bash + npx preflight-dev + # Choose "minimal" or "standard" when prompted + ``` +3. Clear corrupted LanceDB data: + ```bash + rm -rf ~/.preflight/projects/*/timeline.lance + ``` + Then re-index with `index_sessions`. + +### Timeline search returns no results + +**Cause:** Sessions haven't been indexed yet. Preflight doesn't auto-index — you need to run `index_sessions` first. + +**Fix:** In Claude Code, run: +``` +index my sessions +``` +Or use the `index_sessions` tool directly. Indexing reads your `~/.claude/projects/` session files. + +--- + +## Embeddings + +### `OpenAI API key required for openai embedding provider` + +**Cause:** You selected OpenAI embeddings but didn't set the API key. + +**Fixes:** + +Option A — Set the environment variable when adding the MCP server: +```bash +claude mcp add preflight \ + -e OPENAI_API_KEY=sk-... \ + -- npx -y preflight-dev-serve +``` + +Option B — Switch to local embeddings (no API key needed). Create or edit `~/.preflight/config.json`: +```json +{ + "embedding_provider": "local", + "embedding_model": "Xenova/all-MiniLM-L6-v2" +} +``` + +### Local embeddings are slow on first run + +**Expected.** The model (~80MB) downloads on first use and is cached afterward. Subsequent runs are fast. + +--- + +## `.preflight/` Config + +### `warning - failed to parse .preflight/config.yml` + +**Cause:** YAML syntax error in your project's `.preflight/config.yml`. + +**Fix:** Validate your YAML: +```bash +npx yaml-lint .preflight/config.yml +``` +Or check for common issues: wrong indentation, tabs instead of spaces, missing colons. + +### Config changes not taking effect + +**Cause:** Preflight reads config at MCP server startup, not on every tool call. + +**Fix:** Restart Claude Code after editing `.preflight/config.yml` or `.preflight/triage.yml`. + +--- + +## Profiles + +### Which profile should I use? + +| Profile | Tools | Best for | +|---------|-------|----------| +| **minimal** | 4 | Try it out, low overhead | +| **standard** | 16 | Daily use, no vector search needed | +| **full** | 20 | Power users who want timeline search | + +You can change profiles by re-running the setup wizard: +```bash +npx preflight-dev +``` + +--- + +## Platform-Specific + +### Apple Silicon (M1/M2/M3/M4): LanceDB build fails + +LanceDB ships prebuilt binaries for Apple Silicon. If `npm install` fails on the native module: +```bash +# Ensure you're using the ARM64 version of Node +node -p process.arch # should print "arm64" + +# If it prints "x64", reinstall Node natively (not via Rosetta) +``` + +### Linux: Permission denied on `~/.preflight/` + +```bash +chmod -R u+rwX ~/.preflight/ +``` + +--- + +## Still stuck? + +1. Check [open issues](https://github.com/TerminalGravity/preflight/issues) — someone may have hit the same problem +2. [Open a new issue](https://github.com/TerminalGravity/preflight/issues/new) with: + - Your Node version (`node --version`) + - Your OS and architecture (`uname -a`) + - The full error message + - Which profile you selected From 948c34777ae5faa53b5ac93838934b91b6d4179a Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 15:42:33 -0700 Subject: [PATCH 4/5] docs: add example .preflight/ config and triage YAML files Adds well-commented example config.yml and triage.yml to examples/.preflight/ so users can copy them into their project root and customize. Every field is documented inline with descriptions of valid values and defaults. --- examples/.preflight/config.yml | 50 +++++++++++++++++----------------- examples/.preflight/triage.yml | 44 ++++++++++++------------------ 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/examples/.preflight/config.yml b/examples/.preflight/config.yml index f59170f..dde3cff 100644 --- a/examples/.preflight/config.yml +++ b/examples/.preflight/config.yml @@ -1,35 +1,35 @@ -# .preflight/config.yml — Drop this in your project root -# -# This is an example config for a typical Next.js + microservices setup. -# Every field is optional — preflight works with sensible defaults out of the box. -# Commit this to your repo so the whole team gets the same preflight behavior. +# .preflight/config.yml +# Drop this directory in your project root to configure preflight per-project. +# All fields are optional — defaults are used for anything you omit. -# Profile controls how much detail preflight returns. -# "minimal" — only flags ambiguous+ prompts, skips clarification detail -# "standard" — balanced (default) -# "full" — maximum detail on every non-trivial prompt +# Tool profile: how many tools to expose +# minimal — 4 tools (preflight_check, prompt_score, clarify_intent, scope_work) +# standard — 16 tools (adds scorecards, cost estimation, corrections, contracts) +# full — 20 tools (adds LanceDB timeline search — requires Node 20+) profile: standard -# Related projects for cross-service awareness. -# Preflight will search these for shared types, routes, and contracts -# so it can warn you when a change might break a consumer. +# Related projects for cross-service contract search. +# Preflight scans these for shared types, API routes, and schemas. related_projects: - - path: /Users/you/code/auth-service - alias: auth - - path: /Users/you/code/billing-api - alias: billing - - path: /Users/you/code/shared-types + - path: ../api-service + alias: api + - path: ../shared-types alias: types -# Behavioral thresholds — tune these to your workflow +# Tuning knobs thresholds: - session_stale_minutes: 30 # Warn if no activity for this long - max_tool_calls_before_checkpoint: 100 # Suggest a checkpoint after N tool calls - correction_pattern_threshold: 3 # Min corrections before flagging a pattern + # Minutes before a session is considered "stale" (triggers session_health warnings) + session_stale_minutes: 30 -# Embedding provider for semantic search over session history. -# "local" uses Xenova transformers (no API key needed, runs on CPU). -# "openai" uses text-embedding-3-small (faster, needs OPENAI_API_KEY). + # Tool calls before preflight nudges you to checkpoint progress + max_tool_calls_before_checkpoint: 100 + + # How many times a correction pattern repeats before it becomes a warning + correction_pattern_threshold: 3 + +# Embedding config (only matters for "full" profile with timeline search) embeddings: + # "local" — runs Xenova/all-MiniLM-L6-v2 in-process, no API key needed + # "openai" — uses text-embedding-3-small, requires openai_api_key or OPENAI_API_KEY env var provider: local - # openai_api_key: sk-... # Uncomment if using openai provider + # openai_api_key: sk-... # or set OPENAI_API_KEY env var instead diff --git a/examples/.preflight/triage.yml b/examples/.preflight/triage.yml index b3d394e..32261d3 100644 --- a/examples/.preflight/triage.yml +++ b/examples/.preflight/triage.yml @@ -1,45 +1,37 @@ -# .preflight/triage.yml — Controls how preflight classifies your prompts -# -# The triage engine routes prompts into categories: -# TRIVIAL → pass through (commit, format, lint) -# CLEAR → well-specified, no intervention needed -# AMBIGUOUS → needs clarification before proceeding -# MULTI-STEP → complex task, preflight suggests a plan -# CROSS-SERVICE → touches multiple projects, pulls in contracts -# -# Customize the keywords below to match your domain. +# .preflight/triage.yml +# Controls how preflight_check triages your prompts. +# Separate from config.yml so you can tune triage rules independently. + +# Strictness level: +# relaxed — only flags clearly ambiguous prompts +# standard — balanced (default) +# strict — flags anything that could be more specific +strictness: standard rules: - # Prompts containing these words are always flagged as AMBIGUOUS. - # Add domain-specific terms that tend to produce vague prompts. + # Keywords that ALWAYS trigger a full preflight check, even for short prompts. + # Use this for high-risk areas of your codebase. always_check: - rewards - permissions - migration - schema - - pricing # example: your billing domain - - onboarding # example: multi-step user flows + - billing + - auth - # Prompts containing these words skip checks entirely (TRIVIAL). - # These are safe, mechanical tasks that don't need guardrails. + # Keywords that SKIP preflight checks entirely. + # Use this for low-risk, routine commands. skip: - commit - format - lint - prettier - - "git push" - # Prompts containing these words trigger CROSS-SERVICE classification. - # Preflight will search related_projects for relevant types and routes. + # Keywords that trigger cross-service contract scanning. + # When these appear, preflight also checks related_projects for shared types. cross_service_keywords: - auth - notification - event - webhook - - billing # matches the related_project alias - -# How aggressively to classify prompts. -# "relaxed" — more prompts pass as clear (experienced users) -# "standard" — balanced (default) -# "strict" — more prompts flagged as ambiguous (new teams, complex codebases) -strictness: standard + - payment From 7f59b1788b2bc2a9f36db5f67bd6d39a0daf1fec Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 17:15:26 -0700 Subject: [PATCH 5/5] test: add unit tests for state.ts (load/save/appendLog/readLog/rotation) --- tests/lib/state.test.ts | 127 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/lib/state.test.ts diff --git a/tests/lib/state.test.ts b/tests/lib/state.test.ts new file mode 100644 index 0000000..73c23f9 --- /dev/null +++ b/tests/lib/state.test.ts @@ -0,0 +1,127 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; + +// Must use a literal or import-free expression inside vi.mock factory (hoisted) +vi.mock("../../src/lib/files.js", () => { + const os = require("os"); + const path = require("path"); + return { + PROJECT_DIR: path.join(os.tmpdir(), "preflight-state-test"), + }; +}); + +import { loadState, saveState, appendLog, readLog, now, STATE_DIR } from "../../src/lib/state.js"; + +const TEST_DIR = join(tmpdir(), "preflight-state-test"); + +describe("state", () => { + beforeEach(() => { + mkdirSync(TEST_DIR, { recursive: true }); + }); + + afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }); + }); + + describe("loadState", () => { + it("returns empty object when file does not exist", () => { + expect(loadState("nonexistent")).toEqual({}); + }); + + it("loads valid JSON state file", () => { + mkdirSync(STATE_DIR, { recursive: true }); + const data = { foo: "bar", count: 42 }; + writeFileSync(join(STATE_DIR, "test.json"), JSON.stringify(data)); + expect(loadState("test")).toEqual(data); + }); + + it("returns empty object for corrupt JSON", () => { + mkdirSync(STATE_DIR, { recursive: true }); + writeFileSync(join(STATE_DIR, "corrupt.json"), "{bad json"); + expect(loadState("corrupt")).toEqual({}); + }); + }); + + describe("saveState", () => { + it("creates state dir and writes JSON", () => { + const data = { key: "value", nested: { a: 1 } }; + saveState("mystate", data); + const raw = readFileSync(join(STATE_DIR, "mystate.json"), "utf-8"); + expect(JSON.parse(raw)).toEqual(data); + }); + + it("overwrites existing state", () => { + saveState("overwrite", { v: 1 }); + saveState("overwrite", { v: 2 }); + expect(loadState("overwrite")).toEqual({ v: 2 }); + }); + }); + + describe("appendLog / readLog", () => { + it("appends and reads JSONL entries", () => { + appendLog("test.jsonl", { event: "a" }); + appendLog("test.jsonl", { event: "b" }); + appendLog("test.jsonl", { event: "c" }); + + const entries = readLog("test.jsonl"); + expect(entries).toHaveLength(3); + expect(entries[0]).toEqual({ event: "a" }); + expect(entries[2]).toEqual({ event: "c" }); + }); + + it("readLog returns empty array for missing file", () => { + expect(readLog("missing.jsonl")).toEqual([]); + }); + + it("readLog supports lastN parameter", () => { + appendLog("last.jsonl", { n: 1 }); + appendLog("last.jsonl", { n: 2 }); + appendLog("last.jsonl", { n: 3 }); + + const last2 = readLog("last.jsonl", 2); + expect(last2).toHaveLength(2); + expect(last2[0]).toEqual({ n: 2 }); + expect(last2[1]).toEqual({ n: 3 }); + }); + + it("readLog skips corrupt lines gracefully", () => { + mkdirSync(STATE_DIR, { recursive: true }); + writeFileSync( + join(STATE_DIR, "mixed.jsonl"), + '{"ok":true}\n{bad line\n{"also":"ok"}\n' + ); + const entries = readLog("mixed.jsonl"); + expect(entries).toHaveLength(2); + expect(entries[0]).toEqual({ ok: true }); + expect(entries[1]).toEqual({ also: "ok" }); + }); + + it("rotates log when exceeding max size", () => { + mkdirSync(STATE_DIR, { recursive: true }); + const logPath = join(STATE_DIR, "big.jsonl"); + // Write a file just over 5MB + const bigLine = JSON.stringify({ data: "x".repeat(1000) }) + "\n"; + const count = Math.ceil((5 * 1024 * 1024) / bigLine.length) + 10; + writeFileSync(logPath, bigLine.repeat(count)); + + // Next append should trigger rotation + appendLog("big.jsonl", { after: "rotation" }); + + expect(existsSync(logPath + ".old")).toBe(true); + // New file should only have the one new entry + const entries = readLog("big.jsonl"); + expect(entries).toHaveLength(1); + expect(entries[0]).toEqual({ after: "rotation" }); + }); + }); + + describe("now", () => { + it("returns a valid ISO timestamp", () => { + const ts = now(); + expect(() => new Date(ts)).not.toThrow(); + expect(new Date(ts).toISOString()).toBe(ts); + }); + }); +});