mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat: enhance app specification structure and XML conversion
- Introduced a TypeScript interface for structured specification output to standardize project details. - Added a JSON schema for reliable parsing of structured output. - Implemented XML conversion for structured specifications, ensuring comprehensive project representation. - Updated spec generation options to include output format configuration. - Enhanced prompt instructions for generating specifications to improve clarity and completeness.
This commit is contained in:
@@ -4,6 +4,235 @@
|
||||
* This format must be included in all prompts that generate, modify, or regenerate
|
||||
* app specifications to ensure consistency across the application.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TypeScript interface for structured spec output
|
||||
*/
|
||||
export interface SpecOutput {
|
||||
project_name: string;
|
||||
overview: string;
|
||||
technology_stack: string[];
|
||||
core_capabilities: string[];
|
||||
implemented_features: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
file_locations?: string[];
|
||||
}>;
|
||||
additional_requirements?: string[];
|
||||
development_guidelines?: string[];
|
||||
implementation_roadmap?: Array<{
|
||||
phase: string;
|
||||
status: "completed" | "in_progress" | "pending";
|
||||
description: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Schema for structured spec output
|
||||
* Used with Claude's structured output feature for reliable parsing
|
||||
*/
|
||||
export const specOutputSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
project_name: {
|
||||
type: "string",
|
||||
description: "The name of the project",
|
||||
},
|
||||
overview: {
|
||||
type: "string",
|
||||
description:
|
||||
"A comprehensive description of what the project does, its purpose, and key goals",
|
||||
},
|
||||
technology_stack: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description:
|
||||
"List of all technologies, frameworks, libraries, and tools used",
|
||||
},
|
||||
core_capabilities: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "List of main features and capabilities the project provides",
|
||||
},
|
||||
implemented_features: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Name of the implemented feature",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
description: "Description of what the feature does",
|
||||
},
|
||||
file_locations: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "File paths where this feature is implemented",
|
||||
},
|
||||
},
|
||||
required: ["name", "description"],
|
||||
},
|
||||
description: "Features that have been implemented based on code analysis",
|
||||
},
|
||||
additional_requirements: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Any additional requirements or constraints",
|
||||
},
|
||||
development_guidelines: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Development standards and practices",
|
||||
},
|
||||
implementation_roadmap: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
phase: {
|
||||
type: "string",
|
||||
description: "Name of the implementation phase",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
enum: ["completed", "in_progress", "pending"],
|
||||
description: "Current status of this phase",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
description: "Description of what this phase involves",
|
||||
},
|
||||
},
|
||||
required: ["phase", "status", "description"],
|
||||
},
|
||||
description: "Phases or roadmap items for implementation",
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"project_name",
|
||||
"overview",
|
||||
"technology_stack",
|
||||
"core_capabilities",
|
||||
"implemented_features",
|
||||
],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape special XML characters
|
||||
*/
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert structured spec output to XML format
|
||||
*/
|
||||
export function specToXml(spec: SpecOutput): string {
|
||||
const indent = " ";
|
||||
|
||||
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project_specification>
|
||||
${indent}<project_name>${escapeXml(spec.project_name)}</project_name>
|
||||
|
||||
${indent}<overview>
|
||||
${indent}${indent}${escapeXml(spec.overview)}
|
||||
${indent}</overview>
|
||||
|
||||
${indent}<technology_stack>
|
||||
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join("\n")}
|
||||
${indent}</technology_stack>
|
||||
|
||||
${indent}<core_capabilities>
|
||||
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join("\n")}
|
||||
${indent}</core_capabilities>
|
||||
|
||||
${indent}<implemented_features>
|
||||
${spec.implemented_features
|
||||
.map(
|
||||
(f) => `${indent}${indent}<feature>
|
||||
${indent}${indent}${indent}<name>${escapeXml(f.name)}</name>
|
||||
${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${
|
||||
f.file_locations && f.file_locations.length > 0
|
||||
? `\n${indent}${indent}${indent}<file_locations>
|
||||
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join("\n")}
|
||||
${indent}${indent}${indent}</file_locations>`
|
||||
: ""
|
||||
}
|
||||
${indent}${indent}</feature>`
|
||||
)
|
||||
.join("\n")}
|
||||
${indent}</implemented_features>`;
|
||||
|
||||
// Optional sections
|
||||
if (spec.additional_requirements && spec.additional_requirements.length > 0) {
|
||||
xml += `
|
||||
|
||||
${indent}<additional_requirements>
|
||||
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join("\n")}
|
||||
${indent}</additional_requirements>`;
|
||||
}
|
||||
|
||||
if (spec.development_guidelines && spec.development_guidelines.length > 0) {
|
||||
xml += `
|
||||
|
||||
${indent}<development_guidelines>
|
||||
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join("\n")}
|
||||
${indent}</development_guidelines>`;
|
||||
}
|
||||
|
||||
if (spec.implementation_roadmap && spec.implementation_roadmap.length > 0) {
|
||||
xml += `
|
||||
|
||||
${indent}<implementation_roadmap>
|
||||
${spec.implementation_roadmap
|
||||
.map(
|
||||
(r) => `${indent}${indent}<phase>
|
||||
${indent}${indent}${indent}<name>${escapeXml(r.phase)}</name>
|
||||
${indent}${indent}${indent}<status>${escapeXml(r.status)}</status>
|
||||
${indent}${indent}${indent}<description>${escapeXml(r.description)}</description>
|
||||
${indent}${indent}</phase>`
|
||||
)
|
||||
.join("\n")}
|
||||
${indent}</implementation_roadmap>`;
|
||||
}
|
||||
|
||||
xml += `
|
||||
</project_specification>`;
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prompt instruction for structured output (simpler than XML instructions)
|
||||
*/
|
||||
export function getStructuredSpecPromptInstruction(): string {
|
||||
return `
|
||||
Analyze the project and provide a comprehensive specification with:
|
||||
|
||||
1. **project_name**: The name of the project
|
||||
2. **overview**: A comprehensive description of what the project does, its purpose, and key goals
|
||||
3. **technology_stack**: List all technologies, frameworks, libraries, and tools used
|
||||
4. **core_capabilities**: List the main features and capabilities the project provides
|
||||
5. **implemented_features**: For each implemented feature, provide:
|
||||
- name: Feature name
|
||||
- description: What it does
|
||||
- file_locations: Key files where it's implemented (optional)
|
||||
6. **additional_requirements**: Any system requirements, dependencies, or constraints (optional)
|
||||
7. **development_guidelines**: Development standards and best practices (optional)
|
||||
8. **implementation_roadmap**: Project phases with status (completed/in_progress/pending) (optional)
|
||||
|
||||
Be thorough in your analysis. The output will be automatically formatted as structured JSON.
|
||||
`;
|
||||
}
|
||||
export const APP_SPEC_XML_FORMAT = `
|
||||
The app_spec.txt file MUST follow this exact XML format:
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ export function createSpecGenerationOptions(
|
||||
allowedTools: [...TOOL_PRESETS.specGeneration],
|
||||
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||
...(config.abortController && { abortController: config.abortController }),
|
||||
...(config.outputFormat && { outputFormat: config.outputFormat }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import type { EventEmitter } from "../../lib/events.js";
|
||||
import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js";
|
||||
import {
|
||||
specOutputSchema,
|
||||
specToXml,
|
||||
getStructuredSpecPromptInstruction,
|
||||
type SpecOutput,
|
||||
} from "../../lib/app-spec-format.js";
|
||||
import { createLogger } from "../../lib/logger.js";
|
||||
import { createSpecGenerationOptions } from "../../lib/sdk-options.js";
|
||||
import { logAuthStatus } from "./common.js";
|
||||
@@ -42,9 +47,7 @@ export async function generateSpec(
|
||||
- Existing technologies and frameworks
|
||||
- Project structure and architecture
|
||||
- Current features and capabilities
|
||||
- Code patterns and conventions
|
||||
|
||||
After your analysis, OUTPUT the complete XML specification in your response. Do NOT attempt to write any files - just return the XML content and it will be saved automatically.`;
|
||||
- Code patterns and conventions`;
|
||||
} else {
|
||||
// Use default tech stack
|
||||
techStackDefaults = `Default Technology Stack:
|
||||
@@ -54,9 +57,7 @@ After your analysis, OUTPUT the complete XML specification in your response. Do
|
||||
- Styling: Tailwind CSS
|
||||
- Frontend: React
|
||||
|
||||
Use these technologies as the foundation for the specification.
|
||||
|
||||
OUTPUT the complete XML specification in your response. Do NOT attempt to write any files - just return the XML content and it will be saved automatically.`;
|
||||
Use these technologies as the foundation for the specification.`;
|
||||
}
|
||||
|
||||
const prompt = `You are helping to define a software project specification.
|
||||
@@ -70,7 +71,7 @@ ${techStackDefaults}
|
||||
|
||||
${analysisInstructions}
|
||||
|
||||
${getAppSpecFormatInstruction()}`;
|
||||
${getStructuredSpecPromptInstruction()}`;
|
||||
|
||||
logger.info("========== PROMPT BEING SENT ==========");
|
||||
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||
@@ -85,6 +86,10 @@ ${getAppSpecFormatInstruction()}`;
|
||||
const options = createSpecGenerationOptions({
|
||||
cwd: projectPath,
|
||||
abortController,
|
||||
outputFormat: {
|
||||
type: "json_schema",
|
||||
schema: specOutputSchema,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
||||
@@ -105,6 +110,7 @@ ${getAppSpecFormatInstruction()}`;
|
||||
|
||||
let responseText = "";
|
||||
let messageCount = 0;
|
||||
let structuredOutput: SpecOutput | null = null;
|
||||
|
||||
logger.info("Starting to iterate over stream...");
|
||||
|
||||
@@ -118,72 +124,49 @@ ${getAppSpecFormatInstruction()}`;
|
||||
);
|
||||
|
||||
if (msg.type === "assistant") {
|
||||
// Log the full message structure to debug
|
||||
logger.info(`Assistant msg keys: ${Object.keys(msg).join(", ")}`);
|
||||
const msgAny = msg as any;
|
||||
if (msgAny.message) {
|
||||
logger.info(
|
||||
`msg.message keys: ${Object.keys(msgAny.message).join(", ")}`
|
||||
);
|
||||
if (msgAny.message.content) {
|
||||
logger.info(
|
||||
`msg.message.content length: ${msgAny.message.content.length}`
|
||||
);
|
||||
for (const block of msgAny.message.content) {
|
||||
if (msgAny.message?.content) {
|
||||
for (const block of msgAny.message.content) {
|
||||
if (block.type === "text") {
|
||||
responseText += block.text;
|
||||
logger.info(
|
||||
`Block keys: ${Object.keys(block).join(", ")}, type: ${
|
||||
block.type
|
||||
}`
|
||||
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
||||
);
|
||||
if (block.type === "text") {
|
||||
responseText += block.text;
|
||||
logger.info(
|
||||
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
||||
);
|
||||
logger.info(`Text preview: ${block.text.substring(0, 200)}...`);
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_regeneration_progress",
|
||||
content: block.text,
|
||||
projectPath: projectPath,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
logger.info("Tool use:", block.name);
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_tool",
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_regeneration_progress",
|
||||
content: block.text,
|
||||
projectPath: projectPath,
|
||||
});
|
||||
} else if (block.type === "tool_use") {
|
||||
logger.info("Tool use:", block.name);
|
||||
events.emit("spec-regeneration:event", {
|
||||
type: "spec_tool",
|
||||
tool: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
logger.warn("msg.message.content is falsy");
|
||||
}
|
||||
} else {
|
||||
logger.warn("msg.message is falsy");
|
||||
// Log full message to see structure
|
||||
logger.info(
|
||||
`Full assistant msg: ${JSON.stringify(msg).substring(0, 1000)}`
|
||||
);
|
||||
}
|
||||
} else if (msg.type === "result" && (msg as any).subtype === "success") {
|
||||
logger.info("Received success result");
|
||||
logger.info(`Result value length: ${((msg as any).result || "").length}`);
|
||||
logger.info(
|
||||
`Final accumulated responseText length: ${responseText.length}`
|
||||
);
|
||||
// Don't overwrite responseText - the accumulated text contains the XML spec
|
||||
// The result.result field contains Claude's conversational summary (e.g., "I've created the spec...")
|
||||
// which is NOT what we want to save to app_spec.txt
|
||||
// See: https://github.com/AutoMaker-Org/automaker/issues/149
|
||||
// Check for structured output - this is the reliable way to get spec data
|
||||
const resultMsg = msg as any;
|
||||
if (resultMsg.structured_output) {
|
||||
structuredOutput = resultMsg.structured_output as SpecOutput;
|
||||
logger.info("✅ Received structured output");
|
||||
logger.debug("Structured output:", JSON.stringify(structuredOutput, null, 2));
|
||||
} else {
|
||||
logger.warn("⚠️ No structured output in result, will fall back to text parsing");
|
||||
}
|
||||
} else if (msg.type === "result") {
|
||||
// Handle all result types
|
||||
// Handle error result types
|
||||
const subtype = (msg as any).subtype;
|
||||
logger.info(`Result message: subtype=${subtype}`);
|
||||
if (subtype === "error_max_turns") {
|
||||
logger.error(
|
||||
"❌ Hit max turns limit! Claude used too many tool calls."
|
||||
);
|
||||
logger.info(`responseText so far: ${responseText.length} chars`);
|
||||
logger.error("❌ Hit max turns limit!");
|
||||
} else if (subtype === "error_max_structured_output_retries") {
|
||||
logger.error("❌ Failed to produce valid structured output after retries");
|
||||
throw new Error("Could not produce valid spec output");
|
||||
}
|
||||
} else if ((msg as { type: string }).type === "error") {
|
||||
logger.error("❌ Received error message from stream:");
|
||||
@@ -203,32 +186,45 @@ ${getAppSpecFormatInstruction()}`;
|
||||
|
||||
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
||||
logger.info(`Response text length: ${responseText.length} chars`);
|
||||
logger.info("========== FINAL RESPONSE TEXT ==========");
|
||||
logger.info(responseText || "(empty)");
|
||||
logger.info("========== END RESPONSE TEXT ==========");
|
||||
|
||||
if (!responseText || responseText.trim().length === 0) {
|
||||
logger.error("❌ WARNING: responseText is empty! Nothing to save.");
|
||||
}
|
||||
// Determine XML content to save
|
||||
let xmlContent: string;
|
||||
|
||||
// Extract XML content from response - Claude might include conversational text before/after
|
||||
// See: https://github.com/AutoMaker-Org/automaker/issues/149
|
||||
let xmlContent = responseText;
|
||||
const xmlStart = responseText.indexOf("<project_specification>");
|
||||
const xmlEnd = responseText.lastIndexOf("</project_specification>");
|
||||
|
||||
if (xmlStart !== -1 && xmlEnd !== -1) {
|
||||
// Extract just the XML content
|
||||
xmlContent = responseText.substring(xmlStart, xmlEnd + "</project_specification>".length);
|
||||
logger.info(`Extracted XML content: ${xmlContent.length} chars (from position ${xmlStart})`);
|
||||
} else if (xmlStart === -1) {
|
||||
logger.warn("⚠️ Response does not contain <project_specification> tag - saving raw response");
|
||||
if (structuredOutput) {
|
||||
// Use structured output - convert JSON to XML
|
||||
logger.info("✅ Using structured output for XML generation");
|
||||
xmlContent = specToXml(structuredOutput);
|
||||
logger.info(`Generated XML from structured output: ${xmlContent.length} chars`);
|
||||
} else {
|
||||
logger.warn("⚠️ Response has incomplete XML (missing closing tag) - saving raw response");
|
||||
// Fallback: Extract XML content from response text
|
||||
// Claude might include conversational text before/after
|
||||
// See: https://github.com/AutoMaker-Org/automaker/issues/149
|
||||
logger.warn("⚠️ No structured output, falling back to text parsing");
|
||||
logger.info("========== FINAL RESPONSE TEXT ==========");
|
||||
logger.info(responseText || "(empty)");
|
||||
logger.info("========== END RESPONSE TEXT ==========");
|
||||
|
||||
if (!responseText || responseText.trim().length === 0) {
|
||||
throw new Error("No response text and no structured output - cannot generate spec");
|
||||
}
|
||||
|
||||
xmlContent = responseText;
|
||||
const xmlStart = responseText.indexOf("<project_specification>");
|
||||
const xmlEnd = responseText.lastIndexOf("</project_specification>");
|
||||
|
||||
if (xmlStart !== -1 && xmlEnd !== -1) {
|
||||
// Extract just the XML content
|
||||
xmlContent = responseText.substring(xmlStart, xmlEnd + "</project_specification>".length);
|
||||
logger.info(`Extracted XML content: ${xmlContent.length} chars (from position ${xmlStart})`);
|
||||
} else if (xmlStart === -1) {
|
||||
logger.warn("⚠️ Response does not contain <project_specification> tag - saving raw response");
|
||||
} else {
|
||||
logger.warn("⚠️ Response has incomplete XML (missing closing tag) - saving raw response");
|
||||
}
|
||||
}
|
||||
|
||||
// Save spec to .automaker directory
|
||||
const specDir = await ensureAutomakerDir(projectPath);
|
||||
await ensureAutomakerDir(projectPath);
|
||||
const specPath = getAppSpecPath(projectPath);
|
||||
|
||||
logger.info("Saving spec to:", specPath);
|
||||
|
||||
Reference in New Issue
Block a user