Recurring pitfalls log
pitfalls_logTrack recurring issues and their resolutions. Store a pitfall with title and description, list open or resolved entries, or mark a pitfall as resolved. Builds a searchable history of project-specific problems and workarounds.
Instructions
Multi-action log of recurring problems and their resolutions. Pick one via action: • store — record a new pitfall (title + body required). Writes a row. • list — list open pitfalls (set include_resolved=true for all). Read-only. • resolve — mark a pitfall resolved (pitfall_id required). Use when something keeps biting and you want a queryable problem→resolution log. For one-off design choices, use decisions_log.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | Which sub-operation to perform: `store`, `list`, or `resolve`. | |
| project_path | Yes | Absolute project path the pitfall belongs to (or to scope `list`). Required. | |
| title | No | Short symptom title (required for `store`). E.g. `"esbuild fails on M1 when using esm + node-gyp"`. | |
| body | No | Markdown body (required for `store`). Should describe the problem and the resolution / workaround. | |
| importance | No | Importance score in [0, 1] for `store`. Default 0.6. | |
| limit | No | Maximum pitfalls to return for `list` (1-50). Default 10. | |
| include_resolved | No | For `list`: if true, also include resolved pitfalls. Default false (open only). | |
| pitfall_id | No | Required for `action="resolve"` — the id of the pitfall to mark resolved. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| message | Yes | For `store`: `Pitfall logged/updated with ID: <id>`. For `list`: markdown list with `[RESOLVED]` or `[xN]` (occurrence count) prefixes, or `No pitfalls found.` For `resolve`: `Pitfall <id> marked as resolved.` or `Pitfall <id> not found.` |
Implementation Reference
- src/tools/pitfalls-log.ts:4-28 (handler)The `handlePitfallsLog` function implements the tool logic for `pitfalls_log`. It dispatches on `action` ('store', 'list', 'resolve') and calls the corresponding `PitfallsRepo` methods.
export async function handlePitfallsLog(repo: PitfallsRepo, params: { action: string; project_path: string; title?: string; body?: string; importance?: number; limit?: number; include_resolved?: boolean; pitfall_id?: string; }): Promise<string> { if (params.action === "store") { if (!params.title || !params.body) return "title and body are required for action='store'."; const id = repo.store(params.project_path, params.title, params.body, params.importance); return `Pitfall logged/updated with ID: ${id}`; } if (params.action === "list") { const pitfalls = repo.list(params.project_path, params.limit, params.include_resolved); if (!pitfalls.length) return "No pitfalls found."; return pitfalls.map(p => { const status = p.resolved ? "RESOLVED" : `x${p.occurrence_count}`; return `[${status}] ${p.title}\n ID: ${p.id}\n ${(p.body as string).slice(0, 300)}\n Last seen: ${p.last_seen_at}`; }).join("\n\n"); } if (params.action === "resolve") { if (!params.pitfall_id) return "pitfall_id is required for action='resolve'."; return repo.resolve(params.pitfall_id) ? `Pitfall ${params.pitfall_id} marked as resolved.` : `Pitfall ${params.pitfall_id} not found.`; } return `Invalid action: ${params.action}. Use 'store', 'list', or 'resolve'.`; } - src/tools/pitfalls-log.ts:4-7 (schema)Type-level schema for the params object: action, project_path, title?, body?, importance?, limit?, include_resolved?, pitfall_id?
export async function handlePitfallsLog(repo: PitfallsRepo, params: { action: string; project_path: string; title?: string; body?: string; importance?: number; limit?: number; include_resolved?: boolean; pitfall_id?: string; }): Promise<string> { - src/index.ts:475-508 (registration)The `pitfalls_log` tool is registered on the MCP server via `server.registerTool('pitfalls_log', ...)` with inputSchema using Zod validation and an async handler calling `handlePitfallsLog`.
server.registerTool( "pitfalls_log", { title: "Recurring pitfalls log", description: [ "Multi-action log of recurring problems and their resolutions. Pick one via `action`:", " • `store` — record a new pitfall (`title` + `body` required). Writes a row.", " • `list` — list open pitfalls (set `include_resolved=true` for all). Read-only.", " • `resolve` — mark a pitfall resolved (`pitfall_id` required).", "Use when something keeps biting and you want a queryable problem→resolution log. For one-off design choices, use `decisions_log`.", ].join(" "), inputSchema: { action: z.enum(["store", "list", "resolve"]).describe("Which sub-operation to perform: `store`, `list`, or `resolve`."), project_path: z.string().describe("Absolute project path the pitfall belongs to (or to scope `list`). Required."), title: z.string().default("").describe("Short symptom title (required for `store`). E.g. `\"esbuild fails on M1 when using esm + node-gyp\"`."), body: z.string().default("").describe("Markdown body (required for `store`). Should describe the problem and the resolution / workaround."), importance: z.number().min(0).max(1).default(0.6).describe("Importance score in [0, 1] for `store`. Default 0.6."), limit: z.number().int().min(1).max(50).default(10).describe("Maximum pitfalls to return for `list` (1-50). Default 10."), include_resolved: z.boolean().default(false).describe("For `list`: if true, also include resolved pitfalls. Default false (open only)."), pitfall_id: z.string().default("").describe("Required for `action=\"resolve\"` — the id of the pitfall to mark resolved."), }, annotations: { title: "Recurring pitfalls log", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, }, outputSchema: { message: z.string().describe("For `store`: `Pitfall logged/updated with ID: <id>`. For `list`: markdown list with `[RESOLVED]` or `[xN]` (occurrence count) prefixes, or `No pitfalls found.` For `resolve`: `Pitfall <id> marked as resolved.` or `Pitfall <id> not found.`"), }, }, async (params) => textResult(await handlePitfallsLog(pitRepo, params)) ); - src/index.ts:486-495 (schema)Input schema for `pitfalls_log` using Zod: action (enum), project_path (string), title, body, importance, limit, include_resolved, pitfall_id — with descriptions and defaults.
inputSchema: { action: z.enum(["store", "list", "resolve"]).describe("Which sub-operation to perform: `store`, `list`, or `resolve`."), project_path: z.string().describe("Absolute project path the pitfall belongs to (or to scope `list`). Required."), title: z.string().default("").describe("Short symptom title (required for `store`). E.g. `\"esbuild fails on M1 when using esm + node-gyp\"`."), body: z.string().default("").describe("Markdown body (required for `store`). Should describe the problem and the resolution / workaround."), importance: z.number().min(0).max(1).default(0.6).describe("Importance score in [0, 1] for `store`. Default 0.6."), limit: z.number().int().min(1).max(50).default(10).describe("Maximum pitfalls to return for `list` (1-50). Default 10."), include_resolved: z.boolean().default(false).describe("For `list`: if true, also include resolved pitfalls. Default false (open only)."), pitfall_id: z.string().default("").describe("Required for `action=\"resolve\"` — the id of the pitfall to mark resolved."), }, - src/db/pitfalls.ts:10-75 (helper)The `PitfallsRepo` class (store, list, listAll, resolve) provides the database operations backing the `pitfalls_log` tool — including secret scrubbing, dedup logic, and occurrence counting.
export class PitfallsRepo { constructor(private db: Database.Database) {} private getOrCreateProject(rootPath: string): string { const row = this.db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath) as any; if (row) return row.id; const id = randomUUID(); this.db.prepare("INSERT INTO projects (id, name, root_path) VALUES (?, ?, ?)").run(id, rootPath.split("/").pop() ?? rootPath, rootPath); return id; } store(projectPath: string, title: string, body: string, importance = 0.6): string { const projectId = this.getOrCreateProject(projectPath); const now = nowIso(); // Issue #12: scrub secrets from title and body at write time. const cleanTitle = scrubSecrets(title); const cleanBody = scrubSecrets(body); if (cleanTitle !== title) { logger.warn(`Secret pattern detected and scrubbed in pitfall title`); } if (hasPrivate(title)) { logger.warn(`Warning: <private> tags detected in pitfall title — tags do not redact in titles. Move sensitive content to body.`); } const existing = this.db.prepare( "SELECT id, occurrence_count FROM pitfalls WHERE project_id = ? AND title = ? AND deleted_at IS NULL AND resolved = 0" ).get(projectId, cleanTitle) as any; // Issue #4: set has_private flag on store/update. const hasPrivateFlag = hasPrivate(cleanBody) ? 1 : 0; if (existing) { this.db.prepare("UPDATE pitfalls SET occurrence_count = occurrence_count + 1, last_seen_at = ?, body = ?, has_private = ? WHERE id = ?") .run(now, cleanBody, hasPrivateFlag, existing.id); return existing.id; } const id = randomUUID(); this.db.prepare(` INSERT INTO pitfalls (id, project_id, title, body, importance_score, last_seen_at, has_private, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `).run(id, projectId, cleanTitle, cleanBody, importance, now, hasPrivateFlag, now); return id; } list(projectPath: string, limit = 10, includeResolved = false): any[] { const projectId = this.getOrCreateProject(projectPath); const resolvedClause = includeResolved ? "" : "AND resolved = 0"; return this.db.prepare(` SELECT * FROM pitfalls WHERE project_id = ? AND deleted_at IS NULL ${resolvedClause} ORDER BY occurrence_count DESC, importance_score DESC LIMIT ? `).all(projectId, limit) as any[]; } listAll(limit = 10, includeResolved = false): any[] { const resolvedClause = includeResolved ? "" : "AND resolved = 0"; return this.db.prepare(` SELECT * FROM pitfalls WHERE deleted_at IS NULL ${resolvedClause} ORDER BY occurrence_count DESC, importance_score DESC LIMIT ? `).all(limit) as any[]; } resolve(pitfallId: string): boolean { return this.db.prepare("UPDATE pitfalls SET resolved = 1 WHERE id = ? AND deleted_at IS NULL").run(pitfallId).changes > 0; } }