From d5f0d016aff065ead0401b10d35b29cd29378168 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Wed, 18 Mar 2026 13:41:13 -0700 Subject: [PATCH 1/4] 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/4] 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 9c1d97cbe4b4328f9af648abdedee5515cb58bbc Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 13:45:04 -0700 Subject: [PATCH 3/4] feat: add export_timeline tool for markdown session reports Adds a new MCP tool that generates structured markdown reports from timeline data including summary stats, daily activity, commits, tool usage breakdown, and error/correction highlights. Supports relative date ranges, scope filtering, branch/author filters, and custom report titles. Closes #5 --- src/index.ts | 2 + src/tools/export-timeline.ts | 276 +++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 src/tools/export-timeline.ts diff --git a/src/index.ts b/src/index.ts index e7e9d00..c2a525a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,7 @@ import { registerScanSessions } from "./tools/scan-sessions.js"; import { registerGenerateScorecard } from "./tools/generate-scorecard.js"; import { registerSearchContracts } from "./tools/search-contracts.js"; import { registerEstimateCost } from "./tools/estimate-cost.js"; +import { registerExportTimeline } from "./tools/export-timeline.js"; // Validate related projects from config function validateRelatedProjects(): void { @@ -110,6 +111,7 @@ const toolRegistry: Array<[string, RegisterFn]> = [ ["generate_scorecard", registerGenerateScorecard], ["estimate_cost", registerEstimateCost], ["search_contracts", registerSearchContracts], + ["export_timeline", registerExportTimeline], ]; let registered = 0; diff --git a/src/tools/export-timeline.ts b/src/tools/export-timeline.ts new file mode 100644 index 0000000..e4672cc --- /dev/null +++ b/src/tools/export-timeline.ts @@ -0,0 +1,276 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getTimeline, listIndexedProjects } from "../lib/timeline-db.js"; +import { getRelatedProjects } from "../lib/config.js"; +import type { SearchScope } from "../types.js"; + +const RELATIVE_DATE_RE = /^(\d+)(days?|weeks?|months?|years?)$/; + +function parseRelativeDate(input: string): string { + const match = input.match(RELATIVE_DATE_RE); + if (!match) return input; + const [, numStr, unit] = match; + const num = parseInt(numStr, 10); + const d = new Date(); + if (unit.startsWith("day")) d.setDate(d.getDate() - num); + else if (unit.startsWith("week")) d.setDate(d.getDate() - num * 7); + else if (unit.startsWith("month")) d.setMonth(d.getMonth() - num); + else if (unit.startsWith("year")) d.setFullYear(d.getFullYear() - num); + return d.toISOString(); +} + +async function getSearchProjects(scope: SearchScope): Promise { + const currentProject = process.env.CLAUDE_PROJECT_DIR; + switch (scope) { + case "current": + return currentProject ? [currentProject] : []; + case "related": { + const related = getRelatedProjects(); + return currentProject ? [currentProject, ...related] : related; + } + case "all": { + const projects = await listIndexedProjects(); + return projects.map((p) => p.project); + } + default: + return currentProject ? [currentProject] : []; + } +} + +const TYPE_ICONS: Record = { + prompt: "💬", + assistant: "🤖", + tool_call: "🔧", + correction: "❌", + commit: "📦", + compaction: "🗜️", + sub_agent_spawn: "🚀", + error: "⚠️", +}; + +interface TimelineEvent { + timestamp?: string; + type: string; + content?: string; + summary?: string; + commit_hash?: string; + tool_name?: string; + metadata?: string; + project?: string; +} + +function computeStats(events: TimelineEvent[]) { + const byType: Record = {}; + const byDay: Record = {}; + let totalPromptChars = 0; + let promptCount = 0; + + for (const e of events) { + byType[e.type] = (byType[e.type] || 0) + 1; + if (e.timestamp) { + const day = new Date(e.timestamp).toISOString().slice(0, 10); + byDay[day] = (byDay[day] || 0) + 1; + } + if (e.type === "prompt" && e.content) { + totalPromptChars += e.content.length; + promptCount++; + } + } + + return { byType, byDay, avgPromptLen: promptCount > 0 ? Math.round(totalPromptChars / promptCount) : 0, promptCount }; +} + +function generateMarkdownReport( + events: TimelineEvent[], + opts: { project: string; since?: string; until?: string; title?: string } +): string { + const stats = computeStats(events); + const sortedDays = Object.keys(stats.byDay).sort().reverse(); + const dateRange = + sortedDays.length > 1 + ? `${sortedDays[sortedDays.length - 1]} → ${sortedDays[0]}` + : sortedDays[0] || "no events"; + + const lines: string[] = []; + + // Title + lines.push(`# ${opts.title || `Session Report: ${opts.project}`}`); + lines.push(""); + lines.push(`**Period:** ${dateRange} `); + lines.push(`**Total events:** ${events.length} `); + lines.push(`**Generated:** ${new Date().toISOString().slice(0, 16)}Z`); + lines.push(""); + + // Summary stats + lines.push("## Summary"); + lines.push(""); + lines.push("| Metric | Value |"); + lines.push("|--------|-------|"); + for (const [type, count] of Object.entries(stats.byType).sort((a, b) => b[1] - a[1])) { + const icon = TYPE_ICONS[type] || "❓"; + lines.push(`| ${icon} ${type} | ${count} |`); + } + if (stats.promptCount > 0) { + lines.push(`| Avg prompt length | ${stats.avgPromptLen} chars |`); + } + lines.push(""); + + // Activity by day + lines.push("## Daily Activity"); + lines.push(""); + for (const day of sortedDays) { + const count = stats.byDay[day]; + const bar = "█".repeat(Math.min(count, 40)); + lines.push(`- **${day}** ${bar} ${count}`); + } + lines.push(""); + + // Error/correction highlights + const issues = events.filter((e) => e.type === "error" || e.type === "correction"); + if (issues.length > 0) { + lines.push("## Issues & Corrections"); + lines.push(""); + for (const e of issues) { + const time = e.timestamp ? new Date(e.timestamp).toISOString().slice(0, 16) : "??"; + const icon = TYPE_ICONS[e.type] || "❓"; + const content = (e.content || e.summary || "").slice(0, 200).replace(/\n/g, " "); + lines.push(`- ${icon} **${time}** — ${content}`); + } + lines.push(""); + } + + // Commits + const commits = events.filter((e) => e.type === "commit"); + if (commits.length > 0) { + lines.push("## Commits"); + lines.push(""); + for (const e of commits) { + const time = e.timestamp ? new Date(e.timestamp).toISOString().slice(0, 16) : "??"; + const hash = e.commit_hash ? e.commit_hash.slice(0, 7) : ""; + const msg = (e.content || e.summary || "").slice(0, 120).replace(/\n/g, " "); + lines.push(`- \`${hash}\` ${msg} _(${time})_`); + } + lines.push(""); + } + + // Tool usage breakdown + const toolCalls = events.filter((e) => e.type === "tool_call"); + if (toolCalls.length > 0) { + const toolCounts: Record = {}; + for (const e of toolCalls) { + const name = e.tool_name || "unknown"; + toolCounts[name] = (toolCounts[name] || 0) + 1; + } + lines.push("## Tool Usage"); + lines.push(""); + lines.push("| Tool | Calls |"); + lines.push("|------|-------|"); + for (const [name, count] of Object.entries(toolCounts).sort((a, b) => b[1] - a[1])) { + lines.push(`| ${name} | ${count} |`); + } + lines.push(""); + } + + // Detailed timeline (last 30 events) + const recent = events.slice(-30); + lines.push("## Recent Activity (last 30 events)"); + lines.push(""); + for (const e of recent) { + const time = e.timestamp ? new Date(e.timestamp).toISOString().slice(11, 16) : "??:??"; + const day = e.timestamp ? new Date(e.timestamp).toISOString().slice(0, 10) : ""; + const icon = TYPE_ICONS[e.type] || "❓"; + const content = (e.content || e.summary || "").slice(0, 100).replace(/\n/g, " "); + lines.push(`- ${day} ${time} ${icon} ${content}`); + } + lines.push(""); + + return lines.join("\n"); +} + +export function registerExportTimeline(server: McpServer) { + server.tool( + "export_timeline", + "Export timeline data as a structured markdown report with summary statistics, daily activity, commits, tool usage, and issue highlights. Use for weekly summaries, session reviews, and team reports.", + { + scope: z + .enum(["current", "related", "all"]) + .default("current") + .describe("Search scope: current project, related projects, or all indexed"), + project: z.string().optional().describe("Filter to a specific project (overrides scope)"), + since: z + .string() + .optional() + .describe('Start date (ISO or relative like "7days", "2weeks", "1month")'), + until: z.string().optional().describe("End date (ISO or relative)"), + title: z.string().optional().describe("Custom report title"), + branch: z.string().optional().describe("Filter to branch"), + author: z.string().optional().describe("Filter commits to author (partial match)"), + limit: z.number().default(500).describe("Max events to include"), + }, + async (params) => { + const since = params.since ? parseRelativeDate(params.since) : undefined; + const until = params.until ? parseRelativeDate(params.until) : undefined; + + let projectDirs: string[]; + if (params.project) { + projectDirs = [params.project]; + } else { + projectDirs = await getSearchProjects(params.scope); + } + + if (projectDirs.length === 0) { + return { + content: [ + { + type: "text" as const, + text: `No projects found for scope "${params.scope}". Make sure CLAUDE_PROJECT_DIR is set or projects are onboarded.`, + }, + ], + }; + } + + let events = await getTimeline({ + project_dirs: projectDirs, + project: undefined, + branch: params.branch, + since, + until, + type: undefined, + limit: params.limit, + offset: 0, + }); + + // Post-filter by author + if (params.author) { + const authorLower = params.author.toLowerCase(); + events = events.filter((e: any) => { + if (e.type !== "commit") return true; + try { + const meta = JSON.parse(e.metadata || "{}"); + return (meta.author || "").toLowerCase().includes(authorLower); + } catch { + return true; + } + }); + } + + if (events.length === 0) { + return { + content: [{ type: "text" as const, text: "No events found for the given filters. Nothing to export." }], + }; + } + + const projectName = params.project || projectDirs[0] || "all projects"; + const report = generateMarkdownReport(events, { + project: projectName, + since, + until, + title: params.title, + }); + + return { + content: [{ type: "text" as const, text: report }], + }; + } + ); +} From 1ecc1c11a4682f47459c330a56e5d01760a8294d Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 14:14:56 -0700 Subject: [PATCH 4/4] test: add unit tests for export-timeline helpers Export parseRelativeDate, computeStats, generateMarkdownReport and TimelineEvent from export-timeline.ts so they can be tested directly. 17 tests covering: - relative date parsing (days, weeks, months, years, passthrough) - stats computation (by type, by day, avg prompt length, empty input) - markdown report generation (title, commits, errors, tool usage, edge cases) --- src/tools/export-timeline.ts | 8 +- tests/export-timeline.test.ts | 142 ++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 tests/export-timeline.test.ts diff --git a/src/tools/export-timeline.ts b/src/tools/export-timeline.ts index e4672cc..d588ef8 100644 --- a/src/tools/export-timeline.ts +++ b/src/tools/export-timeline.ts @@ -6,7 +6,7 @@ import type { SearchScope } from "../types.js"; const RELATIVE_DATE_RE = /^(\d+)(days?|weeks?|months?|years?)$/; -function parseRelativeDate(input: string): string { +export function parseRelativeDate(input: string): string { const match = input.match(RELATIVE_DATE_RE); if (!match) return input; const [, numStr, unit] = match; @@ -48,7 +48,7 @@ const TYPE_ICONS: Record = { error: "⚠️", }; -interface TimelineEvent { +export interface TimelineEvent { timestamp?: string; type: string; content?: string; @@ -59,7 +59,7 @@ interface TimelineEvent { project?: string; } -function computeStats(events: TimelineEvent[]) { +export function computeStats(events: TimelineEvent[]) { const byType: Record = {}; const byDay: Record = {}; let totalPromptChars = 0; @@ -80,7 +80,7 @@ function computeStats(events: TimelineEvent[]) { return { byType, byDay, avgPromptLen: promptCount > 0 ? Math.round(totalPromptChars / promptCount) : 0, promptCount }; } -function generateMarkdownReport( +export function generateMarkdownReport( events: TimelineEvent[], opts: { project: string; since?: string; until?: string; title?: string } ): string { diff --git a/tests/export-timeline.test.ts b/tests/export-timeline.test.ts new file mode 100644 index 0000000..f3e9652 --- /dev/null +++ b/tests/export-timeline.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect, vi } from "vitest"; +import { + parseRelativeDate, + computeStats, + generateMarkdownReport, + type TimelineEvent, +} from "../src/tools/export-timeline.js"; + +describe("parseRelativeDate", () => { + it("returns ISO strings unchanged", () => { + expect(parseRelativeDate("2026-01-15T00:00:00Z")).toBe("2026-01-15T00:00:00Z"); + }); + + it("parses '7days' as relative", () => { + const now = new Date(); + const result = new Date(parseRelativeDate("7days")); + const diff = now.getTime() - result.getTime(); + // Should be ~7 days (allow 1 second tolerance) + expect(diff).toBeGreaterThan(6.99 * 86400000); + expect(diff).toBeLessThan(7.01 * 86400000); + }); + + it("parses '1day' singular", () => { + const result = new Date(parseRelativeDate("1day")); + const diff = Date.now() - result.getTime(); + expect(diff).toBeGreaterThan(0.99 * 86400000); + expect(diff).toBeLessThan(1.01 * 86400000); + }); + + it("parses '2weeks'", () => { + const result = new Date(parseRelativeDate("2weeks")); + const diff = Date.now() - result.getTime(); + expect(diff).toBeGreaterThan(13.99 * 86400000); + expect(diff).toBeLessThan(14.01 * 86400000); + }); + + it("parses '1month'", () => { + const result = new Date(parseRelativeDate("1month")); + // Just check it's in the past and roughly a month + expect(result.getTime()).toBeLessThan(Date.now()); + }); + + it("parses '1year'", () => { + const result = new Date(parseRelativeDate("1year")); + const diff = Date.now() - result.getTime(); + expect(diff).toBeGreaterThan(360 * 86400000); + }); + + it("returns non-matching input as-is", () => { + expect(parseRelativeDate("garbage")).toBe("garbage"); + expect(parseRelativeDate("")).toBe(""); + }); +}); + +describe("computeStats", () => { + const events: TimelineEvent[] = [ + { type: "prompt", timestamp: "2026-03-01T10:00:00Z", content: "hello world" }, + { type: "prompt", timestamp: "2026-03-01T11:00:00Z", content: "do something" }, + { type: "commit", timestamp: "2026-03-01T12:00:00Z", content: "fix bug" }, + { type: "error", timestamp: "2026-03-02T09:00:00Z", content: "oops" }, + { type: "tool_call", timestamp: "2026-03-02T10:00:00Z" }, + ]; + + it("counts events by type", () => { + const stats = computeStats(events); + expect(stats.byType["prompt"]).toBe(2); + expect(stats.byType["commit"]).toBe(1); + expect(stats.byType["error"]).toBe(1); + expect(stats.byType["tool_call"]).toBe(1); + }); + + it("counts events by day", () => { + const stats = computeStats(events); + expect(stats.byDay["2026-03-01"]).toBe(3); + expect(stats.byDay["2026-03-02"]).toBe(2); + }); + + it("calculates average prompt length", () => { + const stats = computeStats(events); + expect(stats.promptCount).toBe(2); + // "hello world" = 11, "do something" = 12, avg = 11.5 → 12 rounded + expect(stats.avgPromptLen).toBe(12); + }); + + it("handles empty events", () => { + const stats = computeStats([]); + expect(stats.byType).toEqual({}); + expect(stats.byDay).toEqual({}); + expect(stats.avgPromptLen).toBe(0); + expect(stats.promptCount).toBe(0); + }); +}); + +describe("generateMarkdownReport", () => { + const events: TimelineEvent[] = [ + { type: "prompt", timestamp: "2026-03-01T10:00:00Z", content: "hello" }, + { type: "commit", timestamp: "2026-03-01T12:00:00Z", content: "initial commit", commit_hash: "abc1234def" }, + { type: "error", timestamp: "2026-03-02T09:00:00Z", content: "something broke" }, + { type: "tool_call", timestamp: "2026-03-02T10:00:00Z", tool_name: "read_file" }, + ]; + + it("includes title and date range", () => { + const report = generateMarkdownReport(events, { project: "/my/project" }); + expect(report).toContain("# Session Report: /my/project"); + expect(report).toContain("2026-03-01"); + expect(report).toContain("2026-03-02"); + }); + + it("uses custom title when provided", () => { + const report = generateMarkdownReport(events, { project: "/my/project", title: "Weekly Report" }); + expect(report).toContain("# Weekly Report"); + }); + + it("includes commits section with truncated hash", () => { + const report = generateMarkdownReport(events, { project: "/p" }); + expect(report).toContain("## Commits"); + expect(report).toContain("`abc1234`"); + expect(report).toContain("initial commit"); + }); + + it("includes issues & corrections section", () => { + const report = generateMarkdownReport(events, { project: "/p" }); + expect(report).toContain("## Issues & Corrections"); + expect(report).toContain("something broke"); + }); + + it("includes tool usage breakdown", () => { + const report = generateMarkdownReport(events, { project: "/p" }); + expect(report).toContain("## Tool Usage"); + expect(report).toContain("read_file"); + }); + + it("handles empty events gracefully in report sections", () => { + const report = generateMarkdownReport( + [{ type: "prompt", timestamp: "2026-03-01T10:00:00Z", content: "hi" }], + { project: "/p" } + ); + expect(report).not.toContain("## Commits"); + expect(report).not.toContain("## Issues & Corrections"); + expect(report).not.toContain("## Tool Usage"); + }); +});