Skip to content
Open
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
151 changes: 151 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,154 @@ Then commit `.preflight/` to your repo — your whole team gets the same preflig
| `contracts/*.yml` | Manual type/interface definitions for cross-service awareness | No — auto-extraction works without it |

All files are optional. Preflight works out of the box with zero config — these files let you tune it to your codebase.

---

## Usage Examples

Once preflight is registered as an MCP server, these tools are available inside Claude Code. Here's what real usage looks like for the most common tools.

### `preflight_check` — The unified entry point

This is the tool you'll use most. It triages your prompt and automatically chains the right checks.

**Vague prompt → caught and clarified:**

```
You: "fix the tests"

preflight_check fires → triage level: AMBIGUOUS

🚨 Ambiguous prompt detected — clarification needed:

Which tests? I found:
• 14 test files in tests/
• 3 currently failing:
- tests/triage.test.ts (assertion error line 42)
- tests/config.test.ts (timeout)
- tests/patterns.test.ts (missing fixture)

Also: 2 correction patterns matched — last time "fix tests" led to
editing the wrong test file. Be specific about which test and what
the expected behavior should be.
```

**Specific prompt → green light with context:**

```
You: "refactor src/lib/triage.ts to extract the keyword matching into its own function"

preflight_check fires → triage level: CLEAR

✅ Prompt is clear. Context gathered:
• Branch: feat/triage-refactor (3 commits ahead of main)
• src/lib/triage.ts — 287 lines, last modified 2h ago
• 2 files import from triage.ts: preflight-check.ts, clarify-intent.ts
• No related correction patterns found
```

### `prompt_score` — Gamified prompt quality

Scores your prompt on specificity, context, and actionability. Tracks your session average.

```
You: "add error handling"

prompt_score result:
Score: 3/10 ⚠️
- Missing: which file? what errors? what should happen on failure?
- Session average: 6.2 (↓ from 7.1)
- Tip: try "add try/catch to src/lib/embeddings.ts embedText() —
catch fetch failures and return a fallback zero vector"
```

### `scope_work` — Plan before you build

Generates a scoped work plan with file targets, risk areas, and estimated complexity.

```
You: "add OAuth login with Google"

scope_work result:
📋 Work Plan: Google OAuth Integration

Files to create:
• src/auth/google.ts — OAuth flow handler
• src/auth/callback.ts — Token exchange endpoint

Files to modify:
• src/middleware/session.ts — add OAuth session type
• src/types.ts — add GoogleUser interface

Dependencies to add:
• googleapis or passport-google-oauth20

Risk areas:
• Session cookie config (SameSite issues in dev)
• Redirect URI mismatch between dev/prod

Estimated scope: Medium (4-6 files, ~200 lines)
Related contracts: UserSession interface in api.yml
```

### `estimate_cost` — Know before you go

Estimates token spend for a task based on historical patterns.

```
You: "rewrite the entire scoring engine"

estimate_cost result:
💰 Estimated cost for this task:
• Estimated tokens: ~45,000 (input) + ~12,000 (output)
• Estimated cost: ~$0.85
• Based on: 3 similar refactoring tasks in history
• Warning: Large refactors average 2.1 correction cycles —
consider breaking into smaller pieces
```

### `log_correction` — Teach preflight your patterns

When Claude goes wrong, log it so preflight warns you next time.

```
You: log_correction("said 'update the config' and Claude edited
package.json instead of .preflight/config.yml")

✅ Correction logged.
Pattern: "update config" → wrong file target
Next time you say "update config", preflight will ask which config file.
```

### `search_history` — Find anything from past sessions

Semantic search across all your Claude Code session history.

```
You: "how did I set up the database migrations last month?"

search_history result:
Found 3 relevant sessions:

1. [Feb 14] "Set up Prisma migrations for user table"
→ Created prisma/migrations/001_users.sql
→ Used `prisma migrate dev --name init`

2. [Feb 16] "Fix migration conflict after schema change"
→ Resolved by resetting dev DB: `prisma migrate reset`

3. [Feb 20] "Add index to sessions table"
→ prisma/migrations/003_session_index.sql
```

### Workflow tip: Let `preflight_check` run automatically

Add this to your Claude Code custom instructions (CLAUDE.md):

```markdown
Before executing any task, run preflight_check with the user's prompt.
If the triage level is AMBIGUOUS or higher, present the clarification
before proceeding. Never skip preflight on multi-file changes.
```

This makes preflight automatic — you don't have to remember to call it.
21 changes: 20 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 @@ -87,3 +87,22 @@ export function getDiffStat(ref = "HEAD~5"): string {
if (!fallback.startsWith("[")) return fallback;
return "no diff stats available";
}

/**
* Run an arbitrary shell command string (with pipes, redirects, etc.).
* Use sparingly — prefer `run()` with explicit args for git commands.
* Returns stdout trimmed, or empty string on failure.
*/
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 {
return "";
}
}
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(l => l && !l.startsWith("["));
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")) || 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
8 changes: 4 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,11 @@ ${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]);
const result = shell(`${addCmd} && git commit -m "${commitMsg.replace(/"/g, '\\"')}" 2>&1`);
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'") || "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
21 changes: 13 additions & 8 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,36 @@ 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`);
return shell(`git ls-files -- '${safeArea}*' 2>/dev/null | head -20`);
}

// 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 files = shell(`git ls-files 2>/dev/null | grep -i '${safeArea}' | head -20`);
if (files) return files;

// 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");
if (!area) return shell("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10");

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 tests = shell(`git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | grep -i '${safeArea}' | head -10`);
return tests || shell("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10");
}

/** 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
4 changes: 2 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,7 @@ 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 gitFiles = shell("git ls-files 2>/dev/null | head -1000");
const knownDirs = new Set<string>();
for (const f of gitFiles.split("\n").filter(Boolean)) {
const parts = f.split("/");
Expand Down
8 changes: 4 additions & 4 deletions src/tools/session-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ 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`);
return !!result && !result.startsWith("[command failed");
const result = shell(`command -v ${cmd} 2>/dev/null`);
return !!result;
}

export function registerSessionHandoff(server: McpServer): void {
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") || "[]";
if (openPRs && openPRs !== "[]") {
sections.push(`## Open PRs\n\`\`\`json\n${openPRs}\n\`\`\``);
}
Expand Down
Loading
Loading