UNPKG

cmte

Version:

Design by Committee™ except it's just you and LLMs

35 lines 292 kB
[ { "content": "# Committee Design Specification\n\n## Overview\n\nCommittee is a system that facilitates structured, iterative design processes through simulated multi-stakeholder feedback. This specification defines the architecture and components used to create workflows that orchestrate tasks, manage context, and produce outputs.\n\n## Core Concepts\n\nCommittee uses a hierarchical structure composed of the following core concepts:\n\n1. **Task**: The atomic unit of work, typically involving a template for an LLM call\n2. **Set**: A collection of tasks or other sets, with a defined execution mode\n3. **Phase**: A major stage in a workflow with its own set of related tasks/sets\n4. **Workflow**: A complete process that orchestrates multiple phases\n\n## Component Definitions\n\n### Task\n\nA task is the basic unit of work in Committee. Tasks are typically template-driven and produce outputs that can be used by downstream components.\n\n```yaml\n# Example task definition in frontmatter\n---\nname: \"thinking-task\"\nrole: \"service\"\nrequiredInput: [\"directiveName\", \"serviceName\"]\nrequiredOutput: [\"thinking\"]\n---\n```\n\n#### Task Properties\n\n| Property | Description | Required |\n|----------|-------------|----------|\n| `name` | Unique identifier for the task | Yes |\n| `role` | Role to use for the task (e.g., \"service\", \"architect\") | No |\n| `requiredInput` | List of context variables required as input | No |\n| `requiredOutput` | List of context variables produced as output | No |\n\n### Set\n\nA set organizes related tasks or other sets with a defined execution mode. Sets can be executed sequentially or in parallel.\n\n```yaml\n# Example set definition\nname: \"service-feedback\"\ndescription: \"Collect feedback from a service\"\nexecution: \"sequential\" # or \"parallel\"\ntasks:\n - description: \"Think about requirements\"\n useTask: \"thinking.task\"\n variables:\n role: \"service\"\n \n - description: \"Generate response\"\n useTask: \"response.task\"\n variables:\n role: \"service\"\nrequiredInput: [\"directiveName\", \"serviceName\"]\nrequiredOutput: [\"serviceRequirements\"]\n```\n\n#### Set Properties\n\n| Property | Description | Required |\n|----------|-------------|----------|\n| `name` | Unique identifier for the set | Yes |\n| `description` | Human-readable description of the set's purpose | No |\n| `execution` | Mode of execution: \"sequential\" or \"parallel\" | Yes |\n| `tasks` | List of tasks to execute | Yes (if not using `set`) |\n| `set` | List of child sets to execute | Yes (if not using `tasks`) |\n| `requiredInput` | List of context variables required as input | No |\n| `requiredOutput` | List of context variables produced as output | No |\n| `for_each` | Collection to iterate over | No |\n| `variables` | Variables to set for each task | No |\n\n### Phase\n\nA phase represents a major stage in a workflow and contains a set of tasks or other sets.\n\n```yaml\n# Example phase definition\nname: \"requirements-collection\"\ndescription: \"Collect requirements from all services\"\nexecution: \"sequential\"\nhumanInputRequired: [] # No human input required for this phase\nset:\n - description: \"Collect requirements from each service in parallel\"\n template: \"service-feedback\"\n execution: \"parallel\"\n for_each: \"services\"\n variables:\n serviceName: \"{{item.name}}\"\n serviceDescription: \"{{item.description}}\"\n \n - description: \"Synthesize all the collected requirements\"\n useSet: \"architect-synthesis\"\n requiredInput: [\"serviceRequirements\"]\n requiredOutput: [\"synthesizedRequirements\"]\ndependsOn: [] # No dependencies for this phase\n```\n\n```yaml\n# Example phase with human input required\nname: \"draft-spec-review\"\ndescription: \"Review the draft specification\"\nhumanInputRequired:\n - \"_output/draft-spec/draft-spec.o.md\"\nexecution: \"sequential\"\nset:\n - description: \"Process human feedback on draft spec\"\n useSet: \"feedback-processor\"\ndependsOn: [\"draft-spec-creation\"]\n```\n\n#### Phase Properties\n\n| Property | Description | Required |\n|----------|-------------|----------|\n| `name` | Unique identifier for the phase | Yes |\n| `description` | Human-readable description of the phase's purpose | No |\n| `execution` | Mode of execution: \"sequential\" or \"parallel\" | Yes |\n| `humanInputRequired` | List of files requiring human review before starting this phase | No |\n| `set` | List of sets to execute | Yes |\n| `dependsOn` | List of phases this phase depends on | No |\n| `requiredInput` | List of context variables required as input | No |\n| `requiredOutput` | List of context variables produced as output | No |\n| `condition` | Condition for executing this phase | No |\n\n### Workflow\n\nA workflow is the top-level container that orchestrates multiple phases to achieve a complete process.\n\n```yaml\n# Example workflow definition\nname: \"directive-review\"\ndescription: \"Review and design for a directive\"\noutputPath: \"_output/directive-review\" # Base path for all outputs\nphases:\n - usePhase: \"requirements-collection\"\n - usePhase: \"requirements-synthesis\"\n dependsOn: [\"requirements-collection\"]\n - usePhase: \"draft-spec-creation\"\n dependsOn: [\"requirements-synthesis\"]\n - usePhase: \"draft-spec-review\"\n dependsOn: [\"draft-spec-creation\"]\n```\n\n#### Workflow Properties\n\n| Property | Description | Required |\n|----------|-------------|----------|\n| `name` | Unique identifier for the workflow | Yes |\n| `description` | Human-readable description of the workflow's purpose | No |\n| `outputPath` | Base path for all workflow outputs | Yes |\n| `phases` | List of phases to execute | Yes |\n\n## Component References\n\nComponents can reference other components using a path-like syntax. The system resolves references using a specificity cascade:\n\n1. First looks in the current workflow's templates directory\n2. If not found, looks in the global templates directory\n3. If still not found, raises an error\n\nReference types:\n\n- `useTask`: Reference to a task template\n- `useSet`: Reference to a set template\n- `usePhase`: Reference to a phase template\n- `template`: Reference to a template that will be instantiated (potentially multiple times)\n\n## File Structure and Naming Conventions\n\n```\nworkflows/\n ├── templates/ # Global templates\n │ ├── tasks/ \n │ │ ├── thinking.task.md # Task template with frontmatter\n │ │ ├── response.task.md \n │ │ └── human-review.task.md \n │ │\n │ ├── sets/ \n │ │ ├── two-phase-process.set.yaml\n │ │ ├── service-feedback.set.yaml\n │ │ └── synthesis.set.yaml\n │ │\n │ └── phases/ \n │ └── feedback-collection.phase.yaml\n │\n ├── directive-review/ # A specific workflow\n │ ├── workflow.yaml # Main workflow definition\n │ │\n │ ├── templates/ # Workflow-specific templates\n │ │ ├── tasks/\n │ │ │ └── directive-thinking.task.md\n │ │ └── sets/\n │ │ └── directive-feedback.set.yaml\n │ │\n │ └── phases/ # Workflow-specific phases\n │ ├── requirements.phase.yaml\n │ ├── draft-spec.phase.yaml\n │ └── final-spec.phase.yaml\n```\n\n### Naming Conventions\n\n- Task templates: `[name].task.md` or `[name].task.yaml`\n- Set templates: `[name].set.yaml`\n- Phase templates: `[name].phase.yaml`\n- Workflow definition: `workflow.yaml`\n\n## Output Path Standardization\n\nCommittee uses a standardized convention for output paths. All output files end with `.o.md` and follow this structure:\n\n```\n{workflow_path}/{phase}/{set}/{task}.o.md # For task outputs\n{workflow_path}/{phase}/{set}.o.md # For set outputs\n{workflow_path}/{phase}/{phase}.o.md # For phase outputs\n```\n\nWhere `workflow_path` is defined in the workflow configuration. This standardization makes outputs predictable and easy to locate without the need for per-component output path configuration.\n\nFor example, a task output might be located at:\n```\n_output/directive-review/requirements-collection/service-feedback-parser/thinking.o.md\n```\n\n## Context Management\n\nCommittee manages context throughout the workflow execution:\n\n1. Each component declares what context it requires (`requiredInput`) and what it produces (`requiredOutput`)\n2. Context flows from upstream components to downstream components based on dependencies\n3. Components can access context variables using `{{variableName}}` syntax in templates\n4. For parallel execution, all required context must be available at the start\n5. For sequential execution, context can flow between steps\n\n## State Management\n\nCommittee uses a state file (`resume.yaml`) to track execution progress. This file is append-only to simplify state management and avoid race conditions:\n\n```yaml\n# Simplified resume.yaml example\nawaitingHumanInput: true\nfilesAwaitingReview:\n - \"_output/directive-review/draft-spec/draft-spec.o.md\"\n \n# Simple list of completed sets (append-only)\nprogress:\n - phaseId: \"requirements-collection\"\n setId: \"service-feedback-parser-service\"\n - phaseId: \"requirements-collection\"\n setId: \"service-feedback-validator-service\"\n - phaseId: \"requirements-collection\"\n setId: \"requirements-synthesis\"\n```\n\nThis state file enables:\n1. Resuming execution after interruption (at the set level)\n2. Tracking human review requirements\n3. Recording progress without complex state management\n\n## Human Input Handling\n\nHuman input is managed at phase boundaries rather than as a specific task type. When a phase completes, the system checks if the next phase requires human input (`humanInputRequired` property). If so:\n\n1. The system adds the required files to `filesAwaitingReview` in the resume file\n2. Sets `awaitingHumanInput` to true\n3. The CLI exits with a message indicating which files need review\n4. When the user marks the files as reviewed, the system will continue with the next phase\n\nThis approach ensures human interaction happens at well-defined points in the workflow rather than arbitrarily within phases.\n\n> **Note**: If a workflow needs multiple human interaction points within what would conceptually be a single phase, it is recommended to split it into multiple phases with naming conventions like `phase-1` and `phase-2` to indicate their relationship.\n\n## CLI Commands\n\nCommittee provides a command-line interface for interacting with workflows:\n\n- `cmte <workflow-folder>`: Start or resume a workflow\n- `cmte next <workflow-folder>`: Run the next task in a workflow\n- `cmte review <workflow-folder> <file-path>`: Mark a file as reviewed\n- `cmte init <workflow-folder>`: Initialize a workflow without running any tasks\n\n## Example: Complete Workflow\n\nHere's a complete example of a simple requirements collection workflow:\n\n### Workflow Definition\n\n```yaml\n# workflows/simple-requirements/workflow.yaml\nname: \"simple-requirements\"\ndescription: \"Collect and synthesize requirements\"\noutputPath: \"_output/simple-requirements\"\nphases:\n - usePhase: \"requirements-collection\"\n - usePhase: \"requirements-synthesis\"\n dependsOn: [\"requirements-collection\"]\n - usePhase: \"human-review\"\n dependsOn: [\"requirements-synthesis\"]\n humanInputRequired:\n - \"_output/simple-requirements/requirements-synthesis/requirements-synthesis.o.md\"\n```\n\n### Phase Definition\n\n```yaml\n# workflows/simple-requirements/phases/requirements-collection.phase.yaml\nname: \"requirements-collection\"\ndescription: \"Collect requirements from all services\"\nexecution: \"sequential\"\nhumanInputRequired: []\nset:\n - description: \"Collect requirements from each service in parallel\"\n template: \"service-feedback\"\n execution: \"parallel\"\n for_each: \"services\"\n variables:\n serviceName: \"{{item.name}}\"\n serviceDescription: \"{{item.description}}\"\n requiredInput: [\"directiveName\"]\n requiredOutput: [\"serviceRequirements\"]\n```\n\n### Set Template\n\n```yaml\n# workflows/templates/sets/service-feedback.set.yaml\nname: \"service-feedback\"\ndescription: \"Collect feedback from a service\"\nexecution: \"sequential\"\ntasks:\n - description: \"Think about requirements\"\n useTask: \"thinking.task\"\n variables:\n role: \"service\"\n \n - description: \"Generate response\"\n useTask: \"response.task\"\n variables:\n role: \"service\"\nrequiredInput: [\"directiveName\", \"serviceName\"]\nrequiredOutput: [\"serviceRequirements\"]\n```\n\n### Task Template\n\n```markdown\n# workflows/templates/tasks/thinking.task.md\n---\nname: \"thinking-task\"\nrole: \"{{role}}\"\nrequiredInput: [\"directiveName\", \"serviceName\"]\nrequiredOutput: [\"thinking\"]\n---\n\n# {{directiveName}} Type Requirements Analysis - Thinking Phase\n\n## Context\nThis is an example template for the requirements thinking phase.\n\n## Service Context\nYou are the lead developer for {{serviceName}}, which is responsible for {{serviceDescription}}.\n\n## Task\nThink about what your service needs from the {{directiveName}} type system. Consider:\n\n1. What properties must exist in {{directiveName}} types?\n2. What pain points exist in the current implementation?\n3. How would more structured types improve your service's code?\n4. What type discriminators would make processing more robust?\n\n## Reflection\nBefore providing your final answer, reflect on:\n- Essential vs. nice-to-have properties\n- Implementation complexity\n- Cross-service impacts\n- Concrete use cases\n```\n\n## Implementation Considerations\n\nWhen implementing the Committee system:\n\n1. **Component Registry**: Create a registry that loads and indexes all components\n2. **Context Manager**: Implement a robust context management system that tracks variables\n3. **Executor**: Build executors for different execution modes (sequential, parallel)\n4. **Template Rendering**: Use a template engine that supports variable substitution\n5. **State Management**: Implement append-only state tracking for simplicity and robustness\n6. **Output Path Generation**: Generate standardized output paths based on workflow structure\n7. **Human Input Handling**: Manage human input at phase boundaries\n8. **Path Resolution**: Implement the specificity cascade for component references\n\n## Benefits of This Design\n\n1. **Composability**: Components can be composed and reused across workflows\n2. **Clarity**: Clear separation of concerns between different component types\n3. **Flexibility**: Support for both sequential and parallel execution\n4. **Maintainability**: Templates and their configurations are kept together\n5. **Predictability**: Standardized output paths make results easy to locate\n6. **Simplicity**: Append-only state management eliminates complex state tracking\n7. **Human Integration**: Clear, phase-boundary integration points for human review ", "filename": "SPEC.md" }, { "content": "# {{directiveName}} Type Requirements - Response Phase\n\n## Context\nYou are finalizing your requirements for {{directiveName}} types.\n\n## Service Context\nYou are the lead developer for {{serviceName}}, which is responsible for {{serviceDescription}}.\n\n## Previous Thinking\nYou previously analyzed the requirements and came to these conclusions:\n\n{{thinking}}\n\n## Task\nBased on your previous analysis, create a formal list of requirements for {{directiveName}} types from your service's perspective:\n\n1. Provide a prioritized list of properties that should be included in {{directiveName}} types\n2. Specify any type discriminators that would be helpful\n3. Highlight integration points with other services\n4. Suggest validation rules for these types\n\n## Response Format\nStructure your response with clear headings and bullet points for each requirement category. Include code examples where appropriate to illustrate your points. Be concise but thorough. ", "filename": "_temp/requirements-collection/requirements-response/requirements-response.template.md" }, { "content": "import yaml from 'js-yaml';\nimport { readFile, fileExists } from '../utils/fs.ts';\nimport logger from '../utils/logger.ts';\nimport { Configuration } from './types.ts';\n\n/**\n * Validates the configuration object\n * @param config Configuration object\n * @throws Error if validation fails\n */\nfunction validateConfig(config: any): void {\n // Check required top-level properties\n const requiredProps = ['project', 'directives', 'services', 'ai', 'process'];\n for (const prop of requiredProps) {\n if (!config[prop]) {\n throw new Error(`Missing required configuration property: ${prop}`);\n }\n }\n\n // Check project configuration\n if (!config.project.name) {\n throw new Error('Project name is required');\n }\n if (!config.project.outputDirectory) {\n throw new Error('Project output directory is required');\n }\n\n // Check directives\n if (!Array.isArray(config.directives) || config.directives.length === 0) {\n throw new Error('At least one directive type must be defined');\n }\n\n // Check services\n if (!Array.isArray(config.services) || config.services.length === 0) {\n throw new Error('At least one service must be defined');\n }\n\n // Check AI configuration\n if (!config.ai.provider) {\n throw new Error('AI provider is required');\n }\n if (!config.ai.defaultModel) {\n throw new Error('Default AI model is required');\n }\n\n // Check process flows\n if (!Array.isArray(config.process.flows) || config.process.flows.length === 0) {\n throw new Error('At least one process flow must be defined');\n }\n\n // Validate each flow has phases\n for (const flow of config.process.flows) {\n if (!flow.name) {\n throw new Error('All flows must have a name');\n }\n if (!Array.isArray(flow.phases) || flow.phases.length === 0) {\n throw new Error(`Flow \"${flow.name}\" must have at least one phase`);\n }\n }\n\n logger.debug('Configuration validation passed');\n}\n\n/**\n * Loads and validates the configuration from a YAML file\n * @param configPath Path to the configuration file\n * @returns Validated configuration object\n */\nexport async function loadConfig(configPath: string): Promise<Configuration> {\n logger.info(`Loading configuration from ${configPath}`);\n \n // Check if the file exists\n if (!(await fileExists(configPath))) {\n throw new Error(`Configuration file not found: ${configPath}`);\n }\n \n try {\n // Read the YAML file\n const yamlContent = await readFile(configPath);\n \n // Parse YAML\n const config = yaml.load(yamlContent) as Configuration;\n \n // Validate the configuration\n validateConfig(config);\n \n logger.info('Configuration loaded successfully');\n return config;\n } catch (error) {\n if (error instanceof yaml.YAMLException) {\n logger.error('Invalid YAML format', { error });\n throw new Error(`Invalid YAML format in ${configPath}: ${(error as yaml.YAMLException).message}`);\n } else if (error instanceof Error) {\n logger.error('Failed to load configuration', { error });\n throw error;\n } else {\n // For any other unknown error types\n logger.error('Unknown error loading configuration', { error });\n throw new Error(`Unknown error loading configuration: ${String(error)}`);\n }\n }\n}\n\n/**\n * Creates a default configuration object\n * @returns Default configuration\n */\nexport function createDefaultConfig(): Configuration {\n return {\n project: {\n name: 'Mlld Type System Redesign',\n description: 'Comprehensive redesign of the mlld type system',\n outputDirectory: '_dev/cleanup',\n },\n directives: [\n {\n name: 'variables',\n description: 'Variable reference type system',\n }\n ],\n services: [\n {\n name: 'ParserService',\n description: 'Responsible for parsing source files into AST nodes',\n }\n ],\n ai: {\n provider: 'anthropic',\n defaultModel: 'claude-3-7-sonnet-latest',\n apiKeyEnv: 'ANTHROPIC_API_KEY',\n },\n process: {\n flows: [\n {\n name: 'directive-review',\n description: 'Review and design types for a specific directive',\n phases: [\n {\n name: 'requirements-collection',\n description: 'Collect service requirements for directive',\n processType: 'feedback-collector',\n params: {\n thinkingPromptTemplate: 'templates/requirements-thinking.md',\n responsePromptTemplate: 'templates/requirements-response.md',\n outputDir: '_dev/cleanup/{{directiveName}}/requirements',\n }\n }\n ]\n }\n ]\n }\n };\n} ", "filename": "config/loader.ts" }, { "content": "// Configuration Type Definitions\n\n/**\n * Principle represents a architectural principle to follow\n */\nexport interface Principle {\n name: string;\n description: string;\n}\n\n/**\n * Directive Type represents a type of directive to analyze\n */\nexport interface DirectiveType {\n name: string;\n description: string;\n clarityDoc?: string;\n subtypes?: string[];\n additionalContext?: string;\n}\n\n/**\n * Service represents a service that uses the directive types\n */\nexport interface Service {\n name: string;\n description: string;\n documents?: string[];\n code?: string[];\n [key: string]: any; // Allow for additional properties\n}\n\n/**\n * PhaseConfig represents the configuration for a process phase\n */\nexport interface PhaseConfig {\n name: string;\n description: string;\n processType: 'feedback-collector' | 'architect-processor' | 'human-review' |\n 'service-planning' | 'cross-team-review' | 'pm-processor';\n dependsOn?: string[];\n params: {\n thinkingPromptTemplate?: string;\n responsePromptTemplate?: string;\n promptTemplate?: string;\n outputPath?: string | {\n thinking?: string;\n response?: string;\n [key: string]: string | undefined;\n };\n outputDir?: string;\n [key: string]: any;\n };\n required?: boolean;\n condition?: string; // String representation of a condition function\n}\n\n/**\n * Flow represents a process flow with phases\n */\nexport interface Flow {\n name: string;\n description: string;\n phases: PhaseConfig[];\n}\n\n/**\n * AIConfig represents AI provider configuration\n */\nexport interface AIConfig {\n provider: string;\n defaultModel: string;\n thinkingModel?: string;\n responseModel?: string;\n apiKeyEnv: string;\n maxTokens?: number;\n temperature?: number;\n}\n\n/**\n * OrchestrationConfig represents process orchestration settings\n */\nexport interface OrchestrationConfig {\n parallelDirectives?: boolean;\n parallelServices?: boolean;\n maxConcurrency?: number;\n resumable?: boolean;\n humanReviewHandling?: 'pause-until-review' | 'skip' | 'simulate';\n}\n\n/**\n * The complete configuration object\n */\nexport interface Configuration {\n project: {\n name: string;\n description: string;\n outputDirectory: string;\n principles?: Principle[];\n };\n directives: DirectiveType[];\n services: Service[];\n ai: AIConfig;\n process: {\n flows: Flow[];\n orchestration?: OrchestrationConfig;\n };\n} ", "filename": "config/types.ts" }, { "content": "import { Anthropic } from '@anthropic-ai/sdk';\nimport { logger } from '../utils/logger.js';\nimport * as llmxml from '../utils/llmxml.ts';\nimport path from 'path';\nimport fs from 'fs/promises';\nimport { fileURLToPath } from 'url';\nimport { dirname } from 'path';\nimport { config } from 'dotenv';\nimport { loadEnvVars } from '../utils/env.js';\n\n// Define the message types and roles\nexport type MessageRole = 'user' | 'assistant';\n\nexport interface Message {\n role: MessageRole;\n content: string;\n}\n\nexport interface ModelConfig {\n model?: string;\n temperature?: number;\n maxTokens?: number;\n useXML?: boolean; // New field for XML formatting\n savePrompt?: boolean; // Whether to save the prompt to a file\n dryRun?: boolean; // Whether to skip the actual API call\n apiDryRun?: boolean; // Whether to use compressed prompts and one-sentence responses\n useHaikuModel?: boolean; // Whether to use small, inexpensive models\n outputPath?: string; // Path to save the response (used to determine prompt path)\n}\n\n// Smaller, inexpensive model for haiku mode\nconst HAIKU_MODEL = 'claude-3-haiku-20240307';\n\n/**\n * Claude API client for interacting with Anthropic's Claude models\n */\nexport class ClaudeClient {\n private anthropic: Anthropic | null = null;\n private defaultModel: string;\n private defaultMaxTokens: number;\n private defaultTemperature: number;\n private useXML: boolean;\n private isTestMode: boolean;\n private static instance: ClaudeClient | null = null;\n\n /**\n * Creates a new Claude API client\n */\n private constructor() {\n this.isTestMode = process.env.NODE_ENV === 'test';\n \n // Ensure environment variables are loaded\n loadEnvVars();\n \n const apiKey = process.env.ANTHROPIC_API_KEY;\n \n if (!apiKey && !this.isTestMode) {\n throw new Error('ANTHROPIC_API_KEY environment variable is required');\n }\n\n if (apiKey) {\n this.anthropic = new Anthropic({\n apiKey,\n });\n }\n\n this.defaultModel = process.env.DEFAULT_MODEL || 'claude-3-7-sonnet-latest';\n this.defaultMaxTokens = parseInt(process.env.MAX_TOKENS || '4000', 10);\n this.defaultTemperature = parseFloat(process.env.TEMPERATURE || '0.7');\n this.useXML = process.env.USE_XML === 'true';\n\n logger.debug('Claude client initialized', {\n defaultModel: this.defaultModel,\n defaultMaxTokens: this.defaultMaxTokens,\n defaultTemperature: this.defaultTemperature,\n useXML: this.useXML,\n isTestMode: this.isTestMode\n });\n }\n\n /**\n * Gets the singleton instance of the Claude client\n */\n public static getInstance(): ClaudeClient {\n if (!ClaudeClient.instance) {\n ClaudeClient.instance = new ClaudeClient();\n }\n return ClaudeClient.instance;\n }\n\n /**\n * Save a prompt to a file\n * @param prompt The prompt content to save\n * @param outputPath The output path for the response (used to determine prompt filename)\n */\n private async savePromptToFile(prompt: string, outputPath?: string): Promise<void> {\n if (!outputPath) {\n logger.warn('Cannot save prompt: output path not provided');\n return;\n }\n \n try {\n // Create the prompt file path by changing the extension\n // Example: for \"path/to/response.o.xml\" -> \"path/to/response.sent.xml\"\n const promptPath = outputPath.replace(/\\.o\\.xml$|\\.o\\.md$/, '.sent.xml');\n \n if (promptPath === outputPath) {\n // If no replacement was made, append .sent.xml to the path\n const parsedPath = path.parse(outputPath);\n const promptPathWithExt = path.join(\n parsedPath.dir, \n `${parsedPath.name}.sent.xml`\n );\n await this.saveFileWithDir(promptPathWithExt, prompt);\n } else {\n await this.saveFileWithDir(promptPath, prompt);\n }\n \n logger.info(`Saved prompt to ${promptPath}`);\n } catch (error) {\n logger.error('Failed to save prompt file', { error, outputPath });\n }\n }\n \n /**\n * Save a file, creating directories if they don't exist\n * @param filePath The file path\n * @param content The content to save\n */\n private async saveFileWithDir(filePath: string, content: string): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, content);\n }\n\n /**\n * Creates a compressed version of a prompt for API dry run mode\n * @param content The original prompt content\n * @returns Compressed prompt with only structure and metadata\n */\n private async createCompressedPrompt(content: string): Promise<string> {\n // Get the first 200 characters to show the prompt structure\n const promptPreview = content.substring(0, 200);\n \n // Extract headings to understand the structure\n const headings: string[] = [];\n const headingRegex = /^#+\\s+(.+)$/gm;\n let match;\n while ((match = headingRegex.exec(content)) !== null) {\n headings.push(match[1]);\n }\n \n // Count code blocks and their languages\n const codeBlocks: Record<string, number> = {};\n const codeBlockRegex = /```([a-zA-Z0-9]*)/g;\n while ((match = codeBlockRegex.exec(content)) !== null) {\n const language = match[1] || 'text';\n codeBlocks[language] = (codeBlocks[language] || 0) + 1;\n }\n \n // Extract variables used in the prompt\n const variables: string[] = [];\n const variableRegex = /\\{\\{([^}]+)\\}\\}/g;\n while ((match = variableRegex.exec(content)) !== null) {\n variables.push(match[1]);\n }\n \n // Build a structured compressed prompt\n const compressed = [\n \"# Compressed Prompt for API Dry Run\",\n \"\",\n \"## Prompt Preview\",\n \"```\",\n promptPreview + (content.length > 200 ? \"...\" : \"\"),\n \"```\",\n \"\",\n \"## Prompt Structure\",\n `- Total length: ${content.length} characters`,\n `- Sections: ${headings.length}`,\n headings.length > 0 ? \"- Section headings: \" + headings.join(\", \") : \"\",\n \"\",\n \"## Code Blocks\",\n Object.entries(codeBlocks).length > 0 \n ? Object.entries(codeBlocks).map(([lang, count]) => `- ${lang || \"plain\"}: ${count}`).join(\"\\n\") \n : \"- No code blocks\",\n \"\",\n \"## Variables\",\n variables.length > 0 \n ? variables.map(v => `- ${v}`).join(\"\\n\") \n : \"- No variables\",\n \"\",\n \"## Instructions for Model\",\n \"Please provide a one-sentence response identifying what this prompt appears to be asking for.\",\n ].join(\"\\n\");\n \n return compressed;\n }\n\n /**\n * Completes a conversation with Claude\n * @param messages Array of message objects with role and content\n * @param config Optional model configuration\n * @returns Claude's response text\n */\n async completeMessages(\n messages: Message[],\n config?: ModelConfig\n ): Promise<string> {\n if (this.isTestMode) {\n logger.warn('Claude client is in test mode - real API calls are disabled');\n return \"This is a test mode response. Override 'think' and 'respond' methods for custom test responses.\";\n }\n \n if (!this.anthropic && !config?.dryRun) {\n throw new Error('Claude client is not initialized with API key');\n }\n \n const model = config?.useHaikuModel \n ? HAIKU_MODEL \n : (config?.model || this.defaultModel);\n const maxTokens = config?.maxTokens || this.defaultMaxTokens;\n const temperature = config?.temperature || this.defaultTemperature;\n const useXML = config?.useXML !== undefined ? config?.useXML : this.useXML;\n const dryRun = config?.dryRun || false;\n const apiDryRun = config?.apiDryRun || false;\n const savePrompt = config?.savePrompt || dryRun || apiDryRun; // Always save prompts in dry run modes\n const outputPath = config?.outputPath;\n\n logger.debug('Configuring request to Claude API', {\n model,\n maxTokens,\n temperature,\n useXML,\n dryRun,\n apiDryRun,\n useHaikuModel: config?.useHaikuModel,\n savePrompt,\n messageCount: messages.length,\n });\n\n try {\n // Process messages with LLMXML if enabled and not in API dry run mode\n const processedMessages = useXML && !apiDryRun\n ? await Promise.all(messages.map(async msg => {\n if (msg.role === 'user') {\n const xmlContent = await llmxml.toXML(msg.content);\n \n // Debug log the XML content (only first 500 chars for brevity)\n logger.debug(`XML formatted prompt (first 500 chars): ${xmlContent.substring(0, 500)}...`);\n \n // Write full content to debug file for inspection\n try {\n const debugDir = 'debug';\n await fs.mkdir(debugDir, { recursive: true });\n await fs.writeFile(`${debugDir}/last-claude-prompt.xml`, xmlContent);\n logger.debug('Full XML prompt saved to debug/last-claude-prompt.xml');\n } catch (e) {\n logger.warn('Could not save debug file', e);\n }\n \n return {\n role: msg.role, \n content: xmlContent\n };\n }\n return msg;\n }))\n : messages;\n \n // Handle API dry run mode\n let finalMessages = processedMessages;\n if (apiDryRun) {\n // Create compressed prompt for API dry run\n finalMessages = await Promise.all(messages.map(async msg => {\n if (msg.role === 'user') {\n const compressedPrompt = await this.createCompressedPrompt(msg.content);\n return {\n role: msg.role,\n content: compressedPrompt\n };\n }\n return msg;\n }));\n \n // Set a smaller max tokens for API dry run\n const apiDryRunMaxTokens = 100;\n logger.info(`API dry run: Using compressed prompt and limiting response to ${apiDryRunMaxTokens} tokens`);\n }\n \n // Save prompt if requested\n if (savePrompt && finalMessages.length > 0) {\n await this.savePromptToFile(\n finalMessages[0].content, \n outputPath\n );\n }\n\n // In dry run mode, don't make the API call\n if (dryRun) {\n logger.info('DRY RUN MODE: Skipping API call to Claude');\n return \"DRY RUN MODE: This is a placeholder response. No API call was made.\";\n }\n\n // In haiku mode, log the model being used\n if (config?.useHaikuModel) {\n logger.info(`HAIKU MODE: Using smaller model (${HAIKU_MODEL}) for faster, less expensive results`);\n }\n\n // Initialize Anthropic client if not already initialized\n if (!this.anthropic) {\n if (!process.env.ANTHROPIC_API_KEY) {\n throw new Error('ANTHROPIC_API_KEY environment variable is not set');\n }\n this.anthropic = new Anthropic({\n apiKey: process.env.ANTHROPIC_API_KEY,\n });\n }\n\n // Make the API call with the processed messages using streaming\n const stream = await this.anthropic.messages.create({\n model,\n max_tokens: apiDryRun ? 100 : maxTokens,\n temperature,\n messages: finalMessages.map(msg => ({\n role: msg.role,\n content: msg.content,\n })),\n stream: true,\n });\n\n logger.debug('Started streaming response from Claude API');\n\n // Collect the response chunks\n let responseText = '';\n let chunkCount = 0;\n for await (const chunk of stream) {\n if (chunk.type === 'content_block_delta' && chunk.delta?.type === 'text_delta') {\n const text = chunk.delta.text;\n responseText += text;\n chunkCount++;\n if (chunkCount % 10 === 0) { // Log every 10 chunks to avoid spam\n logger.info('Received API chunk', {\n chunkNumber: chunkCount,\n chunkLength: text.length,\n totalLength: responseText.length,\n chunkPreview: text.substring(0, 50)\n });\n }\n }\n }\n\n logger.info('Completed API response', {\n totalChunks: chunkCount,\n totalLength: responseText.length,\n responsePreview: responseText.substring(0, 200),\n model,\n });\n\n // Debug log the response\n logger.debug(`Response (first 500 chars): ${responseText.substring(0, 500)}...`);\n \n // Write full response to debug file\n try {\n const debugDir = 'debug';\n await fs.mkdir(debugDir, { recursive: true });\n await fs.writeFile(`${debugDir}/last-claude-response.txt`, responseText);\n logger.debug('Full response saved to debug/last-claude-response.txt');\n } catch (e) {\n logger.warn('Could not save debug file', e);\n }\n\n // For API dry run mode, return the response as is (it's already a simplified response)\n if (apiDryRun) {\n return responseText;\n }\n\n // Convert XML back to markdown if needed (and not in API dry run mode)\n if (useXML && !apiDryRun) {\n try {\n logger.debug('Converting XML response to Markdown');\n const markdown = await llmxml.toMarkdown(responseText);\n \n // Save converted markdown for debugging\n try {\n await fs.writeFile('debug/last-claude-response-markdown.md', markdown);\n logger.debug('Converted markdown saved to debug/last-claude-response-markdown.md');\n } catch (e) {\n logger.warn('Could not save debug file', e);\n }\n \n return markdown;\n } catch (error) {\n logger.warn('Failed to convert XML response to Markdown, returning as is', { error });\n return responseText;\n }\n }\n \n return responseText;\n } catch (error) {\n logger.error('Error calling Claude API', { error });\n throw new Error(`Claude API error: ${(error as Error).message}`);\n }\n }\n\n /**\n * Sends a prompt to Claude and returns the response\n * @param prompt The prompt text\n * @param config Optional model configuration\n * @returns Claude's response text\n */\n async completePrompt(prompt: string, config?: ModelConfig): Promise<string> {\n const messages: Message[] = [\n {\n role: 'user',\n content: prompt,\n },\n ];\n\n return this.completeMessages(messages, config);\n }\n\n /**\n * Simplified method for thinking-phase prompts\n * @param prompt The thinking prompt\n * @param config Optional model configuration\n * @returns Claude's thinking response\n */\n async think(prompt: string, config?: ModelConfig): Promise<string> {\n const thinkingModel = process.env.THINKING_MODEL || this.defaultModel;\n return this.completePrompt(prompt, {\n model: thinkingModel,\n ...config,\n });\n }\n\n /**\n * Simplified method for response-phase prompts\n * @param prompt The response prompt\n * @param config Optional model configuration\n * @returns Claude's response\n */\n async respond(prompt: string, config?: ModelConfig): Promise<string> {\n const responseModel = process.env.RESPONSE_MODEL || this.defaultModel;\n return this.completePrompt(prompt, {\n model: responseModel,\n ...config,\n });\n }\n}\n\n// Export a function to get the singleton instance\nexport default function getClaudeClient(): ClaudeClient {\n return ClaudeClient.getInstance();\n} ", "filename": "core/claude.ts" }, { "content": "import yaml from 'js-yaml';\nimport { Task } from './interfaces.js';\nimport { readFile } from '../../utils/fs.js';\nimport logger from '../../utils/logger.js';\n\n/**\n * Extracts frontmatter from a markdown file\n * \n * @param content Markdown content to parse\n * @returns Object with frontmatter metadata or null if not found\n */\nexport function extractFrontmatter(content: string): Record<string, any> | null {\n const frontmatterRegex = /^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/;\n const match = content.match(frontmatterRegex);\n \n if (!match) {\n return null;\n }\n \n try {\n const frontmatterYaml = match[1];\n return yaml.load(frontmatterYaml) as Record<string, any>;\n } catch (error) {\n logger.error('Error parsing frontmatter YAML', { error });\n return null;\n }\n}\n\n/**\n * Extracts the content after frontmatter\n * \n * @param content Markdown content to parse\n * @returns Content without frontmatter\n */\nexport function extractContentWithoutFrontmatter(content: string): string {\n const frontmatterRegex = /^---\\s*\\n[\\s\\S]*?\\n---\\s*\\n/;\n return content.replace(frontmatterRegex, '');\n}\n\n/**\n * Loads a task template file and parses its frontmatter\n * \n * @param filePath Path to the task template file\n * @returns Task metadata and template content\n */\nexport async function loadTaskFromFile(filePath: string): Promise<{ \n task: Task; \n content: string;\n}> {\n try {\n const fileContent = await readFile(filePath);\n const frontmatter = extractFrontmatter(fileContent);\n \n if (!frontmatter) {\n throw new Error(`No frontmatter found in task file: ${filePath}`);\n }\n \n if (!frontmatter.name) {\n throw new Error(`Task name is required in frontmatter: ${filePath}`);\n }\n \n const task: Task = {\n name: frontmatter.name,\n description: frontmatter.description,\n role: frontmatter.role,\n requiredInput: frontmatter.requiredInput,\n requiredOutput: frontmatter.requiredOutput\n };\n \n const content = extractContentWithoutFrontmatter(fileContent);\n \n return { task, content };\n } catch (error) {\n logger.error(`Error loading task from file: ${filePath}`, { error });\n throw error;\n }\n} ", "filename": "core/components/frontmatter.ts" }, { "content": "// Export interfaces\nexport { FileContext } from './interfaces.js';\nexport * from './interfaces.js';\n\n// Export frontmatter loader\nexport * from './frontmatter.js';\n\n// Export component registry\nexport * from './registry.js';\n\n// Export path utilities\nexport * from './path.js';\n\n/**\n * The components module provides the hierarchical component model for Committee.\n * \n * Key concepts:\n * \n * 1. Task - The atomic unit of work, typically involving a template for an LLM call\n * 2. Set - A collection of tasks or other sets, with a defined execution mode\n * 3. Phase - A major stage in a workflow with its own set of related tasks/sets\n * 4. Workflow - A complete process that orchestrates multiple phases\n * \n * The component registry handles path resolution using a specificity cascade:\n * 1. First looks in the current workflow's templates directory\n * 2. If not found, looks in the global templates directory\n * \n * The path generator standardizes output paths using the .o.md convention:\n * {workflow_path}/{phase}/{set}/{task}.o.md # For task outputs\n * {workflow_path}/{phase}/{set}.o.md # For set outputs\n * {workflow_path}/{phase}/{phase}.o.md # For phase outputs\n */ ", "filename": "core/components/index.ts" }, { "content": "/**\n * Core component interfaces for Committee's hierarchical component model\n */\n\n/**\n * The base component interface with common properties\n */\nexport interface Component {\n name: string;\n description?: string;\n requiredInput?: string[];\n requiredOutput?: string[];\n}\n\n/**\n * File collection context for components\n */\nexport interface FileContext {\n [key: string]: string[]; // Collection name -> array of file paths\n}\n\n/**\n * Type for context variables\n */\nexport type Context = Record<string, any>;\n\n/**\n * Model configuration for LLM calls\n */\nexport interface ModelConfig {\n temperature?: number;\n maxTokens?: number;\n topP?: number;\n frequencyPenalty?: number;\n presencePenalty?: number;\n}\n\n/**\n * Task is the atomic unit of work, typically involving a template for an LLM call\n */\nexport interface Task extends Component {\n role?: \"service\" | \"architect\" | \"pm\";\n phase?: string;\n processType?: \"two-phase\" | \"human-review\";\n content?: string;\n context?: FileContext;\n outputPath?: {\n thinking?: string;\n response?: string;\n };\n modelConfig?: ModelConfig;\n inputPath?: string;\n required?: boolean;\n requiredInput?: string[];\n requiredOutput?: string[];\n}\n\n/**\n * Task reference with variables\n */\nexport interface TaskReference {\n description?: string;\n useTask: string;\n variables?: Record<string, string>;\n context?: FileContext; // New context property for file collections\n requiredInput?: string[];\n requiredOutput?: string[];\n}\n\n/**\n * Set reference with variables\n */\nexport interface SetReference {\n description?: string;\n useSet: string;\n variables?: Record<string, string>;\n context?: FileContext; // New context property for file collections\n requiredInput?: string[];\n requiredOutput?: string[];\n}\n\n/**\n * Template instantiation with variables\n */\nexport interface TemplateReference {\n description?: string;\n template: string;\n execution: \"sequential\" | \"parallel\";\n for_each?: string;\n variables?: Record<string, string>;\n context?: FileContext; // New context property for file collections\n requiredInput?: string[];\n requiredOutput?: string[];\n}\n\n/**\n * Set organizes related tasks or other sets with a defined execution mode\n */\nexport interface Set extends Component {\n execution: \"sequential\" | \"parallel\";\n tasks?: TaskReference[];\n set?: (SetReference | TemplateReference)[];\n for_each?: string;\n variables?: Record<string, string>;\n context?: FileContext; // New context property for file collections\n}\n\n/**\n * Phase reference with dependencies\n */\nexport interface PhaseReference {\n usePhase: string;\n dependsOn?: string[];\n humanInputRequired?: string[];\n context?: FileContext; // New context property for file collections\n condition?: string;\n for_each?: string;\n variables?: Record<string, any>;\n}\n\n/**\n * Phase represents a major stage in a workflow\n */\nexport interface Phase extends Component {\n execution: \"sequential\" | \"parallel\";\n humanInputRequired?: string[];\n set: (SetReference | TemplateReference)[];\n dependsOn?: string[];\n context?: FileContext; // New context property for file collections\n condition?: string;\n variables?: Record<string, any>;\n}\n\n/**\n * Workflow is the top-level container that orchestrates multiple phases\n */\nexport interface Workflow extends Component {\n outputPath: string;\n phases: PhaseReference[];\n context?: FileContext;\n variables?: Record<string, any>; // Add support for workflow-level variables\n} ", "filename": "core/components/interfaces.ts" }, { "content": "import path from 'path';\nimport { ensureDir } from '../../utils/fs.js';\nimport { ComponentRegistry, ComponentType } from './index.js';\n\n/**\n * Output path generator for standardizing paths according to the .o.md convention\n */\nexport class PathGenerator {\n private baseOutputPath: string;\n \n constructor(baseOutputPath: string) {\n this.baseOutputPath = baseOutputPath;\n }\n \n /**\n * Generate a task output path\n * \n * @param phaseName Phase name\n * @param phaseIteration Optional phase iteration name\n * @param setName Set name\n * @param setIteration Optional set iteration name\n * @param taskName Task name\n * @param taskIteration Optional task iteration name\n * @returns Standardized task output path\n */\n generateTaskOutputPath(\n phaseName: string,\n phaseIteration: string | undefined,\n setName: string,\n setIteration: string | undefined,\n taskName: string,\n taskIteration?: string\n ): string {\n const parts = [phaseName];\n \n if (phaseIteration) {\n parts.push(phaseIteration);\n }\n \n parts.push(setName);\n \n if (setIteration) {\n parts.push(setIteration);\n }\n \n parts.push(taskName);\n \n if (taskIteration) {\n parts.push(taskIteration);\n }\n \n return path.join(this.baseOutputPath, ...parts, `${taskName}.o.md`);\n }\n \n /**\n * Generate a set output path\n * \n * @param phaseName Phase name\n * @param phaseIteration Optional phase iteration name\n * @param setName Set name\n * @param setIteration Optional set iteration name\n * @returns Standardized set output path\n */\n generateSetOutputPath(\n phaseName: string,\n phaseIteration: string | undefined,\n setName: string,\n setIteration?: string\n ): string {\n const parts = [phaseName];\n \n if (phaseIteration) {\n parts.push(phaseIteration);\n }\n \n parts.push(setName);\n \n if (setIteration) {\n parts.push(setIteration);\n }\n \n return path.join(this.baseOutputPath, ...parts, `${setName}.o.md`);\n }\n \n /**\n * Generate a phase output path\n * \n * @param phaseName Phase name\n * @param phaseIteration Optional phase iteration name\n * @returns Standardized phase output path\n */\n generatePhaseOutputPath(\n phaseName: string,\n phaseIteration?: string\n ): string {\n const parts = [phaseName];\n \n if (phaseIteration) {\n parts.push(phaseIteration);\n }\n \n return path.join(this.baseOutputPath, ...parts, `${phaseName}.o.md`);\n }\n \n /**\n * Generate an output path for an iterated item\n * \n * @param phaseName Phase name\n * @param setName Set name\n * @param itemName Item identifier\n * @param taskName Task name\n * @returns Standardized output path for an iterated item\n */\n generateIteratedTaskOutputPath(\n phaseName: string, \n setName: string, \n itemName: string, \n taskName: string\n ): string {\n return pat