diff --git a/.gitignore b/.gitignore index a5512679..fe35b2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ out/ dist/ *.vsix +# Generated files +*.generated.ts + # IDE .idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0037e0c3..eb75df4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [3.14.4](https://github.com/breaking-brake/cc-wf-studio/compare/v3.14.3...v3.14.4) (2026-01-13) + +### Improvements + +* stabilize AI editing flow with explicit process diagram ([#446](https://github.com/breaking-brake/cc-wf-studio/issues/446)) ([633aec9](https://github.com/breaking-brake/cc-wf-studio/commit/633aec9de3512da17a7330fa84e09a116517cfe7)) + ## [3.14.3](https://github.com/breaking-brake/cc-wf-studio/compare/v3.14.2...v3.14.3) (2026-01-13) ### Bug Fixes diff --git a/package-lock.json b/package-lock.json index fcdc1b8e..692e5a60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-wf-studio", - "version": "3.14.3", + "version": "3.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio", - "version": "3.14.3", + "version": "3.14.4", "license": "AGPL-3.0-or-later", "dependencies": { "@modelcontextprotocol/sdk": "^1.25.2", diff --git a/package.json b/package.json index ff18911d..dd2f47ce 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cc-wf-studio", "displayName": "Claude Code Workflow Studio", "description": "Visual workflow editor for Claude Code Slash Commands, Sub Agents, Agent Skills, and MCP Tools", - "version": "3.14.3", + "version": "3.14.4", "publisher": "breaking-brake", "icon": "resources/icon.png", "repository": { @@ -89,7 +89,8 @@ "scripts": { "vscode:prepublish": "npm run build", "generate:toon": "npx tsx scripts/generate-toon-schema.ts", - "build": "npm run generate:toon && npm run build:webview && npm run build:extension", + "generate:editing-flow": "npx tsx scripts/generate-editing-flow.ts", + "build": "npm run generate:toon && npm run generate:editing-flow && npm run build:webview && npm run build:extension", "build:dev": "npm run build:webview:dev && npm run build:extension:dev", "build:webview": "cd src/webview && npm run build", "build:webview:dev": "cd src/webview && npm run build:dev", diff --git a/resources/ai-editing-process-flow.md b/resources/ai-editing-process-flow.md new file mode 100644 index 00000000..d35fd15f --- /dev/null +++ b/resources/ai-editing-process-flow.md @@ -0,0 +1,68 @@ +# AI Editing Process Flow + +This file defines the AI editing process flow for workflow refinement. +It is used by the `generate-editing-flow.ts` script to generate TypeScript constants. + +## Mermaid Diagram + +```mermaid +flowchart TD + A[1. Review current workflow completely] --> B[2. Analyze user request] + B --> C{3. Request type?} + C -->|Question/Understanding| D[Answer without editing - use clarification status] + C -->|Edit request| E{4. Is request clear and specific?} + C -->|Cannot determine| F[Return clarification request] + E -->|Unclear/Ambiguous| F + E -->|Clear| G[5. Identify ONLY the specific changes needed] + G --> H{6. Change type?} + H -->|Add| I[7a. Add new nodes/connections] + H -->|Modify| J[7b. Modify ONLY target nodes] + H -->|Delete| K[7c. Delete target nodes/connections] + I --> L[8. COPY unchanged nodes with their EXACT original data] + J --> L + K --> L + L --> M[9. Output complete workflow JSON] +``` + +## Process Steps + +1. Review current workflow: Understand ALL existing nodes and their data fields completely +2. Analyze user request: Identify what the user wants +3. Request type check: Is this a question/understanding request OR an edit request? +4. Clarity check: If edit request is vague or ambiguous, return clarification +5. Identify changes: List ONLY the specific nodes/connections that need modification +6. Determine change type: Categorize as add, modify, or delete operation +7. Apply minimal changes: Execute ONLY the identified changes +8. Preserve unchanged: COPY ALL unchanged nodes with their EXACT original data field +9. Output: Return the complete workflow with changes applied + +## Request Type Guidelines + +### Question or Understanding Request + +- Questions starting with "What", "Why", "How", "Explain", "Tell me" +- Requests for explanation or clarification about the workflow +- No action verbs like "add", "modify", "delete", "change", "update", "remove" +- Response: Use `{ status: "clarification", message: "your answer" }` + +### Edit Request + +- Contains action verbs: "add", "modify", "delete", "change", "update", "remove", "insert" +- Specifies target node or location +- Describes desired outcome or new content +- Response: Process through steps 4-9, then use `{ status: "success", ... }` + +### Unclear Request + +- Vague instructions like "improve", "make better", "fix", "optimize" +- Missing target specification (which node? where?) +- Ambiguous or multiple interpretations possible +- Response: Use `{ status: "clarification", message: "ask for details" }` + +## Clarification Triggers + +- User request does not specify which node to modify +- User request is ambiguous about the desired outcome +- Multiple valid interpretations exist for the request +- Required information (node name, position, content) is missing +- User request conflicts with existing workflow structure diff --git a/scripts/generate-editing-flow.ts b/scripts/generate-editing-flow.ts new file mode 100644 index 00000000..a02b3b9a --- /dev/null +++ b/scripts/generate-editing-flow.ts @@ -0,0 +1,224 @@ +/** + * AI Editing Flow Generator + * + * Parses ai-editing-process-flow.md and generates TypeScript constants. + * This allows the Mermaid diagram and process steps to be maintained in a + * human-readable markdown file while being embedded in the prompt builder. + * + * Executed during build: npm run build + */ + +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +const RESOURCES_DIR = path.resolve(__dirname, '../resources'); +const MD_PATH = path.join(RESOURCES_DIR, 'ai-editing-process-flow.md'); +const OUTPUT_PATH = path.resolve( + __dirname, + '../src/extension/services/editing-flow-constants.generated.ts' +); + +interface EditingFlowData { + mermaidDiagram: string; + steps: string[]; + requestTypeGuidelines: { + questionOrUnderstanding: string[]; + editRequest: string[]; + unclearRequest: string[]; + }; + clarificationTriggers: string[]; +} + +/** + * Extract content between ```mermaid and ``` markers + */ +function extractMermaidDiagram(content: string): string { + const match = content.match(/```mermaid\n([\s\S]*?)```/); + if (!match) { + throw new Error('Mermaid diagram not found in markdown file'); + } + return match[1].trim(); +} + +/** + * Extract numbered list items under a heading + */ +function extractNumberedList(content: string, heading: string): string[] { + const headingRegex = new RegExp(`## ${heading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`); + const match = content.match(headingRegex); + if (!match) { + return []; + } + + const section = match[1]; + const items: string[] = []; + const lines = section.split('\n'); + + for (const line of lines) { + const numberedMatch = line.match(/^\d+\.\s+(.+)$/); + if (numberedMatch) { + items.push(numberedMatch[1].trim()); + } + } + + return items; +} + +/** + * Extract bullet list items under a subsection heading + */ +function extractBulletList(content: string, mainHeading: string, subHeading: string): string[] { + // Find the main section + const mainRegex = new RegExp(`## ${mainHeading}\\n\\n([\\s\\S]*?)(?=\\n## [^#]|$)`); + const mainMatch = content.match(mainRegex); + if (!mainMatch) { + return []; + } + + const mainSection = mainMatch[1]; + + // Find the subsection + const subRegex = new RegExp(`### ${subHeading}\\n\\n([\\s\\S]*?)(?=\\n### |$)`); + const subMatch = mainSection.match(subRegex); + if (!subMatch) { + return []; + } + + const subSection = subMatch[1]; + const items: string[] = []; + const lines = subSection.split('\n'); + + for (const line of lines) { + const bulletMatch = line.match(/^-\s+(.+)$/); + if (bulletMatch) { + // Remove backticks from inline code + items.push(bulletMatch[1].trim().replace(/`/g, '')); + } + } + + return items; +} + +/** + * Extract bullet list items under a heading (top-level) + */ +function extractTopLevelBulletList(content: string, heading: string): string[] { + const headingRegex = new RegExp(`## ${heading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`); + const match = content.match(headingRegex); + if (!match) { + return []; + } + + const section = match[1]; + const items: string[] = []; + const lines = section.split('\n'); + + for (const line of lines) { + const bulletMatch = line.match(/^-\s+(.+)$/); + if (bulletMatch) { + items.push(bulletMatch[1].trim()); + } + } + + return items; +} + +/** + * Parse the markdown file and extract all data + */ +function parseMarkdown(content: string): EditingFlowData { + return { + mermaidDiagram: extractMermaidDiagram(content), + steps: extractNumberedList(content, 'Process Steps'), + requestTypeGuidelines: { + questionOrUnderstanding: extractBulletList( + content, + 'Request Type Guidelines', + 'Question or Understanding Request' + ), + editRequest: extractBulletList(content, 'Request Type Guidelines', 'Edit Request'), + unclearRequest: extractBulletList(content, 'Request Type Guidelines', 'Unclear Request'), + }, + clarificationTriggers: extractTopLevelBulletList(content, 'Clarification Triggers'), + }; +} + +/** + * Generate TypeScript code from parsed data + */ +function generateTypeScript(data: EditingFlowData): string { + const toStringArray = (arr: string[]) => + arr.map((item) => ` '${item.replace(/'/g, "\\'")}',`).join('\n'); + + return `/** + * AI Editing Flow Constants + * + * AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY + * Generated from: resources/ai-editing-process-flow.md + * To modify, edit the markdown file and run: npm run generate:editing-flow + */ + +/** + * Mermaid diagram representing the AI editing process flow + */ +export const EDITING_PROCESS_MERMAID_DIAGRAM = \`${data.mermaidDiagram}\`; + +/** + * Step-by-step process for AI editing + */ +export const EDITING_PROCESS_STEPS: readonly string[] = [ +${toStringArray(data.steps)} +] as const; + +/** + * Guidelines for identifying request types + */ +export const REQUEST_TYPE_GUIDELINES = { + questionOrUnderstanding: [ +${toStringArray(data.requestTypeGuidelines.questionOrUnderstanding)} + ] as const, + editRequest: [ +${toStringArray(data.requestTypeGuidelines.editRequest)} + ] as const, + unclearRequest: [ +${toStringArray(data.requestTypeGuidelines.unclearRequest)} + ] as const, +} as const; + +/** + * Conditions that should trigger a clarification request + */ +export const CLARIFICATION_TRIGGERS: readonly string[] = [ +${toStringArray(data.clarificationTriggers)} +] as const; +`; +} + +async function generateEditingFlow(): Promise { + console.log('Generating editing flow constants from ai-editing-process-flow.md...'); + + try { + // Read markdown file + const mdContent = await fs.readFile(MD_PATH, 'utf-8'); + + // Parse markdown + const data = parseMarkdown(mdContent); + + // Generate TypeScript + const tsContent = generateTypeScript(data); + + // Write output file + await fs.writeFile(OUTPUT_PATH, tsContent, 'utf-8'); + + console.log('Editing flow constants generated successfully:'); + console.log(` Source: ${path.relative(process.cwd(), MD_PATH)}`); + console.log(` Output: ${path.relative(process.cwd(), OUTPUT_PATH)}`); + console.log(` Steps: ${data.steps.length}`); + console.log(` Clarification triggers: ${data.clarificationTriggers.length}`); + } catch (error) { + console.error('Failed to generate editing flow constants:', error); + process.exit(1); + } +} + +generateEditingFlow(); diff --git a/src/extension/services/refinement-prompt-builder.ts b/src/extension/services/refinement-prompt-builder.ts index 316ade8a..d46a887a 100644 --- a/src/extension/services/refinement-prompt-builder.ts +++ b/src/extension/services/refinement-prompt-builder.ts @@ -8,6 +8,12 @@ import { encode } from '@toon-format/toon'; import type { ConversationHistory, Workflow } from '../../shared/types/workflow-definition'; import { getCurrentLocale } from '../i18n/i18n-service'; +import { + CLARIFICATION_TRIGGERS, + EDITING_PROCESS_MERMAID_DIAGRAM, + EDITING_PROCESS_STEPS, + REQUEST_TYPE_GUIDELINES, +} from './editing-flow-constants.generated'; import type { ValidationErrorInfo } from './refinement-service'; import type { SchemaLoadResult } from './schema-loader-service'; import type { SkillRelevanceScore } from './skill-relevance-matcher'; @@ -38,27 +44,26 @@ export class RefinementPromptBuilder { responseLocale: locale, role: 'expert workflow designer for Claude Code Workflow Studio', task: 'Refine the existing workflow based on user feedback', + // AI Editing Process Flow - MUST follow this process strictly + // Generated from: resources/ai-editing-process-flow.md + editingProcessFlow: { + description: 'You MUST follow this editing process step by step. Do NOT skip any steps.', + mermaidDiagram: EDITING_PROCESS_MERMAID_DIAGRAM, + steps: EDITING_PROCESS_STEPS, + requestTypeGuidelines: REQUEST_TYPE_GUIDELINES, + clarificationTriggers: CLARIFICATION_TRIGGERS, + }, currentWorkflow: { id: this.currentWorkflow.id, name: this.currentWorkflow.name, + // Include COMPLETE node data for ALL node types to enable precise editing nodes: this.currentWorkflow.nodes.map((n) => ({ id: n.id, type: n.type, name: n.name, - 'position.x': n.position.x, - 'position.y': n.position.y, - // Include data for skill nodes to preserve exact skill information - ...(n.type === 'skill' && n.data - ? { - data: { - name: (n.data as { name?: string }).name, - description: (n.data as { description?: string }).description, - scope: (n.data as { scope?: string }).scope, - validationStatus: (n.data as { validationStatus?: string }).validationStatus, - outputPorts: (n.data as { outputPorts?: number }).outputPorts, - }, - } - : {}), + position: { x: n.position.x, y: n.position.y }, + // Include data for ALL node types - this is CRITICAL for preserving existing content + data: n.data, })), connections: this.currentWorkflow.connections.map((c) => ({ id: c.id, @@ -67,6 +72,11 @@ export class RefinementPromptBuilder { fromPort: c.fromPort, toPort: c.toPort, })), + // Include subAgentFlows if present + ...(this.currentWorkflow.subAgentFlows && + this.currentWorkflow.subAgentFlows.length > 0 && { + subAgentFlows: this.currentWorkflow.subAgentFlows, + }), }, conversationHistory: recentMessages.map((m) => ({ sender: m.sender, @@ -74,13 +84,12 @@ export class RefinementPromptBuilder { })), userRequest: this.userMessage, refinementGuidelines: [ - 'Preserve existing nodes unless explicitly requested to remove', - 'Add new nodes ONLY if user asks for new functionality', - 'Modify node properties based on feedback', + 'CRITICAL: Preserve ALL unchanged nodes with their EXACT original data - do not modify or regenerate', + 'Only modify nodes that are explicitly requested to change', + 'Add new nodes ONLY if user explicitly asks for new functionality', 'Maintain workflow connectivity and validity', 'Respect node IDs - do not regenerate IDs for unchanged nodes', - 'Update only what the user requested', - 'Node names must match pattern /^[a-zA-Z0-9_-]+$/ (ASCII alphanumeric, hyphens, underscores only - NO spaces or non-ASCII characters)', + 'Node names must match pattern /^[a-zA-Z0-9_-]+$/ (ASCII alphanumeric, hyphens, underscores only)', ], nodePositioningGuidelines: [ 'Horizontal spacing: 300px', @@ -92,12 +101,10 @@ export class RefinementPromptBuilder { 'Branch nodes: offset vertically by 150px', ], skillNodeConstraints: [ - 'Must have exactly 1 output port', - 'If branching needed, add ifElse/switch after Skill', - 'Never modify outputPorts field', - 'PRESERVE existing skill node data exactly - do not change name, description, scope', - 'When skill node exists in currentWorkflow, copy its data field exactly', - 'Only use skill names from availableSkills list for NEW skill nodes', + 'Must have exactly 1 output port (outputPorts: 1)', + 'If branching needed, add ifElse/switch node after the Skill node', + 'For existing skill nodes: COPY data field exactly from currentWorkflow', + 'For NEW skill nodes: only use names from availableSkills list', ], branchingNodeSelection: { ifElse: '2-way conditional branching (true/false)', @@ -111,27 +118,41 @@ export class RefinementPromptBuilder { })), workflowSchema: this.schemaResult.schemaString || JSON.stringify(this.schemaResult.schema), outputFormat: { - success: { + description: + 'You MUST output exactly ONE JSON object. Do NOT output multiple JSON blocks or explanatory text.', + successExample: { status: 'success', - message: 'Brief description of changes', - 'values.workflow': '{...}', + message: 'Brief description of what was changed', + values: { + workflow: { + id: 'workflow-id', + name: 'workflow-name', + nodes: ['... all nodes with data ...'], + connections: ['... all connections ...'], + }, + }, }, - clarification: { + clarificationExample: { status: 'clarification', - message: 'Your question here', + message: 'Your answer or question here', }, - error: { + errorExample: { status: 'error', message: 'Error description', }, }, criticalRules: [ - 'ALWAYS output valid JSON', - 'NEVER include markdown code blocks', - 'Even if no changes, wrap in success response', - 'status and message fields REQUIRED', - 'If you need clarification, use { status: "clarification", message: "..." } format', - 'NEVER ask questions in plain text - use clarification JSON format', + 'OUTPUT FORMAT: You MUST output exactly ONE JSON object - no explanatory text, no multiple JSON blocks', + 'DO NOT output workflow JSON separately from status JSON - they must be combined in ONE response', + 'DO NOT wrap JSON in markdown code blocks (```json) - output raw JSON only', + 'For success: workflow MUST be nested inside values.workflow, not as a separate JSON block', + 'Follow the editingProcessFlow steps in order - do NOT skip steps', + 'For questions/understanding requests: use clarification status with your answer', + 'For unclear edit requests: use clarification status to ask for details', + 'For clear edit requests: use success status with the modified workflow inside values.workflow', + 'CRITICAL: When outputting workflow, COPY unchanged nodes with their EXACT original data', + 'NEVER regenerate or modify data for nodes that were not explicitly requested to change', + 'status and message fields are REQUIRED in every response', ], // Include previous validation errors for retry context ...(this.previousValidationErrors && diff --git a/src/webview/package-lock.json b/src/webview/package-lock.json index 7fe04769..01a923a6 100644 --- a/src/webview/package-lock.json +++ b/src/webview/package-lock.json @@ -1,12 +1,12 @@ { "name": "cc-wf-studio-webview", - "version": "3.14.3", + "version": "3.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cc-wf-studio-webview", - "version": "3.14.3", + "version": "3.14.4", "license": "AGPL-3.0-or-later", "dependencies": { "@radix-ui/react-collapsible": "^1.1.12", diff --git a/src/webview/package.json b/src/webview/package.json index db172d7c..8d9c628e 100644 --- a/src/webview/package.json +++ b/src/webview/package.json @@ -1,6 +1,6 @@ { "name": "cc-wf-studio-webview", - "version": "3.14.3", + "version": "3.14.4", "private": true, "license": "AGPL-3.0-or-later", "type": "module",