From 008f38e630f287d1816186bd69398e46cf5c37bf Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 11 Jun 2026 15:37:42 +0100 Subject: [PATCH] feat: add ask_chatgpt prompt builder --- PROJECT_STATE.md | 4 +- TASKS.md | 39 ++++++ src/prompts/ask-chatgpt.js | 71 +++++++++- test/prompts/ask-chatgpt.test.js | 216 +++++++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 test/prompts/ask-chatgpt.test.js diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 5ba71ad..5080659 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -25,7 +25,9 @@ Phase 3 in progress. - Task 2.2 — Response builder (`src/openai/responses.js`) ✅ - Task 2.3 — Error handling and edge cases for OpenAI integration (tests) ✅ - Task 3.1 — Zod input validation schemas (`src/tools/schemas.js`, `test/tools/schemas.test.js`) ✅ +- Task 3.2 — Base prompt template (`src/prompts/base.js`, `test/prompts/base.test.js`) ✅ +- Task 3.3 — ask_chatgpt prompt builder (`src/prompts/ask-chatgpt.js`, `test/prompts/ask-chatgpt.test.js`) ✅ ## Next Phase -Phase 3 — in progress; next is Task 3.2 (Base prompt template). +Phase 3 — in progress; next is Task 3.4 (review_plan prompt builder). diff --git a/TASKS.md b/TASKS.md index a776d08..5ce26e7 100644 --- a/TASKS.md +++ b/TASKS.md @@ -172,6 +172,45 @@ Requirements: - Test that the prompt is a non-empty string. - Test that the prompt does not include tool-specific content (review/code/debug/architecture). +### Task 3.3 - ask_chatgpt prompt builder + +Create `src/prompts/ask-chatgpt.js`. + +Requirements: +- Export a `buildAskChatGptPrompt(input)` function that returns a tool-specific prompt for the ask_chatgpt MCP tool. +- Compose with `buildBasePrompt()` — call it internally and append the result with `---` separator. +- The final prompt has two parts: [base system prompt] + [ask_chatgpt user instructions]. +- Instruct ChatGPT to answer the question directly, act as a second-opinion advisor, identify assumptions, identify risks, suggest safer approaches, state uncertainty clearly. +- Support `expectedOutput` — if provided, append "Expected output: " to the task section. +- Question is required; throw TypeError when missing or empty. +- No review_plan, review_code, debug_issue, or architecture_review content yet. + +Create `test/prompts/ask-chatgpt.test.js`. + +Requirements: +- ~27 tests covering base composition, ask_chatgpt instructions, optional field append/omit, expectedOutput present/absent, tool-specific guard, input validation, and full integration. +- No MCP code. No OpenAI calls. Pure prompt string assertions only. + +Status: ✅ Complete + +### Task 3.4 - review_plan prompt builder (NEXT) + +Create `src/prompts/review-plan.js`. + +Requirements: +- Export a `buildReviewPlanPrompt(input)` function that returns a tool-specific prompt for the review_plan MCP tool. +- Compose with `buildBasePrompt()` — call it internally and append the result with `---` separator. +- Instruct ChatGPT to review a proposed implementation plan before Claude Code acts. +- Look for: missing steps, unsafe assumptions, scope creep, better sequencing, test gaps. +- No other tool prompts yet (debug_issue, architecture_review, etc.). + +Create `test/prompts/review-plan.test.js`. + +Requirements: +- Mirror structure of ask-chatgpt tests: base composition, review_plan-specific instructions, optional fields, expectedOutput, input validation, tool guard, full integration. + +Status: ⬜ Pending + --- Phase 2 complete. Phase 3 in progress. diff --git a/src/prompts/ask-chatgpt.js b/src/prompts/ask-chatgpt.js index 9841c4d..d07db46 100644 --- a/src/prompts/ask-chatgpt.js +++ b/src/prompts/ask-chatgpt.js @@ -1 +1,70 @@ -// ask_chatgpt prompt template. +// Tool-specific prompt builder for ask_chatgpt. + +import { buildBasePrompt } from "./base.js"; + +/** + * Build a tool-specific prompt for the ask_chatgpt MCP tool. + * + * @param {{ question: string, context?: string, constraints?: string[], expectedOutput?: string, projectSummary?: string, taskSummary?: string }} input + * @returns {string} A two-part prompt: base system message + ask-chatgpt user message. + */ +export function buildAskChatGptPrompt(input) { + if (!input || typeof input.question !== "string" || input.question.length === 0) { + const err = new TypeError("ask_chatgpt requires a non-empty 'question' field."); + err.kind = "ValidationError"; + throw err; + } + + const base = buildBasePrompt(); + + const lines = [ + "", + "---", + "", + "You are acting as a general second-opinion advisor for Claude Code. Claude Code will implement your suggestions — you do not implement anything yourself.", + "", + "Your task: answer the question directly, then provide brief reasoning.", + "", + "When responding:", + "- Answer the question directly first.", + "- Act as a second-opinion advisor only.", + "- Identify key assumptions in the question or context.", + "- Identify risks or edge cases others might miss.", + "- Suggest safer approaches when appropriate.", + "- State uncertainty clearly — do not guess.", + '- Prefer small, reviewable changes over large rewrites.', + "", + ]; + + 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}`); + } + + return base + "\n" + lines.join("\n"); +} diff --git a/test/prompts/ask-chatgpt.test.js b/test/prompts/ask-chatgpt.test.js new file mode 100644 index 0000000..80968ec --- /dev/null +++ b/test/prompts/ask-chatgpt.test.js @@ -0,0 +1,216 @@ +import { describe, it, expect } from "vitest"; +import { buildAskChatGptPrompt } from "../../src/prompts/ask-chatgpt.js"; + +describe("buildAskChatGptPrompt", () => { + // --- Basic output contract --- + + it("returns a non-empty string given question-only input", () => { + const result = buildAskChatGptPrompt({ question: "Is this OK?" }); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + it("is deterministic — same input always produces the same output", () => { + const a = buildAskChatGptPrompt({ question: "Is this OK?" }); + const b = buildAskChatGptPrompt({ question: "Is this OK?" }); + expect(a).toBe(b); + }); + + // --- Composition with base prompt --- + + it("includes the base system prompt content", () => { + const result = buildAskChatGptPrompt({ 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 = buildAskChatGptPrompt({ question: "Test" }); + const parts = result.split("\n---\n"); + expect(parts.length).toBe(2); + }); + + // --- ask_chatgpt-specific role instructions --- + + it("contains ask_chatgpt-specific role instructions (second-opinion advisor)", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result.toLowerCase()).toContain("second-opinion advisor"); + }); + + it("instructs ChatGPT to answer the question directly", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result).toMatch(/answer\s+the\s+question\s+direct/i); + }); + + it("includes section about identifying assumptions", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result.toLowerCase()).toContain("assumptions"); + }); + + it("includes section about identifying risks", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result.toLowerCase()).toContain("risks"); + }); + + it("includes section about suggesting safer approaches", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result).toMatch(/safer\s+approaches?/i); + }); + + it("includes section about stating uncertainty clearly", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result.toLowerCase()).toContain("uncertainty"); + }); + + // --- Question field --- + + it("appends the input question verbatim in the task section", () => { + const result = buildAskChatGptPrompt({ question: "What auth method should I use?" }); + expect(result).toContain("Question: What auth method should I use?"); + }); + + // --- Optional fields appended correctly (with question) --- + + it("appends Context when context is provided", () => { + const result = buildAskChatGptPrompt({ + question: "Review this", + context: "The code catches all errors.", + }); + expect(result).toContain("Context: The code catches all errors."); + }); + + it("does NOT append Context section when context is empty", () => { + const result = buildAskChatGptPrompt({ question: "Review this", context: "" }); + expect(result).not.toContain("Context:"); + }); + + it("appends Constraints list when constraints are provided", () => { + const result = buildAskChatGptPrompt({ + question: "Review this", + constraints: ["must be safe", "must be fast"], + }); + expect(result).toContain("Constraints:"); + expect(result).toContain("- must be safe"); + expect(result).toContain("- must be fast"); + }); + + it("does NOT append Constraints section when constraints array is empty", () => { + const result = buildAskChatGptPrompt({ question: "Review this", constraints: [] }); + expect(result).not.toContain("Constraints:"); + }); + + // --- expectedOutput (approved test additions) --- + + it("appends Expected output when expectedOutput is provided", () => { + const result = buildAskChatGptPrompt({ + question: "Polling vs webhooks?", + expectedOutput: "A comparison table with pros/cons and a recommendation.", + }); + expect(result).toContain("Expected output: A comparison table with pros/cons and a recommendation."); + }); + + it("does NOT append Expected output section when expectedOutput is omitted", () => { + const result = buildAskChatGptPrompt({ question: "Polling vs webhooks?" }); + expect(result).not.toContain("Expected output:"); + }); + + it("does NOT append Expected output section when expectedOutput is empty", () => { + const result = buildAskChatGptPrompt({ question: "Polling vs webhooks?", expectedOutput: "" }); + expect(result).not.toContain("Expected output:"); + }); + + it("appends all optional fields alongside expectedOutput", () => { + const result = buildAskChatGptPrompt({ + question: "Should I use JWT or sessions?", + context: "Current API uses token auth.", + constraints: ["no external deps"], + expectedOutput: "A one-paragraph recommendation.", + projectSummary: "Internal inventory tool.", + taskSummary: "Migrating authentication.", + }); + expect(result).toContain("Question: Should I use JWT or sessions?"); + expect(result).toContain("Context: Current API uses token auth."); + expect(result).toContain("Constraints:"); + expect(result).toContain("- no external deps"); + expect(result).toContain("Expected output: A one-paragraph recommendation."); + expect(result).toContain("Project summary: Internal inventory tool."); + expect(result).toContain("Task summary: Migrating authentication."); + }); + + it("does NOT append Project summary when projectSummary is omitted", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result).not.toContain("Project summary:"); + }); + + it("does NOT append Task summary when taskSummary is omitted", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result).not.toContain("Task summary:"); + }); + + // --- Tool-specific guard --- + + it("does NOT contain review_plan specific content", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result).not.toContain("review-plan"); + expect(result).not.toContain("missing steps"); + expect(result).not.toContain("scope creep"); + }); + + it("does NOT contain review_code specific content", () => { + const result = buildAskChatGptPrompt({ question: "Test" }); + expect(result).not.toContain("review_code"); + expect(result).not.toContain("patch"); + expect(result).not.toContain("diff"); + }); + + // --- Input validation --- + + it("throws TypeError when input is missing", () => { + expect(() => buildAskChatGptPrompt()).toThrow(TypeError); + expect(() => buildAskChatGptPrompt(null)).toThrow(TypeError); + }); + + it("throws TypeError when question is missing", () => { + expect(() => buildAskChatGptPrompt({ context: "something" })).toThrow(TypeError); + }); + + it("throws TypeError when question is empty", () => { + expect(() => buildAskChatGptPrompt({ question: "" })).toThrow(TypeError); + }); + + // --- Full integration test --- + + it("returns a well-formed two-part prompt with all fields", () => { + const result = buildAskChatGptPrompt({ + question: "Is this approach safe?", + context: "We log errors to console.error.", + constraints: ["no stack traces", "no raw data"], + expectedOutput: "A concise risk assessment.", + projectSummary: "Private VPC REST API.", + taskSummary: "Hardening error logging.", + }); + + // 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).toMatch(/answer\s+the\s+question\s+direct/i); + expect(result).toContain("assumptions"); + expect(result).toContain("risks"); + expect(result).toContain("safer approaches"); + expect(result).toContain("uncertainty"); + + // All fields present verbatim + expect(result).toContain("Question: Is this approach safe?"); + expect(result).toContain("Context: We log errors to console.error."); + expect(result).toContain("- no stack traces"); + expect(result).toContain("Expected output: A concise risk assessment."); + expect(result).toContain("Project summary: Private VPC REST API."); + expect(result).toContain("Task summary: Hardening error logging."); + }); +});