From a388d1f3af613b1e1089fbb29481a17a2e22c19e Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Sun, 8 Mar 2026 11:10:36 -0700 Subject: [PATCH 1/6] add .preflight/ config example directory with annotated YAML files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - examples/.preflight/config.yml — main config with inline docs - examples/.preflight/triage.yml — triage rules with keyword examples - examples/.preflight/contracts/api.yml — sample contract definitions - examples/.preflight/README.md — getting started guide - README.md — link to examples from config section --- README.md | 2 ++ examples/.preflight/README.md | 43 +++++++++++++++++++++++ examples/.preflight/config.yml | 38 ++++++++++++++++++++ examples/.preflight/contracts/api.yml | 50 +++++++++++++++++++++++++++ examples/.preflight/triage.yml | 45 ++++++++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 examples/.preflight/README.md create mode 100644 examples/.preflight/config.yml create mode 100644 examples/.preflight/contracts/api.yml create mode 100644 examples/.preflight/triage.yml diff --git a/README.md b/README.md index f60fefa..232806a 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,8 @@ Preflight understands that microservices share contracts. When your prompt menti ### Setup +> 💡 **Want a ready-to-use example?** Copy `examples/.preflight/` into your project root — it includes annotated `config.yml`, `triage.yml`, and sample contracts. See [`examples/.preflight/README.md`](examples/.preflight/README.md). + **Option 1: `.preflight/config.yml`** (recommended — committed to repo) ```yaml diff --git a/examples/.preflight/README.md b/examples/.preflight/README.md new file mode 100644 index 0000000..264ecfa --- /dev/null +++ b/examples/.preflight/README.md @@ -0,0 +1,43 @@ +# `.preflight/` Config Directory + +Copy this directory to your project root to configure preflight for your team. + +``` +your-project/ +├── .preflight/ +│ ├── config.yml # Main config (profile, thresholds, related projects) +│ ├── triage.yml # Triage rules (keywords, strictness) +│ └── contracts/ # Manual contract definitions +│ └── api.yml # Example: shared API types and routes +├── src/ +└── ... +``` + +## Getting Started + +```bash +# From your project root: +cp -r /path/to/preflight/examples/.preflight .preflight + +# Edit config.yml with your related project paths: +vim .preflight/config.yml + +# Commit to share with your team: +git add .preflight && git commit -m "add preflight config" +``` + +## Files + +| File | Purpose | Required? | +|------|---------|-----------| +| `config.yml` | Profile, related projects, thresholds, embedding config | No (sensible defaults) | +| `triage.yml` | Keyword rules and strictness for prompt classification | No (sensible defaults) | +| `contracts/*.yml` | Manual type/route/interface definitions | No (auto-extracted from code) | + +All files are optional. Preflight works with zero config — these files let you tune it. + +## Tips + +- **Start minimal.** Drop in just `config.yml` with your `related_projects`. Add triage rules later as you see which prompts get misclassified. +- **Contracts are supplements.** Preflight auto-extracts types and routes from your code. Only add manual contracts for external services or planned interfaces. +- **Commit `.preflight/`.** The whole point is team-shareable configuration. diff --git a/examples/.preflight/config.yml b/examples/.preflight/config.yml new file mode 100644 index 0000000..731d612 --- /dev/null +++ b/examples/.preflight/config.yml @@ -0,0 +1,38 @@ +# .preflight/config.yml — Example configuration +# Copy this directory to your project root and customize. +# +# Every field is optional. Defaults are sensible for solo projects. +# For teams, commit this to your repo so everyone shares the same triage rules. + +# Profile controls how much detail preflight adds to responses. +# "minimal" — only flags ambiguous+ prompts, skips clarification detail +# "standard" — balanced (default) +# "full" — maximum detail on every non-trivial prompt +profile: standard + +# Related projects for cross-service contract awareness. +# Preflight indexes these and surfaces relevant types/routes when your +# prompt touches shared boundaries. +related_projects: + - path: /Users/you/projects/api-gateway + alias: api-gateway + - path: /Users/you/projects/shared-types + alias: shared-types + +# Behavioral thresholds — tune these to your workflow. +thresholds: + # Warn if no session activity for this many minutes + session_stale_minutes: 30 + + # Suggest a checkpoint after this many tool calls + max_tool_calls_before_checkpoint: 100 + + # Minimum corrections before preflight forms a "you keep doing this" pattern + correction_pattern_threshold: 3 + +# Embedding provider for semantic search across session history. +# "local" — zero config, uses Xenova transformers (~50 events/sec, 384d) +# "openai" — faster + higher quality, requires OPENAI_API_KEY (~200 events/sec, 1536d) +embeddings: + provider: local + # openai_api_key: sk-... # uncomment if using openai provider diff --git a/examples/.preflight/contracts/api.yml b/examples/.preflight/contracts/api.yml new file mode 100644 index 0000000..0ec395c --- /dev/null +++ b/examples/.preflight/contracts/api.yml @@ -0,0 +1,50 @@ +# .preflight/contracts/api.yml — Manual contract definitions +# These supplement auto-extracted contracts from your codebase. +# Manual definitions win on name conflicts with auto-extracted ones. +# +# Use these when: +# - A contract lives in a service you don't have locally +# - Auto-extraction misses something important +# - You want to document planned/in-progress interfaces + +- name: User + kind: interface + description: Core user record shared across services + fields: + - name: id + type: string + required: true + - name: email + type: string + required: true + - name: role + type: "'admin' | 'member' | 'viewer'" + required: true + - name: createdAt + type: Date + required: true + +- name: CreateUserRequest + kind: interface + description: POST /api/users request body + fields: + - name: email + type: string + required: true + - name: role + type: "'admin' | 'member' | 'viewer'" + required: false + - name: inviteCode + type: string + required: false + +- name: "POST /api/users" + kind: route + description: Creates a new user account + fields: + - name: body + type: CreateUserRequest + required: true + - name: returns + type: "{ user: User, token: string }" + required: true diff --git a/examples/.preflight/triage.yml b/examples/.preflight/triage.yml new file mode 100644 index 0000000..b6a3775 --- /dev/null +++ b/examples/.preflight/triage.yml @@ -0,0 +1,45 @@ +# .preflight/triage.yml — Triage classification rules +# Controls how preflight routes your prompts through the decision tree. +# +# The triage engine classifies every prompt into one of: +# TRIVIAL → pass through (commit, lint, format) +# CLEAR → well-scoped, no intervention needed +# AMBIGUOUS → needs clarification before proceeding +# MULTI-STEP → complex task, will be sequenced +# CROSS-SERVICE → touches multiple projects/services + +rules: + # Prompts containing these words are always flagged as at least AMBIGUOUS. + # Add domain terms that are frequently under-specified in your codebase. + always_check: + - rewards + - permissions + - migration + - schema + - billing + - onboarding + + # Prompts containing these words pass through as TRIVIAL. + # These are safe, well-understood commands that don't need preflight. + skip: + - commit + - format + - lint + - prettier + - "git push" + + # Prompts containing these words trigger CROSS-SERVICE classification. + # Preflight will search related projects for relevant contracts. + cross_service_keywords: + - auth + - notification + - event + - webhook + - queue + - pub/sub + +# How aggressively to classify prompts. +# "relaxed" — more prompts pass as clear (good for experienced users) +# "standard" — balanced (default) +# "strict" — more prompts flagged as ambiguous (good for teams/onboarding) +strictness: standard From 720683dbeb1fb89fd6066f75db5868834c160004 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Sun, 8 Mar 2026 12:21:00 -0700 Subject: [PATCH 2/6] docs: add TROUBLESHOOTING.md with common setup and usage fixes --- README.md | 6 ++ TROUBLESHOOTING.md | 155 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 TROUBLESHOOTING.md diff --git a/README.md b/README.md index 232806a..5808680 100644 --- a/README.md +++ b/README.md @@ -564,6 +564,12 @@ flowchart TB --- +## Troubleshooting + +Having issues? See **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** for solutions to common problems — LanceDB setup, missing projects, config loading, and more. + +--- + ## 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..52c1a34 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,155 @@ +# Troubleshooting + +Common issues and how to fix them. + +--- + +## Server won't start + +**Symptom:** `claude mcp add` succeeds but tools don't appear, or you see errors in Claude Code's MCP log. + +**Fixes:** + +1. **Check Node version.** Preflight requires Node 20+. + ```bash + node --version # Must be v20.x or higher + ``` + +2. **Missing dependencies.** If running from a clone: + ```bash + cd /path/to/preflight && npm install + ``` + +3. **Wrong path.** The `tsx` command needs the path to `src/index.ts` (dev) or use the npm binary: + ```bash + # From source + claude mcp add preflight -- npx tsx /absolute/path/to/preflight/src/index.ts + + # From npm + npm install -g preflight-dev + claude mcp add preflight -- preflight-dev + ``` + +4. **Restart Claude Code** after adding or changing MCP config. Tools won't appear until restart. + +--- + +## "No projects found" on search/timeline + +**Symptom:** `search_history` or `timeline_view` returns "No projects found for scope." + +**Cause:** `CLAUDE_PROJECT_DIR` isn't set, and no projects have been onboarded. + +**Fix:** Set the env var in your `.mcp.json`: + +```json +{ + "mcpServers": { + "preflight": { + "command": "npx", + "args": ["tsx", "/path/to/preflight/src/index.ts"], + "env": { + "CLAUDE_PROJECT_DIR": "/path/to/your/project" + } + } + } +} +``` + +Or run the `onboard_project` tool first to register a project. + +--- + +## LanceDB / vector search errors + +**Symptom:** Errors mentioning LanceDB, `@lancedb/lancedb`, or embeddings when using `search_history`. + +**Fixes:** + +1. **Native dependency build.** LanceDB includes native code. If `npm install` fails on your platform: + ```bash + # Make sure you have build tools + # macOS: + xcode-select --install + # Linux: + sudo apt install build-essential python3 + ``` + +2. **First-time indexing.** The timeline DB is created on first use. Run `onboard_project` to trigger initial indexing — this may take a moment for large session histories. + +3. **Disk space.** LanceDB stores vectors in `~/.preflight/timeline/`. Check disk space if indexing fails silently. + +--- + +## Embedding provider issues + +**Symptom:** Search returns no results or poor matches. + +**Default behavior:** Preflight uses a local embedding provider (no API key needed). For better quality, you can use OpenAI: + +```bash +claude mcp add preflight \ + -e EMBEDDING_PROVIDER=openai \ + -e OPENAI_API_KEY=sk-... \ + -- npx tsx /path/to/preflight/src/index.ts +``` + +Or set in `.preflight/config.yml`: + +```yaml +embeddings: + provider: openai + model: text-embedding-3-small +``` + +--- + +## Tools appear but do nothing + +**Symptom:** Tools are listed but `preflight_check` returns empty or generic responses. + +**Likely cause:** The project directory doesn't have meaningful git history or session data yet. + +**What to try:** + +1. Make sure you're in a git repo with commits. +2. Use `preflight_check` with a real prompt — it needs actual text to triage. +3. Run `session_stats` to verify session data is accessible. + +--- + +## `.preflight/` config not loading + +**Symptom:** Custom triage rules or thresholds in `.preflight/config.yml` are ignored. + +**Fixes:** + +1. The `.preflight/` directory must be in your **project root** (where `CLAUDE_PROJECT_DIR` points). +2. File must be valid YAML. Validate with: + ```bash + npx yaml < .preflight/config.yml + ``` +3. Check the [config example](/.preflight/) in this repo for correct structure. + +--- + +## High token usage from preflight itself + +**Symptom:** Preflight tools are consuming lots of tokens with long outputs. + +**Fix:** Switch to the `minimal` profile: + +```bash +claude mcp add preflight \ + -e PROMPT_DISCIPLINE_PROFILE=minimal \ + -- npx tsx /path/to/preflight/src/index.ts +``` + +Profiles: `strict` (most checks), `standard` (default), `minimal` (lightweight). + +--- + +## Still stuck? + +- Check [GitHub Issues](https://github.com/TerminalGravity/preflight/issues) for known bugs +- Open a new issue with your Node version, OS, and the error message From c022a29427f35552bded30fa45f48bf25a1b3d03 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Sun, 8 Mar 2026 18:17:12 -0700 Subject: [PATCH 3/6] add concrete usage examples for all major workflows - 8 real-world examples showing preflight_check triage, correction patterns, sub-agent enrichment, cross-service contracts, session health, scorecards, and semantic search - linked from README nav bar --- README.md | 2 +- examples/USAGE-EXAMPLES.md | 221 +++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 examples/USAGE-EXAMPLES.md diff --git a/README.md b/README.md index 5808680..0943a7d 100644 --- a/README.md +++ b/README.md @@ -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) · [Usage Examples](examples/USAGE-EXAMPLES.md) · [Tool Reference](#tool-reference) · [Configuration](#configuration) · [Scoring](#the-12-category-scorecard) diff --git a/examples/USAGE-EXAMPLES.md b/examples/USAGE-EXAMPLES.md new file mode 100644 index 0000000..2caa7e4 --- /dev/null +++ b/examples/USAGE-EXAMPLES.md @@ -0,0 +1,221 @@ +# Usage Examples + +Real workflows showing how preflight tools work in practice. + +--- + +## 1. The Basics: Let `preflight_check` Handle Everything + +You don't need to call individual tools. Just use `preflight_check` and it routes your prompt automatically. + +``` +You: "fix the auth bug" + +preflight_check classifies this as AMBIGUOUS → chains clarify_intent: + + ⚠️ Ambiguous prompt detected. I found 3 auth-related issues: + 1. JWT expiry not refreshing → src/auth/jwt.ts:42 + 2. OAuth callback returning 404 → src/auth/oauth.ts:118 + 3. Session cookie missing SameSite → src/middleware/session.ts:15 + + Which one? (Or say "all" and I'll sequence them.) +``` + +``` +You: "commit this" + +preflight_check classifies this as TRIVIAL → passes through with no intervention. +``` + +``` +You: "refactor the rewards system to use event sourcing" + +preflight_check classifies this as MULTI-STEP → chains scope_work + sequence_tasks: + + 📋 Execution plan (4 phases): + 1. Define event types in src/rewards/events.ts + 2. Create event store adapter (src/rewards/store.ts) + 3. Migrate existing mutations to event dispatches + 4. Add projection layer for read models + + Estimated: ~800 lines changed across 12 files + Risk: HIGH — touches billing integration + Suggest: checkpoint before starting +``` + +--- + +## 2. Starting a Session Right + +When you begin a coding session, `preflight_check` with a planning prompt sets context: + +``` +You: "I'm working on the checkout flow today. The Stripe webhook + handler drops events when the DB is under load." + +preflight_check classifies this as CLEAR (specific + scoped): + + ✅ Good prompt. Context loaded: + - Found 3 related sessions from last week touching src/payments/ + - Known correction: "webhook retry logic was added but not tested" (logged 3 days ago) + - Related contract: StripeWebhookEvent type in shared-types/ + + Ready to go. +``` + +--- + +## 3. Catching Repeat Mistakes with Correction Patterns + +First, log a correction when Claude goes wrong: + +``` +You: "No, don't use raw SQL — we always use Prisma for that table." + +→ Use log_correction to record this: + log_correction({ description: "Used raw SQL instead of Prisma for user table", + context: "src/db/queries.ts" }) + + ✅ Logged. I'll warn you next time. +``` + +Later, when a similar prompt comes in: + +``` +You: "write a query to get users by email" + +preflight_check → check_patterns fires: + + ⚠️ Pattern match: You previously corrected "used raw SQL instead of Prisma + for user table" (3 days ago). Make sure to use Prisma, not raw SQL. +``` + +--- + +## 4. Enriching Sub-Agent Tasks + +Before spawning a sub-agent, use `enrich_agent_task` to give it real context: + +``` +You: "have a sub-agent add input validation to the API routes" + +enrich_agent_task returns: + + 📦 Enriched task for sub-agent: + "Add Zod input validation to these API routes: + - POST /api/users (src/routes/users.ts:23) + - PUT /api/users/:id (src/routes/users.ts:67) + - POST /api/orders (src/routes/orders.ts:15) + + Existing patterns to follow: + - src/routes/auth.ts uses z.object() with .parse() in middleware + - Shared schemas in src/schemas/ + + Related types from api-gateway: + - CreateUserRequest, UpdateUserRequest (shared-types/api.d.ts) + + Run tests: npm test -- --grep 'routes'" +``` + +Without enrichment, the sub-agent would have to discover all of this itself (burning tokens). + +--- + +## 5. Cross-Service Contract Search + +When your prompt touches service boundaries: + +``` +You: "update the notification payload to include user preferences" + +search_contracts finds: + + 🔗 Contracts across 2 projects: + ├── current: NotificationPayload (src/types/notifications.ts:12) + ├── api-gateway: POST /notifications/send expects { userId, type, data } + └── shared-types: UserPreferences interface (src/user.d.ts:45) + + ⚠️ Changing NotificationPayload will break the api-gateway contract. + Update both, or version the payload type. +``` + +--- + +## 6. Session Health Monitoring + +Use `check_session_health` when things feel off: + +``` +check_session_health returns: + + 🏥 Session Health: + ├── Uncommitted files: 14 (⚠️ HIGH — commit soon) + ├── Time since last commit: 47 min (⚠️ overdue) + ├── Turn count: 38 + ├── Context usage: ~62% (OK) + └── Recommendation: Run checkpoint now. You have 14 uncommitted + files and haven't committed in 47 minutes. +``` + +--- + +## 7. End-of-Session Scorecard + +After a session, generate a scorecard: + +``` +generate_scorecard({ type: "session" }) + + 📊 Session Scorecard — March 8, 2026 + ───────────────────────────────────── + Plans .............. A (started with clear scope) + Clarification ...... B+ (82% of prompts had file refs) + Delegation ......... A (sub-agents got enriched context) + Follow-ups ......... C (3 vague follow-ups: "do the rest") + Token Efficiency ... B (7.2 calls/file — in ideal range) + Sequencing ......... A- (1 topic switch) + Compaction Mgmt .... A (committed before both compactions) + Session Lifecycle .. B (avg 22 min between commits) + Error Recovery ..... B+ (2 corrections, recovered in 1 msg each) + Workspace Hygiene .. A (CLAUDE.md up to date) + Continuity ......... A (read context on session start) + Verification ....... C- (no tests run at end ⚠️) + ───────────────────────────────────── + Overall: B+ (83/100) + + 💡 Tip: Run tests before ending sessions. Your Verification + score has been C or below for 3 sessions in a row. +``` + +--- + +## 8. Semantic Search Across History + +Search your past sessions for how you solved something before: + +``` +search_history({ query: "prisma migration rollback", scope: "all" }) + + 🔍 3 matches across 2 projects: + 1. [Feb 12] myapp session #47: "rolled back migration 20240212 + by creating a down migration manually — Prisma doesn't auto-generate" + Relevance: 0.94 + + 2. [Jan 28] api-gateway session #31: "used prisma migrate resolve + --rolled-back to mark failed migration" + Relevance: 0.87 + + 3. [Feb 3] myapp session #52: "tip: always test migrations on a + branch DB first — learned this the hard way" + Relevance: 0.71 +``` + +--- + +## Tips + +- **Start simple.** Just use `preflight_check` for everything — it routes automatically. +- **Log corrections.** The more you log, the smarter pattern matching gets. +- **Set `CLAUDE_PROJECT_DIR`.** Without it, timeline/search features can't find your sessions. +- **Add `.preflight/config.yml`** for team settings — see [examples/.preflight/](./preflight/). +- **Check your scorecard weekly.** Trends matter more than individual scores. From 51b133677b51d9b728b7cd77ef185020d369adcc Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Mon, 9 Mar 2026 07:51:38 -0700 Subject: [PATCH 4/6] fix: remove shell syntax from git.run() calls in 5 tools (#172) Replace shell pipes, redirects, and non-git commands with proper array-arg run() calls and JS-native equivalents: - sharpen-followup: array args for diff/status calls - sequence-tasks: ls-files with JS slice instead of pipe to head - audit-workspace: array args for diff, fs.readdirSync for find|wc - scope-work: ls-files + JS regex filter instead of pipe to grep - session-handoff: execFileSync for 'which' and 'gh' instead of shell command -v and piped gh calls Closes remaining tools from #172 (batch 2). --- src/tools/audit-workspace.ts | 14 ++++++++++++-- src/tools/scope-work.ts | 13 +++++++------ src/tools/sequence-tasks.ts | 3 ++- src/tools/session-handoff.ts | 14 +++++++++++--- src/tools/sharpen-followup.ts | 16 ++++++++-------- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/tools/audit-workspace.ts b/src/tools/audit-workspace.ts index d4306bd..8dcf4ec 100644 --- a/src/tools/audit-workspace.ts +++ b/src/tools/audit-workspace.ts @@ -1,6 +1,9 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { run } from "../lib/git.js"; import { readIfExists, findWorkspaceDocs } from "../lib/files.js"; +import { readdirSync } from "fs"; +import { join } from "path"; +import { PROJECT_DIR } from "../lib/files.js"; /** Extract top-level work areas from file paths generically */ function detectWorkAreas(files: string[]): Set { @@ -36,7 +39,8 @@ 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 recentFilesRaw = run(["diff", "--name-only", "HEAD~10"]); + const recentFiles = (!recentFilesRaw || recentFilesRaw.startsWith("[")) ? [] : recentFilesRaw.split("\n").filter(Boolean); const sections: string[] = []; // Doc freshness @@ -75,7 +79,13 @@ 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 = (() => { + try { + const testsDir = join(PROJECT_DIR, "tests"); + const entries = readdirSync(testsDir, { recursive: true, withFileTypes: false }) as string[]; + return entries.filter((e: string) => /\.(spec|test)\.ts$/.test(String(e))).length; + } catch { return 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`; diff --git a/src/tools/scope-work.ts b/src/tools/scope-work.ts index 9b5d971..ad5e9f5 100644 --- a/src/tools/scope-work.ts +++ b/src/tools/scope-work.ts @@ -17,10 +17,7 @@ const STOP_WORDS = new Set([ "like", "some", "each", "only", "need", "want", "please", "update", "change", ]); -/** Shell-escape a string for use inside single quotes */ -function shellEscape(s: string): string { - return s.replace(/'/g, "'\\''"); -} +/** (shellEscape removed — no longer needed after shell-syntax fix) */ /** Safely parse git porcelain status lines */ function parsePortelainFiles(porcelain: string): string[] { @@ -127,8 +124,12 @@ export function registerScopeWork(server: McpServer): void { .filter((k) => k.length > 2) .slice(0, 5); if (grepTerms.length > 0) { - const pattern = shellEscape(grepTerms.join("|")); - matchedFiles = run(`git ls-files | head -500 | grep -iE '${pattern}' | head -30`); + const allFiles = run(["ls-files"]); + if (allFiles && !allFiles.startsWith("[")) { + const pattern = new RegExp(grepTerms.join("|"), "i"); + matchedFiles = allFiles.split("\n").filter(Boolean).slice(0, 500) + .filter((f) => pattern.test(f)).slice(0, 30).join("\n"); + } } // Check which relevant dirs actually exist (with path traversal protection) diff --git a/src/tools/sequence-tasks.ts b/src/tools/sequence-tasks.ts index 22dea23..1c63258 100644 --- a/src/tools/sequence-tasks.ts +++ b/src/tools/sequence-tasks.ts @@ -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 gitFilesRaw = run(["ls-files"]); + const gitFiles = (!gitFilesRaw || gitFilesRaw.startsWith("[")) ? "" : gitFilesRaw.split("\n").slice(0, 1000).join("\n"); const knownDirs = new Set(); for (const f of gitFiles.split("\n").filter(Boolean)) { const parts = f.split("/"); diff --git a/src/tools/session-handoff.ts b/src/tools/session-handoff.ts index d199462..487f283 100644 --- a/src/tools/session-handoff.ts +++ b/src/tools/session-handoff.ts @@ -2,14 +2,17 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; +import { execFileSync } from "child_process"; import { run, 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`); - return !!result && !result.startsWith("[command failed"); + try { + execFileSync("which", [cmd], { stdio: "pipe", encoding: "utf-8" }); + return true; + } catch { return false; } } export function registerSessionHandoff(server: McpServer): void { @@ -44,7 +47,12 @@ 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 '[]'"); + let openPRs = "[]"; + try { + openPRs = execFileSync("gh", ["pr", "list", "--state", "open", "--json", "number,title,headRefName"], { + encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000, + }).trim(); + } catch { /* gh not available or failed */ } if (openPRs && openPRs !== "[]") { sections.push(`## Open PRs\n\`\`\`json\n${openPRs}\n\`\`\``); } diff --git a/src/tools/sharpen-followup.ts b/src/tools/sharpen-followup.ts index db5acaa..a65f114 100644 --- a/src/tools/sharpen-followup.ts +++ b/src/tools/sharpen-followup.ts @@ -27,15 +27,15 @@ function parsePortelainFiles(output: string): string[] { /** Get recently changed files, safe for first commit / shallow clones */ function getRecentChangedFiles(): string[] { // Try HEAD~1..HEAD, fall back to just staged, then unstaged - const commands = [ - "git diff --name-only HEAD~1 HEAD 2>/dev/null", - "git diff --name-only --cached 2>/dev/null", - "git diff --name-only 2>/dev/null", + const commands: string[][] = [ + ["diff", "--name-only", "HEAD~1", "HEAD"], + ["diff", "--name-only", "--cached"], + ["diff", "--name-only"], ]; const results = new Set(); - for (const cmd of commands) { - const out = run(cmd); - if (out) out.split("\n").filter(Boolean).forEach((f) => results.add(f)); + for (const args of commands) { + const out = run(args); + if (out && !out.startsWith("[")) out.split("\n").filter(Boolean).forEach((f) => results.add(f)); if (results.size > 0) break; // first successful source is enough } return [...results]; @@ -87,7 +87,7 @@ export function registerSharpenFollowup(server: McpServer): void { // Gather context to resolve ambiguity const contextFiles: string[] = [...(previous_files ?? [])]; const recentChanged = getRecentChangedFiles(); - const porcelainOutput = run("git status --porcelain 2>/dev/null"); + const porcelainOutput = run(["status", "--porcelain"]); const untrackedOrModified = parsePortelainFiles(porcelainOutput); const allKnownFiles = [...new Set([...contextFiles, ...recentChanged, ...untrackedOrModified])].filter(Boolean); From 7da334d631633d310908eb5e80c8871564af43aa Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Mon, 9 Mar 2026 07:54:08 -0700 Subject: [PATCH 5/6] fix: remove shell syntax from verify-completion, token-audit, enrich-agent-task (#172) - verify-completion: add shell() helper for non-git commands (tsc, test runners, build), use readFileSync for package.json, array args for git diff - token-audit: array args for git diff, fs.readFileSync/statSync instead of wc -l/wc -c piped through git.run() - enrich-agent-task: JS-native file filtering with getAllGitFiles() instead of piped grep/head, readFileSync instead of head command All 8 tools from #172 are now fixed. --- src/tools/enrich-agent-task.ts | 32 +++++++++++++++++------ src/tools/token-audit.ts | 17 +++++++------ src/tools/verify-completion.ts | 46 +++++++++++++++++++++++++--------- 3 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/tools/enrich-agent-task.ts b/src/tools/enrich-agent-task.ts index 236edfa..3e862ac 100644 --- a/src/tools/enrich-agent-task.ts +++ b/src/tools/enrich-agent-task.ts @@ -21,6 +21,13 @@ function detectPackageManager(): string { return "npm"; } +/** Get all git-tracked files (cached for a single tool call) */ +function getAllGitFiles(): string[] { + const raw = run(["ls-files"]); + if (!raw || raw.startsWith("[")) return []; + return raw.split("\n").filter(Boolean); +} + /** Find files in a target area using git-tracked files (project-agnostic) */ function findAreaFiles(area: string): string { if (!area) return getDiffFiles("HEAD~3"); @@ -29,12 +36,14 @@ 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 matched = getAllGitFiles().filter((f) => f.startsWith(safeArea)).slice(0, 20); + return matched.join("\n") || getDiffFiles("HEAD~3"); } // 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 pattern = new RegExp(safeArea, "i"); + const matched = getAllGitFiles().filter((f) => pattern.test(f)).slice(0, 20); + if (matched.length > 0) return matched.join("\n"); // Fallback to recently changed files return getDiffFiles("HEAD~3"); @@ -42,18 +51,27 @@ function findAreaFiles(area: string): string { /** 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 testPattern = /\.(spec|test)\.(ts|tsx|js|jsx)$/; + const allFiles = getAllGitFiles(); + const testFiles = allFiles.filter((f) => testPattern.test(f)); + + if (!area) return testFiles.slice(0, 10).join("\n"); 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"); + const areaPattern = new RegExp(safeArea, "i"); + const matched = testFiles.filter((f) => areaPattern.test(f)).slice(0, 10); + return (matched.length > 0 ? matched : testFiles.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 fullPath = join(PROJECT_DIR, firstFile); + const content = readFileSync(fullPath, "utf-8"); + return content.split("\n").slice(0, 30).join("\n"); + } catch { return "could not read file"; } } // --------------------------------------------------------------------------- diff --git a/src/tools/token-audit.ts b/src/tools/token-audit.ts index b7aad2c..c00e7f1 100644 --- a/src/tools/token-audit.ts +++ b/src/tools/token-audit.ts @@ -39,8 +39,8 @@ export function registerTokenAudit(server: McpServer): void { let wasteScore = 0; // 1. Git diff size & dirty file count - const diffStat = run("git diff --stat --no-color 2>/dev/null"); - const dirtyFiles = run("git diff --name-only 2>/dev/null"); + const diffStat = run(["diff", "--stat", "--no-color"]); + const dirtyFiles = run(["diff", "--name-only"]); const dirtyList = dirtyFiles.split("\n").filter(Boolean); const dirtyCount = dirtyList.length; @@ -62,9 +62,12 @@ export function registerTokenAudit(server: McpServer): void { const largeFiles: string[] = []; for (const f of dirtyList.slice(0, 30)) { - // Use shell-safe quoting instead of interpolation - const wc = run(`wc -l < '${shellEscape(f)}' 2>/dev/null`); - const lines = parseInt(wc) || 0; + // Count lines using Node.js fs instead of shell wc + let lines = 0; + try { + const content = readFileSync(join(PROJECT_DIR, f), "utf-8"); + lines = content.split("\n").length; + } catch { /* file may not exist */ } estimatedContextTokens += lines * AVG_LINE_BYTES * AVG_TOKENS_PER_BYTE; if (lines > 500) { largeFiles.push(`${f} (${lines} lines)`); @@ -80,8 +83,8 @@ export function registerTokenAudit(server: McpServer): void { // 3. CLAUDE.md bloat check const claudeMd = readIfExists("CLAUDE.md", 1); if (claudeMd !== null) { - const stat = run(`wc -c < '${shellEscape("CLAUDE.md")}' 2>/dev/null`); - const bytes = parseInt(stat) || 0; + let bytes = 0; + try { bytes = statSync(join(PROJECT_DIR, "CLAUDE.md")).size; } catch { /* */ } if (bytes > 5120) { patterns.push(`CLAUDE.md is ${(bytes / 1024).toFixed(1)}KB — injected every session, burns tokens on paste`); recommendations.push("Trim CLAUDE.md to essentials (<5KB). Move reference docs to files read on-demand"); diff --git a/src/tools/verify-completion.ts b/src/tools/verify-completion.ts index 732532f..5822c1e 100644 --- a/src/tools/verify-completion.ts +++ b/src/tools/verify-completion.ts @@ -2,8 +2,29 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { run, getStatus } from "../lib/git.js"; import { PROJECT_DIR } from "../lib/files.js"; -import { existsSync } from "fs"; +import { existsSync, readFileSync } from "fs"; import { join } from "path"; +import { execSync } from "child_process"; + +/** Run a shell command safely, returning last N lines of output */ +function shell(cmd: string, opts: { timeout?: number; tailLines?: number } = {}): string { + const { timeout = 30000, tailLines = 20 } = opts; + try { + const out = execSync(cmd, { + cwd: PROJECT_DIR, + encoding: "utf-8", + timeout, + maxBuffer: 2 * 1024 * 1024, + stdio: ["pipe", "pipe", "pipe"], + }); + const lines = out.trim().split("\n"); + return (tailLines ? lines.slice(-tailLines) : lines).join("\n"); + } catch (e: any) { + const out = (e.stdout || "") + (e.stderr || ""); + const lines = out.trim().split("\n"); + return (tailLines ? lines.slice(-tailLines) : lines).join("\n"); + } +} /** Detect package manager from lockfiles */ function detectPM(): string { @@ -34,7 +55,7 @@ function detectTestRunner(): string | null { /** Check if a build script exists in package.json */ function hasBuildScript(): boolean { try { - const pkg = JSON.parse(run("cat package.json 2>/dev/null")); + const pkg = JSON.parse(readFileSync(join(PROJECT_DIR, "package.json"), "utf-8")); return !!pkg?.scripts?.build; } catch { return false; } } @@ -55,7 +76,7 @@ export function registerVerifyCompletion(server: McpServer): void { const checks: { name: string; passed: boolean; detail: string }[] = []; // 1. Type check (single invocation, extract both result and count) - const tscOutput = run(`${pm === "npx" ? "npx" : pm} tsc --noEmit 2>&1 | tail -20`); + const tscOutput = shell(`${pm === "npx" ? "npx" : pm} tsc --noEmit 2>&1`, { tailLines: 20 }); const errorLines = tscOutput.split("\n").filter(l => /error TS\d+/.test(l)); const typePassed = errorLines.length === 0; checks.push({ @@ -80,39 +101,40 @@ export function registerVerifyCompletion(server: McpServer): void { // 3. Tests if (!skip_tests) { const runner = detectTestRunner(); - const changedFiles = run("git diff --name-only HEAD~1 2>/dev/null").split("\n").filter(Boolean); + const changedFilesRaw = run(["diff", "--name-only", "HEAD~1"]); + const changedFiles = (!changedFilesRaw || changedFilesRaw.startsWith("[")) ? [] : changedFilesRaw.split("\n").filter(Boolean); let testCmd = ""; if (runner === "playwright") { const runnerCmd = `${pm === "npx" ? "npx" : `${pm} exec`} playwright test`; if (test_scope && test_scope !== "all") { testCmd = test_scope.endsWith(".spec.ts") || test_scope.endsWith(".test.ts") - ? `${runnerCmd} ${test_scope} --reporter=line 2>&1 | tail -20` - : `${runnerCmd} --grep "${test_scope}" --reporter=line 2>&1 | tail -20`; + ? `${runnerCmd} ${test_scope} --reporter=line 2>&1` + : `${runnerCmd} --grep "${test_scope}" --reporter=line 2>&1`; } else { // Auto-detect from changed files const changedTests = changedFiles.filter(f => /\.(spec|test)\.(ts|tsx|js)$/.test(f)).slice(0, 5); if (changedTests.length > 0) { - testCmd = `${runnerCmd} ${changedTests.join(" ")} --reporter=line 2>&1 | tail -20`; + testCmd = `${runnerCmd} ${changedTests.join(" ")} --reporter=line 2>&1`; } } } else if (runner === "vitest" || runner === "jest") { const runnerCmd = `${pm === "npx" ? "npx" : `${pm} exec`} ${runner}`; if (test_scope && test_scope !== "all") { - testCmd = `${runnerCmd} --run ${test_scope} 2>&1 | tail -20`; + testCmd = `${runnerCmd} --run ${test_scope} 2>&1`; } else { const changedTests = changedFiles.filter(f => /\.(spec|test)\.(ts|tsx|js)$/.test(f)).slice(0, 5); if (changedTests.length > 0) { - testCmd = `${runnerCmd} --run ${changedTests.join(" ")} 2>&1 | tail -20`; + testCmd = `${runnerCmd} --run ${changedTests.join(" ")} 2>&1`; } } } else if (test_scope) { // No recognized runner but scope given — try npm test - testCmd = `${pm} test 2>&1 | tail -20`; + testCmd = `${pm} test 2>&1`; } if (testCmd) { - const testResult = run(testCmd, { timeout: 120000 }); + const testResult = shell(testCmd, { timeout: 120000, tailLines: 20 }); const testPassed = /pass/i.test(testResult) && !/fail/i.test(testResult); checks.push({ name: "Tests", @@ -130,7 +152,7 @@ export function registerVerifyCompletion(server: McpServer): void { // 4. Build check (only if build script exists and not skipped) if (!skip_build && hasBuildScript()) { - const buildCheck = run(`${pm === "npx" ? "npm run" : pm} build 2>&1 | tail -10`, { timeout: 60000 }); + const buildCheck = shell(`${pm === "npx" ? "npm run" : pm} build 2>&1`, { timeout: 60000, tailLines: 10 }); const buildPassed = !/\b[Ee]rror\b/.test(buildCheck) || /Successfully compiled/.test(buildCheck); checks.push({ name: "Build", From 2ae49f42b90172449ae3466c46de75ba5c2fabdd Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Mon, 9 Mar 2026 07:59:15 -0700 Subject: [PATCH 6/6] fix: bin entry now auto-detects TTY vs MCP server mode (#172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preflight-dev binary previously always ran the init wizard, breaking `claude mcp add preflight -- preflight-dev` (Option C). Now auto-detects: TTY → init wizard, piped → MCP server. Explicit flags: `preflight-dev init` / `preflight-dev serve`. Also fixes init script writing broken MCP config (double assignment with incorrect node_modules path). --- README.md | 4 +++- TROUBLESHOOTING.md | 17 +++++++++++++++++ bin/cli.js | 19 +++++++++++++++---- memory/2026-03-08.md | 9 +++++++++ src/cli/init.ts | 11 +---------- 5 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 memory/2026-03-08.md diff --git a/README.md b/README.md index 0943a7d..a9a4291 100644 --- a/README.md +++ b/README.md @@ -119,9 +119,11 @@ Restart Claude Code. The tools activate automatically. ```bash npm install -g preflight-dev -claude mcp add preflight -- preflight-dev +claude mcp add preflight -- preflight-dev serve ``` +> **Tip:** Running `preflight-dev` in a terminal (TTY) launches the interactive setup wizard. When piped (like `claude mcp add`), it auto-detects and starts the MCP server. You can also force either mode with `preflight-dev init` or `preflight-dev serve`. + --- ## How It Works diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 52c1a34..b2f9009 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -149,6 +149,23 @@ Profiles: `strict` (most checks), `standard` (default), `minimal` (lightweight). --- +## Init wizard launches instead of MCP server + +**Symptom:** Running `preflight-dev` via `claude mcp add` opens the interactive setup wizard instead of starting the MCP server. + +**Cause:** Older versions of the bin entry always ran the init wizard regardless of context. + +**Fix:** Update to the latest version and use `serve` to force server mode: + +```bash +npm install -g preflight-dev@latest +claude mcp add preflight -- preflight-dev serve +``` + +The binary auto-detects TTY vs piped stdin, but `serve` makes it explicit. You can also force the wizard with `preflight-dev init`. + +--- + ## Still stuck? - Check [GitHub Issues](https://github.com/TerminalGravity/preflight/issues) for known bugs diff --git a/bin/cli.js b/bin/cli.js index 69b21ad..5d87970 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -1,11 +1,22 @@ #!/usr/bin/env node -// This is a shim that loads the compiled TypeScript CLI +// Dual-mode entry point: +// - Interactive TTY → run the init wizard (preflight-dev init) +// - Piped / non-TTY → start the MCP server (used by `claude mcp add`) import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// Load the compiled CLI -const cliPath = join(__dirname, '../dist/cli/init.js'); -await import(cliPath); \ No newline at end of file +const forceInit = process.argv.includes('init'); +const forceServe = process.argv.includes('serve'); + +if (forceInit || (!forceServe && process.stdin.isTTY)) { + // Interactive: run setup wizard + const cliPath = join(__dirname, '../dist/cli/init.js'); + await import(cliPath); +} else { + // Non-interactive (MCP stdio): start the server + const serverPath = join(__dirname, '../dist/index.js'); + await import(serverPath); +} \ No newline at end of file diff --git a/memory/2026-03-08.md b/memory/2026-03-08.md new file mode 100644 index 0000000..c5318f9 --- /dev/null +++ b/memory/2026-03-08.md @@ -0,0 +1,9 @@ +# 2026-03-08 + +## Dev Sprint +- Updated PR #117 with 25 comprehensive tests for `src/lib/git.ts` +- Covers: run() (array/string args, trimming, timeouts, ENOENT, stderr/stdout fallbacks), all convenience functions, getDiffFiles/getDiffStat fallback logic +- Total test suite: 68 tests, all passing +- Branch: `test/git-lib-coverage` +- Many shell syntax PRs already open (#175, #176, #178, #180) — avoided piling on more +- Main branch has 3 unpushed commits (protected branch requires PR review) diff --git a/src/cli/init.ts b/src/cli/init.ts index 996906d..18caa4f 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -106,16 +106,7 @@ async function main(): Promise { config.mcpServers["preflight"] = { command: "npx", - args: ["-y", "preflight-dev@latest"], - env, - }; - - // For the actual server entry point, we need to point to index.ts via tsx - // But npx will resolve the bin entry which is the init script - // So use a different approach: command runs the server - config.mcpServers["preflight"] = { - command: "npx", - args: ["-y", "tsx", "node_modules/preflight/src/index.ts"], + args: ["-y", "preflight-dev@latest", "serve"], env, };