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 diff --git a/src/tools/generate-scorecard.ts b/src/tools/generate-scorecard.ts index c15576c..20ec960 100644 --- a/src/tools/generate-scorecard.ts +++ b/src/tools/generate-scorecard.ts @@ -76,12 +76,12 @@ interface ParsedSession { } function classifyEvents(events: TimelineEvent[]): ParsedSession { - const userMessages = events.filter((e) => e.type === "user_prompt"); - const assistantMessages = events.filter((e) => e.type === "assistant_response"); + const userMessages = events.filter((e) => e.type === "prompt" || e.type === "user_prompt"); + const assistantMessages = events.filter((e) => e.type === "assistant" || e.type === "assistant_response"); const toolCalls = events.filter((e) => e.type === "tool_call"); const corrections = events.filter((e) => e.type === "correction"); const compactions = events.filter((e) => e.type === "compaction"); - const commits = events.filter((e) => e.type === "git_commit"); + const commits = events.filter((e) => e.type === "git_commit" || e.type === "commit"); const subAgentSpawns = events.filter((e) => e.type === "sub_agent_spawn"); let durationMinutes = 0; @@ -178,10 +178,10 @@ function scoreFollowUpSpecificity(sessions: ParsedSession[]): CategoryScore { for (const s of sessions) { for (let i = 0; i < s.events.length; i++) { const ev = s.events[i]; - if (ev.type !== "user_prompt") continue; + if (ev.type !== "prompt" && ev.type !== "user_prompt") continue; // Check if preceded by assistant - const prev = s.events.slice(0, i).reverse().find((e) => e.type === "assistant_response" || e.type === "user_prompt"); - if (prev?.type !== "assistant_response") continue; + const prev = s.events.slice(0, i).reverse().find((e) => e.type === "assistant" || e.type === "assistant_response" || e.type === "prompt" || e.type === "user_prompt"); + if (prev?.type !== "assistant" && prev?.type !== "assistant_response") continue; followUps++; if (hasFileRef(ev.content) || ev.content.length >= 50) { @@ -271,7 +271,7 @@ function scoreCompactionManagement(sessions: ParsedSession[]): CategoryScore { totalCompactions++; const cIdx = s.events.indexOf(c); const nearby = s.events.slice(Math.max(0, cIdx - 10), cIdx); - if (nearby.some((e) => e.type === "git_commit")) covered++; + if (nearby.some((e) => e.type === "git_commit" || e.type === "commit")) covered++; } } if (totalCompactions === 0) return { name: "Compaction Management", score: 100, grade: "A+", evidence: "No compactions needed — sessions stayed manageable." }; @@ -311,7 +311,7 @@ function scoreErrorRecovery(sessions: ParsedSession[]): CategoryScore { totalCorrections++; const cIdx = s.events.indexOf(c); const after = s.events.slice(cIdx + 1, cIdx + 3); - if (after.some((e) => e.type === "tool_call" || e.type === "assistant_response")) fastRecoveries++; + if (after.some((e) => e.type === "tool_call" || e.type === "assistant" || e.type === "assistant_response")) fastRecoveries++; } } if (totalCorrections === 0) return { name: "Error Recovery", score: 95, grade: "A", evidence: "No corrections needed." }; diff --git a/tests/lib/scorecard-classify.test.ts b/tests/lib/scorecard-classify.test.ts new file mode 100644 index 0000000..b0b3f3c --- /dev/null +++ b/tests/lib/scorecard-classify.test.ts @@ -0,0 +1,70 @@ +/** + * Tests that generate_scorecard's classifyEvents correctly handles + * both legacy ("user_prompt"/"assistant_response") and actual + * session-parser event types ("prompt"/"assistant"). + * + * Regression test for: event type mismatch between session-parser + * output and scorecard filtering (scorecard expected "user_prompt" + * but parser emits "prompt"). + */ +import { describe, it, expect } from "vitest"; + +// We can't easily import the private classifyEvents, so we replicate +// the filtering logic that was buggy and verify the fix. + +const PROMPT_TYPES = ["prompt", "user_prompt"]; +const ASSISTANT_TYPES = ["assistant", "assistant_response"]; +const COMMIT_TYPES = ["git_commit", "commit"]; + +function isUserMessage(type: string): boolean { + return type === "prompt" || type === "user_prompt"; +} + +function isAssistantMessage(type: string): boolean { + return type === "assistant" || type === "assistant_response"; +} + +function isCommit(type: string): boolean { + return type === "git_commit" || type === "commit"; +} + +describe("scorecard event type matching", () => { + it("should match session-parser 'prompt' type as user message", () => { + // session-parser emits "prompt", not "user_prompt" + expect(isUserMessage("prompt")).toBe(true); + expect(isUserMessage("user_prompt")).toBe(true); + expect(isUserMessage("assistant")).toBe(false); + }); + + it("should match session-parser 'assistant' type as assistant message", () => { + // session-parser emits "assistant", not "assistant_response" + expect(isAssistantMessage("assistant")).toBe(true); + expect(isAssistantMessage("assistant_response")).toBe(true); + expect(isAssistantMessage("prompt")).toBe(false); + }); + + it("should match both commit type variants", () => { + expect(isCommit("git_commit")).toBe(true); + expect(isCommit("commit")).toBe(true); + expect(isCommit("tool_call")).toBe(false); + }); + + it("should correctly filter a mixed event list", () => { + const events = [ + { type: "prompt" }, + { type: "assistant" }, + { type: "tool_call" }, + { type: "correction" }, + { type: "user_prompt" }, // legacy + { type: "assistant_response" }, // legacy + { type: "compaction" }, + { type: "sub_agent_spawn" }, + ]; + + const userMessages = events.filter((e) => isUserMessage(e.type)); + const assistantMessages = events.filter((e) => isAssistantMessage(e.type)); + + expect(userMessages).toHaveLength(2); + expect(assistantMessages).toHaveLength(2); + }); +});