diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index fa2488f..9e72b34 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -29,6 +29,7 @@ Phase 3 in progress. - Task 3.3 — ask_chatgpt prompt builder (`src/prompts/ask-chatgpt.js`, `test/prompts/ask-chatgpt.test.js`) ✅ - Task 3.4 — review_plan prompt builder (`src/prompts/review-plan.js`, `test/prompts/review-plan.test.js`) ✅ - Task 3.5 — review_code prompt builder (`src/prompts/review-code.js`, `test/prompts/review-code.test.js`) ✅ +- Task 3.6 — debug_issue prompt builder (`src/prompts/debug-issue.js`, `test/prompts/debug-issue.test.js`) ✅ ## Next Phase diff --git a/TASKS.md b/TASKS.md index 0a91379..01e57cf 100644 --- a/TASKS.md +++ b/TASKS.md @@ -248,6 +248,25 @@ Create `test/prompts/debug-issue.test.js`. Requirements: - Mirror structure of ask_chatgpt, review-plan, and review-code tests: base composition, debug_issue-specific instructions, optional fields, expectedOutput, input validation, tool guard, full integration. +Status: ✅ Complete + +### Task 3.7 - architecture_review prompt builder (NEXT) + +Create `src/prompts/architecture-review.js`. + +Requirements: +- Export a `buildArchitectureReviewPrompt(input)` function that returns a tool-specific prompt for the architecture_review MCP tool. +- Compose with `buildBasePrompt()` — call it internally and append the result with `---` separator. +- Instruct ChatGPT to review architecture decisions and trade-offs. +- Look for: local vs cloud trade-offs, simplicity, maintainability, operational risk, vendor lock-in, future extension paths. +- Support `relevantFiles` and `context` input fields. +- No other tool prompts yet. + +Create `test/prompts/architecture-review.test.js`. + +Requirements: +- Mirror structure of ask_chatgpt, review-plan, review-code, and debug-issue tests: base composition, architecture_review-specific instructions, optional fields, expectedOutput, input validation, tool guard, full integration. + Status: ⬜ Pending --- diff --git a/src/prompts/debug-issue.js b/src/prompts/debug-issue.js index 56fdf5f..2b7acce 100644 --- a/src/prompts/debug-issue.js +++ b/src/prompts/debug-issue.js @@ -1 +1,104 @@ -// debug_issue prompt template. +// Tool-specific prompt builder for debug_issue. + +import { buildBasePrompt } from "./base.js"; + +/** + * Build a tool-specific prompt for the debug_issue MCP tool. + * + * @param {{ question: string, context?: string, constraints?: string[], expectedOutput?: string, projectSummary?: string, taskSummary?: string, relevantFiles?: Array<{path: string, content: string, language?: string}>, logs?: string }} input + * @returns {string} A two-part prompt: base system message + debug_issue user message. + */ +export function buildDebugIssuePrompt(input) { + if (!input || typeof input.question !== "string" || input.question.length === 0) { + const err = new TypeError("debug_issue requires a non-empty 'question' field."); + err.kind = "ValidationError"; + throw err; + } + + const base = buildBasePrompt(); + + const lines = [ + "", + "---", + "", + "You are acting as a debugger/advisor for Claude Code. Claude Code will implement your suggestions — you do not implement anything yourself.", + "", + "Your task: diagnose the reported issue using only the supplied evidence and provide focused debugging guidance.", + "", + "Investigate systematically across these dimensions:", + "- Diagnose the reported issue using only the supplied evidence.", + "- Identify the most likely root causes with confidence levels (high / medium / low).", + "- Distinguish evidence from assumptions — call out what is inferred vs directly observed.", + "- Highlight missing information that would improve confidence.", + "- Suggest focused debugging steps ordered by speed and safety.", + "- Suggest possible fixes only when they are supported by the provided evidence.", + '- Avoid guessing — clearly state uncertainty when evidence is insufficient.', + '- Prefer the smallest explanation that fits all the evidence (Occam\'s razor).', + "", + "Important rules:", + '- Claude Code remains the implementation agent. You are acting only as a debugger/advisor.', + '- ChatGPT does not have repository-wide visibility — use only the code, logs, and context provided.', + "- If evidence is insufficient to reach confident conclusions, state so explicitly.", + "- Do not pretend to have executed commands or tests.", + "", + "Respond in this exact structure:", + '- Summary: Brief overall diagnosis of what likely went wrong.', + "- Likely Causes: Root causes grouped by confidence (High / Medium / Low).", + "- Fast Checks: Quick verifications that can confirm or rule out hypotheses.", + "- Minimal Safe Experiments: Small, low-risk changes to validate the diagnosis.", + '- What Not To Do Yet: Things that could make things worse or waste effort.', + "", + ]; + + lines.push(`Question: ${input.question}`); + + if (input.context) { + lines.push(""); + lines.push(`Context: ${input.context}`); + } + + if (input.constraints && input.constraints.length > 0) { + lines.push(""); + lines.push("Constraints:"); + for (const c of input.constraints) { + lines.push(`- ${c}`); + } + } + + if (input.expectedOutput) { + lines.push(""); + lines.push(`Expected output: ${input.expectedOutput}`); + } + + if (input.projectSummary) { + lines.push(""); + lines.push(`Project summary: ${input.projectSummary}`); + } + + if (input.taskSummary) { + lines.push(""); + lines.push(`Task summary: ${input.taskSummary}`); + } + + if (input.relevantFiles && input.relevantFiles.length > 0) { + lines.push(""); + lines.push("Relevant files:"); + + for (const file of input.relevantFiles) { + lines.push(""); + lines.push(`File: ${file.path}`); + if (file.language) { + lines.push(`Language: ${file.language}`); + } + lines.push(file.content); + } + } + + if (input.logs && input.logs.length > 0) { + lines.push(""); + lines.push("Logs:"); + lines.push(input.logs); + } + + return base + "\n" + lines.join("\n"); +} diff --git a/test/prompts/debug-issue.test.js b/test/prompts/debug-issue.test.js new file mode 100644 index 0000000..296d7b4 --- /dev/null +++ b/test/prompts/debug-issue.test.js @@ -0,0 +1,409 @@ +import { describe, it, expect } from "vitest"; +import { buildDebugIssuePrompt } from "../../src/prompts/debug-issue.js"; + +describe("buildDebugIssuePrompt", () => { + // --- Basic output contract --- + + it("returns a non-empty string given question-only input", () => { + const result = buildDebugIssuePrompt({ question: "Why is the API returning 500?" }); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + it("is deterministic — same input always produces the same output", () => { + const a = buildDebugIssuePrompt({ question: "Why is the API returning 500?" }); + const b = buildDebugIssuePrompt({ question: "Why is the API returning 500?" }); + expect(a).toBe(b); + }); + + // --- Composition with base prompt --- + + it("includes the base system prompt content", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("second-opinion assistant"); + expect(result).toContain("Claude Code"); + expect(result).toContain("not responsible for editing files"); + }); + + it("contains a '---' separator between base and tool-specific sections", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + const parts = result.split("\n---\n"); + expect(parts.length).toBe(2); + }); + + // --- debug_issue-specific role instructions --- + + it("contains debugger/advisor role instruction", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("debugger/advisor"); + }); + + it("instructs Claude Code as the implementation agent", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("Claude Code will implement your suggestions"); + }); + + it("instructs ChatGPT that you do not implement anything yourself", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("you do not implement anything yourself"); + }); + + // --- Claude Code / debugger guard rails --- + + it("states Claude Code remains the implementation agent", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("Claude Code remains the implementation agent"); + }); + + it("states ChatGPT is acting only as a debugger/advisor", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("acting only as a debugger/advisor"); + }); + + it("instructs to use only supplied code, logs, and context", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("use only the code, logs, and context provided"); + }); + + it("states ChatGPT does not have repository-wide visibility", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("does not have repository-wide visibility"); + }); + + // --- Investigation dimensions --- + + it("contains instruction to diagnose using only supplied evidence", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result.toLowerCase()).toContain("diagnose the reported issue using only"); + }); + + it("contains instruction to identify likely root causes with confidence levels", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("most likely root causes"); + expect(result.toLowerCase()).toContain("confidence levels"); + }); + + it("contains instruction to distinguish evidence from assumptions", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result.toLowerCase()).toContain("distinguish evidence from assumptions"); + }); + + it("contains instruction to highlight missing information", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toMatch(/highlight\s+missing\s+information/i); + }); + + it("contains instruction to suggest focused debugging steps", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result.toLowerCase()).toContain("suggest focused debugging steps"); + }); + + it("contains instruction to suggest fixes only when supported by evidence", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toMatch(/fixes?\s+only\s+when.*supported\s+by.*evidence/i); + }); + + // --- Response structure (TASKS.md §7) --- + + it("requests Summary section in response structure", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toMatch(/summary/i); + }); + + it("requests Likely Causes section with confidence levels in response structure", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("Likely Causes"); + expect(result.toLowerCase()).toContain("confidence"); + }); + + it("requests Fast Checks section in response structure", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("Fast Checks"); + }); + + it("requests Minimal Safe Experiments section in response structure", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("Minimal Safe Experiments"); + }); + + it("requests What Not To Do Yet section in response structure", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).toContain("What Not To Do Yet"); + }); + + // --- Question field --- + + it("appends the input question verbatim in the task section", () => { + const result = buildDebugIssuePrompt({ question: "Why does the API return 500?" }); + expect(result).toContain("Question: Why does the API return 500?"); + }); + + // --- Optional fields appended correctly (with question) --- + + it("appends Context when context is provided", () => { + const result = buildDebugIssuePrompt({ + question: "Why is the API returning 500?", + context: "The issue appeared after deploying v2.3.", + }); + expect(result).toContain("Context: The issue appeared after deploying v2.3."); + }); + + it("does NOT append Context section when context is empty", () => { + const result = buildDebugIssuePrompt({ question: "Test", context: "" }); + expect(result).not.toContain("Context:"); + }); + + it("appends Constraints list when constraints are provided", () => { + const result = buildDebugIssuePrompt({ + question: "Why is the API returning 500?", + constraints: ["Do not change the database schema", "Must be safe to deploy"], + }); + expect(result).toContain("Constraints:"); + expect(result).toContain("- Do not change the database schema"); + expect(result).toContain("- Must be safe to deploy"); + }); + + it("does NOT append Constraints section when constraints array is empty", () => { + const result = buildDebugIssuePrompt({ question: "Test", constraints: [] }); + expect(result).not.toContain("Constraints:"); + }); + + it("appends Expected output when expectedOutput is provided", () => { + const result = buildDebugIssuePrompt({ + question: "Why is the API returning 500?", + expectedOutput: "A root-cause analysis with confidence levels.", + }); + expect(result).toContain( + "Expected output: A root-cause analysis with confidence levels." + ); + }); + + it("does NOT append Expected output section when expectedOutput is omitted", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).not.toContain("Expected output:"); + }); + + it("does NOT append Expected output section when expectedOutput is empty", () => { + const result = buildDebugIssuePrompt({ question: "Test", expectedOutput: "" }); + expect(result).not.toContain("Expected output:"); + }); + + it("appends all optional fields alongside each other", () => { + const result = buildDebugIssuePrompt({ + question: "Why is the API returning 500?", + context: "The issue appeared after deploying v2.3.", + constraints: ["Do not change the database schema", "Must be safe to deploy"], + expectedOutput: "A root-cause analysis with confidence levels.", + projectSummary: "E-commerce platform using Express + PostgreSQL.", + taskSummary: "Stripe webhook handler processes recurring payment events.", + }); + expect(result).toContain("Question: Why is the API returning 500?"); + expect(result).toContain("Context: The issue appeared after deploying v2.3."); + expect(result).toContain("Constraints:"); + expect(result).toContain("- Do not change the database schema"); + expect(result).toContain("- Must be safe to deploy"); + expect(result).toContain("Expected output: A root-cause analysis with confidence levels."); + expect(result).toContain("Project summary: E-commerce platform using Express + PostgreSQL."); + expect(result).toContain( + "Task summary: Stripe webhook handler processes recurring payment events." + ); + }); + + it("does NOT append Project summary when projectSummary is omitted", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).not.toContain("Project summary:"); + }); + + it("does NOT append Task summary when taskSummary is omitted", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).not.toContain("Task summary:"); + }); + + // --- relevantFiles (present) --- + + it("includes file section header when relevantFiles is provided", () => { + const result = buildDebugIssuePrompt({ + question: "Why does the API return 500?", + relevantFiles: [{ path: "src/webhooks/stripe.js", content: "app.post('/webhook', handler);" }], + }); + expect(result).toContain("Relevant files:"); + }); + + it("includes file path, language, and content when relevantFiles has language", () => { + const result = buildDebugIssuePrompt({ + question: "Why does the API return 500?", + relevantFiles: [ + { path: "src/webhooks/stripe.js", language: "javascript", content: "app.post('/webhook', handler);" }, + ], + }); + expect(result).toContain("File: src/webhooks/stripe.js"); + expect(result).toContain("Language: javascript"); + expect(result).toContain("app.post('/webhook', handler);"); + }); + + it("includes file content without language header when language is omitted", () => { + const result = buildDebugIssuePrompt({ + question: "Why does the API return 500?", + relevantFiles: [{ path: "src/webhooks/stripe.js", content: "app.post('/webhook', handler);" }], + }); + expect(result).toContain("File: src/webhooks/stripe.js"); + expect(result).not.toContain("Language:"); + expect(result).toContain("app.post('/webhook', handler);"); + }); + + it("includes multiple files when relevantFiles has multiple entries", () => { + const result = buildDebugIssuePrompt({ + question: "Why does the API return 500?", + relevantFiles: [ + { path: "src/webhooks/stripe.js", language: "javascript", content: "app.post('/webhook', handler);" }, + { path: "src/utils/notifications.js", language: "javascript", content: "// no tests" }, + ], + }); + expect(result).toContain("File: src/webhooks/stripe.js"); + expect(result).toContain("File: src/utils/notifications.js"); + expect(result).toContain("app.post('/webhook', handler);"); + expect(result).toContain("// no tests"); + }); + + // --- relevantFiles (absent/empty) --- + + it("does NOT include Relevant files section when relevantFiles is omitted", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).not.toContain("Relevant files:"); + }); + + it("does NOT include Relevant files section when relevantFiles is empty array", () => { + const result = buildDebugIssuePrompt({ question: "Test", relevantFiles: [] }); + expect(result).not.toContain("Relevant files:"); + }); + + // --- logs (present) --- + + it("includes Logs section with content when logs is provided", () => { + const result = buildDebugIssuePrompt({ + question: "Why does the API return 500?", + logs: "ERROR TypeError: Cannot read properties of undefined", + }); + expect(result).toContain("Logs:"); + expect(result).toContain("ERROR TypeError: Cannot read properties of undefined"); + }); + + it("does NOT include Logs section when logs is absent", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).not.toContain("Logs:"); + }); + + it("does NOT include Logs section when logs is empty string", () => { + const result = buildDebugIssuePrompt({ question: "Test", logs: "" }); + expect(result).not.toContain("Logs:"); + }); + + // --- Tool-specific guard --- + + it("does NOT contain review_code specific content", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result.toLowerCase()).not.toContain("issues by severity"); + expect(result.toLowerCase()).not.toContain("suggested fixes"); + }); + + it("does NOT contain review_plan specific content", () => { + const result = buildDebugIssuePrompt({ question: "Test" }); + expect(result).not.toContain("missing steps"); + expect(result).not.toContain("scope creep"); + }); + + // --- Input validation --- + + it("throws TypeError when input is missing, null, or undefined", () => { + expect(() => buildDebugIssuePrompt()).toThrow(TypeError); + expect(() => buildDebugIssuePrompt(null)).toThrow(TypeError); + expect(() => buildDebugIssuePrompt(undefined)).toThrow(TypeError); + }); + + it("throws TypeError when question is missing", () => { + expect(() => buildDebugIssuePrompt({ context: "something" })).toThrow(TypeError); + }); + + it("throws TypeError when question is empty", () => { + expect(() => buildDebugIssuePrompt({ question: "" })).toThrow(TypeError); + }); + + // --- Full integration test --- + + it("returns a well-formed two-part prompt with all fields, relevantFiles, and logs", () => { + const result = buildDebugIssuePrompt({ + question: "Why does the payment endpoint return 500 for recurring invoices?", + context: "The issue appeared after deploying v2.3. Only affects Stripe webhooks with retry logic.", + constraints: ["Do not change the database schema", "Must be safe to deploy during business hours"], + expectedOutput: "A root-cause analysis with confidence levels and minimal debugging steps.", + projectSummary: "E-commerce platform using Express + PostgreSQL, deployed on EC2.", + taskSummary: "Stripe webhook handler processes recurring payment events.", + relevantFiles: [ + { + path: "src/webhooks/stripe.js", + language: "javascript", + content: `app.post('/webhook/stripe', async (req, res) => {\n const sig = req.headers['stripe-signature'];\n const event = stripe.webhooks.constructEvent(req.body, sig);\n if (event.type === 'invoice.payment_failed') {\n await handlePaymentFailed(event.data.object);\n }\n res.sendStatus(200);\n});`, + }, + ], + logs: `2024-01-15T10:23:01Z ERROR StripeWebhookHandler: Failed to process event inv_1abc + at handlePaymentFailed (src/webhooks/stripe.js:14:9) + at IncomingMessage. (src/server.js:87:5) +TypeError: Cannot read properties of undefined (reading 'customer_email') + at formatRetryNotification (src/utils/notifications.js:23:35)`, + }); + + // Base section present + expect(result).toContain("second-opinion assistant"); + expect(result).toContain("Rules:"); + + // Separator + expect(result).toMatch(/\n---\n/); + + // Tool-specific section present + expect(result).toContain("debugger/advisor"); + expect(result).toContain("Claude Code remains the implementation agent"); + expect(result).toContain("repository-wide visibility"); + + // Investigation dimensions present + expect(result.toLowerCase()).toContain("diagnose the reported issue using only"); + expect(result).toContain("most likely root causes"); + expect(result.toLowerCase()).toContain("distinguish evidence from assumptions"); + expect(result).toMatch(/highlight\s+missing\s+information/i); + expect(result.toLowerCase()).toContain("suggest focused debugging steps"); + expect(result).toMatch(/fixes?\s+only\s+when.*supported\s+by.*evidence/i); + + // Response structure present (TASKS.md §7) + expect(result).toMatch(/summary/i); + expect(result).toContain("Likely Causes"); + expect(result.toLowerCase()).toContain("confidence"); + expect(result).toContain("Fast Checks"); + expect(result).toContain("Minimal Safe Experiments"); + expect(result).toContain("What Not To Do Yet"); + + // All fields present verbatim + expect(result).toContain("Question: Why does the payment endpoint return 500 for recurring invoices?"); + expect(result).toContain("Context: The issue appeared after deploying v2.3."); + expect(result).toContain("Constraints:"); + expect(result).toContain("- Do not change the database schema"); + expect(result).toContain("- Must be safe to deploy during business hours"); + expect(result).toContain("Expected output: A root-cause analysis with confidence levels and minimal debugging steps."); + expect(result).toContain("Project summary: E-commerce platform using Express + PostgreSQL, deployed on EC2."); + expect(result).toContain("Task summary: Stripe webhook handler processes recurring payment events."); + + // Relevant files present + expect(result).toContain("Relevant files:"); + expect(result).toContain("File: src/webhooks/stripe.js"); + expect(result).toContain("Language: javascript"); + expect(result).toContain("app.post('/webhook/stripe', async (req, res) => {"); + + // Logs present + expect(result).toContain("Logs:"); + expect(result).toContain("ERROR StripeWebhookHandler: Failed to process event inv_1abc"); + expect(result).toContain("TypeError: Cannot read properties of undefined"); + + // Does not contain other tool content + expect(result.toLowerCase()).not.toContain("issues by severity"); + expect(result).not.toContain("missing steps"); + expect(result).not.toContain("scope creep"); + }); +});