diff --git a/.github/workflows/weekly-research.lock.yml b/.github/workflows/weekly-research.lock.yml index fe86953f89..02a9aeb93f 100644 --- a/.github/workflows/weekly-research.lock.yml +++ b/.github/workflows/weekly-research.lock.yml @@ -16,7 +16,45 @@ concurrency: run-name: "Weekly Research" jobs: + task: + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + text: ${{ steps.compute-text.outputs.text }} + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: .github + fetch-depth: 1 + - name: Compute current body text + id: compute-text + uses: ./.github/actions/compute-text + + add-reaction: + needs: task + if: github.event_name == 'issues' || github.event_name == 'pull_request' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_comment' || github.event_name == 'pull_request_review_comment' + runs-on: ubuntu-latest + permissions: + contents: write # Read .github + issues: write + pull-requests: write + outputs: + reaction_id: ${{ steps.react.outputs.reaction-id }} + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: .github + - name: Add eyes reaction to the triggering item + id: react + uses: ./.github/actions/reaction + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + mode: add + reaction: eyes + weekly-research: + needs: task runs-on: ubuntu-latest permissions: actions: read diff --git a/README.md b/README.md index e94f614a28..87243f43f0 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ You will see the changes reflected in the `.lock.yml` file, which is the actual By default Claude Code is used as the agentic processor. You can configure the agentic processor by editing the frontmatter of the markdown workflow files. ```markdown -engine: codex # Optional: specify AI engine (claude, codex, ai-inference, gemini) +engine: codex # Optional: specify AI engine (claude, codex, gemini) ``` You can also specify this on the command line when adding or running workflows: diff --git a/docs/frontmatter.md b/docs/frontmatter.md index 7a6d8ac77b..6eadac84a8 100644 --- a/docs/frontmatter.md +++ b/docs/frontmatter.md @@ -147,14 +147,13 @@ Specifies which AI engine to use. Defaults to `claude`. ### Simple String Format ```yaml -engine: claude # or codex, opencode, ai-inference +engine: claude # or codex, gemini ``` **Available engines:** - `claude` (default): Claude Code with full MCP tool support and allow-listing (see [MCP Guide](mcps.md)) - `codex` (**experimental**): Codex with OpenAI endpoints -- `opencode` (**experimental**): OpenCode AI coding assistant -- `ai-inference`: GitHub Models via actions/ai-inference with GitHub MCP support (see [MCP Guide](mcps.md)) +- `gemini`: Google Gemini AI models ### Extended Object Format @@ -166,7 +165,7 @@ engine: ``` **Fields:** -- **`id`** (required): Engine identifier (`claude`, `codex`, `opencode`, `ai-inference`) +- **`id`** (required): Engine identifier (`claude`, `codex`, `gemini`) - **`version`** (optional): Action version (`beta`, `stable`) - **`model`** (optional): Specific LLM model diff --git a/pkg/cli/templates/instructions.md b/pkg/cli/templates/instructions.md index 6c5834479c..19c44ba150 100644 --- a/pkg/cli/templates/instructions.md +++ b/pkg/cli/templates/instructions.md @@ -60,11 +60,11 @@ The YAML frontmatter supports these fields: ### Agentic Workflow Specific Fields - **`engine:`** - AI processor configuration - - String format: `"claude"` (default), `"codex"`, `"ai-inference"`, `"gemini"` + - String format: `"claude"` (default), `"codex"`, `"gemini"` - Object format for extended configuration: ```yaml engine: - id: claude # Required: agent CLI identifier (claude, codex, ai-inference, gemini) + id: claude # Required: agent CLI identifier (claude, codex, gemini) version: beta # Optional: version of the action model: claude-3-5-sonnet-20241022 # Optional: LLM model to use ``` @@ -412,7 +412,7 @@ The workflow frontmatter is validated against JSON Schema during compilation. Co - **Invalid field names** - Only fields in the schema are allowed - **Wrong field types** - e.g., `timeout_minutes` must be integer -- **Invalid enum values** - e.g., `engine` must be "claude", "codex", "ai-inference", or "gemini" +- **Invalid enum values** - e.g., `engine` must be "claude", "codex", or "gemini" - **Missing required fields** - Some triggers require specific configuration Use `gh aw compile --verbose` to see detailed validation messages. \ No newline at end of file diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 4561d9e25b..4a459fbbed 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -254,8 +254,8 @@ "oneOf": [ { "type": "string", - "enum": ["claude", "codex", "opencode", "ai-inference", "gemini", "genaiscript"], - "description": "Simple engine name (claude, codex, opencode, ai-inference, gemini, or genaiscript)" + "enum": ["claude", "codex", "gemini"], + "description": "Simple engine name (claude, codex, or gemini)" }, { "type": "object", @@ -263,8 +263,8 @@ "properties": { "id": { "type": "string", - "enum": ["claude", "codex", "opencode", "ai-inference", "gemini", "genaiscript"], - "description": "Agent CLI identifier (claude, codex, opencode, ai-inference, gemini, or genaiscript)" + "enum": ["claude", "codex", "gemini"], + "description": "Agent CLI identifier (claude, codex, or gemini)" }, "version": { "type": "string", diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index 07c476b3a7..5021cc3eaa 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -97,9 +97,6 @@ func NewEngineRegistry() *EngineRegistry { registry.Register(NewClaudeEngine()) registry.Register(NewCodexEngine()) registry.Register(NewGeminiEngine()) - registry.Register(NewOpenCodeEngine()) - registry.Register(NewAIInferenceEngine()) - registry.Register(NewGenAIScriptEngine()) return registry } diff --git a/pkg/workflow/agentic_engine_test.go b/pkg/workflow/agentic_engine_test.go index 61677681be..6a115d98b7 100644 --- a/pkg/workflow/agentic_engine_test.go +++ b/pkg/workflow/agentic_engine_test.go @@ -9,8 +9,8 @@ func TestEngineRegistry(t *testing.T) { // Test that built-in engines are registered supportedEngines := registry.GetSupportedEngines() - if len(supportedEngines) != 6 { - t.Errorf("Expected 6 supported engines, got %d", len(supportedEngines)) + if len(supportedEngines) != 3 { + t.Errorf("Expected 3 supported engines, got %d", len(supportedEngines)) } // Test getting engines by ID @@ -30,22 +30,6 @@ func TestEngineRegistry(t *testing.T) { t.Errorf("Expected codex engine ID, got '%s'", codexEngine.GetID()) } - opencodeEngine, err := registry.GetEngine("opencode") - if err != nil { - t.Errorf("Expected to find opencode engine, got error: %v", err) - } - if opencodeEngine.GetID() != "opencode" { - t.Errorf("Expected opencode engine ID, got '%s'", opencodeEngine.GetID()) - } - - aiInferenceEngine, err := registry.GetEngine("ai-inference") - if err != nil { - t.Errorf("Expected to find ai-inference engine, got error: %v", err) - } - if aiInferenceEngine.GetID() != "ai-inference" { - t.Errorf("Expected ai-inference engine ID, got '%s'", aiInferenceEngine.GetID()) - } - geminiEngine, err := registry.GetEngine("gemini") if err != nil { t.Errorf("Expected to find gemini engine, got error: %v", err) @@ -69,10 +53,6 @@ func TestEngineRegistry(t *testing.T) { t.Error("Expected codex to be valid engine") } - if !registry.IsValidEngine("opencode") { - t.Error("Expected opencode to be valid engine") - } - if !registry.IsValidEngine("gemini") { t.Error("Expected gemini to be valid engine") } @@ -136,7 +116,7 @@ func TestEngineRegistryCustomEngine(t *testing.T) { // Test that supported engines list is updated supportedEngines := registry.GetSupportedEngines() - if len(supportedEngines) != 7 { - t.Errorf("Expected 7 supported engines after adding custom, got %d", len(supportedEngines)) + if len(supportedEngines) != 4 { + t.Errorf("Expected 4 supported engines after adding custom, got %d", len(supportedEngines)) } } diff --git a/pkg/workflow/ai_inference_engine.go b/pkg/workflow/ai_inference_engine.go deleted file mode 100644 index 06b4d12340..0000000000 --- a/pkg/workflow/ai_inference_engine.go +++ /dev/null @@ -1,136 +0,0 @@ -package workflow - -import ( - "fmt" - "strings" -) - -// AIInferenceEngine represents the AI Inference agentic engine using GitHub Models -type AIInferenceEngine struct { - BaseEngine -} - -func NewAIInferenceEngine() *AIInferenceEngine { - return &AIInferenceEngine{ - BaseEngine: BaseEngine{ - id: "ai-inference", - displayName: "AI Inference", - description: "Uses GitHub Models via actions/ai-inference with GitHub MCP support", - experimental: false, - supportsToolsWhitelist: true, - }, - } -} - -func (e *AIInferenceEngine) GetInstallationSteps(engineConfig *EngineConfig) []GitHubActionStep { - // ai-inference doesn't require installation as it's a GitHub Action - return []GitHubActionStep{} -} - -func (e *AIInferenceEngine) GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig) ExecutionConfig { - config := ExecutionConfig{ - StepName: "Execute AI Inference Action", - Action: "actions/ai-inference@v1", - Inputs: map[string]string{ - "prompt-file": "/tmp/aw-prompts/prompt.txt", - "token": "${{ secrets.GITHUB_TOKEN }}", - "mcp-config": "/tmp/mcp-config/mcp-servers.json", - "max-tokens": "2000", // Increased default for workflow responses - }, - Environment: map[string]string{ - "GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}", - }, - } - - // Add model configuration if specified - if engineConfig != nil && engineConfig.Model != "" { - config.Inputs["model"] = engineConfig.Model - } else { - // Use default model from ai-inference action - config.Inputs["model"] = "openai/gpt-4o" - } - - return config -} - -func (e *AIInferenceEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]any, mcpTools []string) { - yaml.WriteString(" cat > /tmp/mcp-config/mcp-servers.json << 'EOF'\n") - yaml.WriteString(" {\n") - yaml.WriteString(" \"mcpServers\": {\n") - - // Generate configuration for each MCP tool - for i, toolName := range mcpTools { - isLast := i == len(mcpTools)-1 - - switch toolName { - case "github": - githubTool := tools["github"] - e.renderGitHubAIInferenceMCPConfig(yaml, githubTool, isLast) - default: - // Handle custom MCP tools (those with MCP-compatible type) - if toolConfig, ok := tools[toolName].(map[string]any); ok { - if hasMcp, _ := hasMCPConfig(toolConfig); hasMcp { - if err := e.renderAIInferenceMCPConfig(yaml, toolName, toolConfig, isLast); err != nil { - fmt.Printf("Error generating custom MCP configuration for %s: %v\n", toolName, err) - } - } - } - } - } - - yaml.WriteString(" }\n") - yaml.WriteString(" }\n") - yaml.WriteString(" EOF\n") -} - -// renderGitHubAIInferenceMCPConfig generates the GitHub MCP server configuration -// Uses Docker MCP as the default for AI Inference -func (e *AIInferenceEngine) renderGitHubAIInferenceMCPConfig(yaml *strings.Builder, githubTool any, isLast bool) { - githubDockerImageVersion := getGitHubDockerImageVersion(githubTool) - - yaml.WriteString(" \"github\": {\n") - - // Always use Docker-based GitHub MCP server (services mode has been removed) - yaml.WriteString(" \"command\": \"docker\",\n") - yaml.WriteString(" \"args\": [\n") - yaml.WriteString(" \"run\",\n") - yaml.WriteString(" \"-i\",\n") - yaml.WriteString(" \"--rm\",\n") - yaml.WriteString(" \"-e\",\n") - yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\",\n") - yaml.WriteString(" \"ghcr.io/github/github-mcp-server:" + githubDockerImageVersion + "\"\n") - yaml.WriteString(" ],\n") - yaml.WriteString(" \"env\": {\n") - yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"${{ secrets.GITHUB_TOKEN }}\"\n") - yaml.WriteString(" }\n") - - if isLast { - yaml.WriteString(" }\n") - } else { - yaml.WriteString(" },\n") - } -} - -// renderAIInferenceMCPConfig generates custom MCP server configuration for a single tool in AI Inference workflow mcp-servers.json -func (e *AIInferenceEngine) renderAIInferenceMCPConfig(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { - yaml.WriteString(fmt.Sprintf(" \"%s\": {\n", toolName)) - - // Use the shared MCP config renderer with JSON format - renderer := MCPConfigRenderer{ - IndentLevel: " ", - Format: "json", - } - - err := renderSharedMCPConfig(yaml, toolName, toolConfig, isLast, renderer) - if err != nil { - return err - } - - if isLast { - yaml.WriteString(" }\n") - } else { - yaml.WriteString(" },\n") - } - - return nil -} diff --git a/pkg/workflow/ai_inference_engine_test.go b/pkg/workflow/ai_inference_engine_test.go deleted file mode 100644 index 7fcc376054..0000000000 --- a/pkg/workflow/ai_inference_engine_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package workflow - -import ( - "strings" - "testing" -) - -func TestAIInferenceEngine(t *testing.T) { - engine := NewAIInferenceEngine() - - // Test basic engine properties - if engine.GetID() != "ai-inference" { - t.Errorf("Expected ID 'ai-inference', got '%s'", engine.GetID()) - } - - if engine.GetDisplayName() != "AI Inference" { - t.Errorf("Expected display name 'AI Inference', got '%s'", engine.GetDisplayName()) - } - - if engine.IsExperimental() { - t.Error("Expected AI Inference engine to be stable (not experimental)") - } - - if !engine.SupportsToolsWhitelist() { - t.Error("Expected AI Inference engine to support tools whitelist") - } - - // Test installation steps (should be empty) - steps := engine.GetInstallationSteps(nil) - if len(steps) != 0 { - t.Errorf("Expected no installation steps, got %d", len(steps)) - } -} - -func TestAIInferenceExecutionConfig(t *testing.T) { - engine := NewAIInferenceEngine() - - // Test default execution config - config := engine.GetExecutionConfig("test-workflow", "test.log", nil) - - if config.StepName != "Execute AI Inference Action" { - t.Errorf("Expected step name 'Execute AI Inference Action', got '%s'", config.StepName) - } - - if config.Action != "actions/ai-inference@v1" { - t.Errorf("Expected action 'actions/ai-inference@v1', got '%s'", config.Action) - } - - // Check required inputs - expectedInputs := map[string]string{ - "prompt-file": "/tmp/aw-prompts/prompt.txt", - "token": "${{ secrets.GITHUB_TOKEN }}", - "mcp-config": "/tmp/mcp-config/mcp-servers.json", - "model": "openai/gpt-4o", - "max-tokens": "2000", - } - - for key, expectedValue := range expectedInputs { - if actualValue, exists := config.Inputs[key]; !exists { - t.Errorf("Expected input '%s' to be present", key) - } else if actualValue != expectedValue { - t.Errorf("Expected input '%s' to be '%s', got '%s'", key, expectedValue, actualValue) - } - } - - // Test with custom model configuration - engineConfig := &EngineConfig{ - ID: "ai-inference", - Model: "anthropic/claude-3.5-sonnet", - } - - configWithModel := engine.GetExecutionConfig("test-workflow", "test.log", engineConfig) - if configWithModel.Inputs["model"] != "anthropic/claude-3.5-sonnet" { - t.Errorf("Expected custom model 'anthropic/claude-3.5-sonnet', got '%s'", configWithModel.Inputs["model"]) - } -} - -func TestAIInferenceMCPConfig(t *testing.T) { - engine := NewAIInferenceEngine() - yaml := &strings.Builder{} - - // Test with GitHub tools only - tools := map[string]any{ - "github": map[string]any{ - "allowed": []string{"get_issue", "add_issue_comment"}, - }, - } - mcpTools := []string{"github"} - - engine.RenderMCPConfig(yaml, tools, mcpTools) - - output := yaml.String() - if !strings.Contains(output, "mcp-servers.json") { - t.Error("Expected MCP servers configuration file generation") - } - if !strings.Contains(output, "\"github\": {") { - t.Error("Expected GitHub MCP server configuration") - } - if !strings.Contains(output, "ghcr.io/github/github-mcp-server:") { - t.Error("Expected dockerized GitHub MCP server") - } - - // Test with custom MCP tools - yaml.Reset() - toolsWithCustom := map[string]any{ - "github": map[string]any{ - "allowed": []string{"get_issue"}, - }, - "custom": map[string]any{ - "mcp": map[string]any{ - "type": "stdio", - "command": "custom-mcp-server", - }, - }, - } - mcpToolsCustom := []string{"github", "custom"} - - engine.RenderMCPConfig(yaml, toolsWithCustom, mcpToolsCustom) - - outputWithCustom := yaml.String() - if !strings.Contains(outputWithCustom, "\"custom\": {") { - t.Error("Expected custom MCP tool configuration") - } - if !strings.Contains(outputWithCustom, "custom-mcp-server") { - t.Error("Expected custom MCP server command") - } -} - -func TestAIInferenceEngineRegistry(t *testing.T) { - registry := NewEngineRegistry() - - // Test that AI Inference engine is registered - engine, err := registry.GetEngine("ai-inference") - if err != nil { - t.Errorf("Expected to find ai-inference engine, got error: %v", err) - } - - if engine.GetID() != "ai-inference" { - t.Errorf("Expected ai-inference engine ID, got '%s'", engine.GetID()) - } - - // Test that it's included in supported engines - supportedEngines := registry.GetSupportedEngines() - found := false - for _, id := range supportedEngines { - if id == "ai-inference" { - found = true - break - } - } - - if !found { - t.Error("Expected ai-inference to be in supported engines list") - } - - // Test engine validation - if !registry.IsValidEngine("ai-inference") { - t.Error("Expected ai-inference to be a valid engine") - } -} diff --git a/pkg/workflow/engine_config_test.go b/pkg/workflow/engine_config_test.go index 363dedc6f9..b745470631 100644 --- a/pkg/workflow/engine_config_test.go +++ b/pkg/workflow/engine_config_test.go @@ -247,43 +247,6 @@ func TestEngineConfigurationWithModel(t *testing.T) { expectedModel string expectedAPIKey string }{ - { - name: "OpenCode with Claude model", - engine: NewOpenCodeEngine(), - engineConfig: &EngineConfig{ - ID: "opencode", - Model: "anthropic/claude-sonnet-4-20250514", - }, - expectedModel: "--model anthropic/claude-sonnet-4-20250514", - expectedAPIKey: "ANTHROPIC_API_KEY", - }, - { - name: "OpenCode with legacy Claude model format", - engine: NewOpenCodeEngine(), - engineConfig: &EngineConfig{ - ID: "opencode", - Model: "claude-3-5-sonnet-20241022", - }, - expectedModel: "--model claude-3-5-sonnet-20241022", - expectedAPIKey: "ANTHROPIC_API_KEY", - }, - { - name: "OpenCode with GPT model", - engine: NewOpenCodeEngine(), - engineConfig: &EngineConfig{ - ID: "opencode", - Model: "gpt-4o", - }, - expectedModel: "--model gpt-4o", - expectedAPIKey: "OPENCODE_API_KEY", - }, - { - name: "OpenCode without model", - engine: NewOpenCodeEngine(), - engineConfig: &EngineConfig{ID: "opencode"}, - expectedModel: "", - expectedAPIKey: "OPENCODE_API_KEY", - }, { name: "Claude with model", engine: NewClaudeEngine(), @@ -311,18 +274,6 @@ func TestEngineConfigurationWithModel(t *testing.T) { config := tt.engine.GetExecutionConfig("test-workflow", "test-log", tt.engineConfig) switch tt.engine.GetID() { - case "opencode": - if tt.expectedModel != "" { - if !strings.Contains(config.Command, tt.expectedModel) { - t.Errorf("Expected command to contain model %s, got: %s", tt.expectedModel, config.Command) - } - } - - expectedSecret := "${{ secrets." + tt.expectedAPIKey + " }}" - if config.Environment[tt.expectedAPIKey] != expectedSecret { - t.Errorf("Expected environment %s to be %s, got: %s", tt.expectedAPIKey, expectedSecret, config.Environment[tt.expectedAPIKey]) - } - case "claude": if tt.expectedModel != "" { if config.Inputs["model"] != tt.expectedModel { @@ -346,7 +297,7 @@ func TestNilEngineConfig(t *testing.T) { engines := []AgenticEngine{ NewClaudeEngine(), NewCodexEngine(), - NewOpenCodeEngine(), + NewGeminiEngine(), } for _, engine := range engines { diff --git a/pkg/workflow/genaiscript_engine.go b/pkg/workflow/genaiscript_engine.go deleted file mode 100644 index eb567da42d..0000000000 --- a/pkg/workflow/genaiscript_engine.go +++ /dev/null @@ -1,148 +0,0 @@ -package workflow - -import ( - "fmt" - "strings" -) - -// GenAIScriptEngine represents the GenAIScript agentic engine (experimental) -type GenAIScriptEngine struct { - BaseEngine -} - -func NewGenAIScriptEngine() *GenAIScriptEngine { - return &GenAIScriptEngine{ - BaseEngine: BaseEngine{ - id: "genaiscript", - displayName: "GenAIScript", - description: "Uses GenAIScript with markdown scripts and MCP support (experimental)", - experimental: true, - supportsToolsWhitelist: true, - }, - } -} - -func (e *GenAIScriptEngine) GetInstallationSteps(engineConfig *EngineConfig) []GitHubActionStep { - // Build the npm install command, optionally with version - installCmd := "npm install -g genaiscript" - if engineConfig != nil && engineConfig.Version != "" { - installCmd = fmt.Sprintf("npm install -g genaiscript@%s", engineConfig.Version) - } - - return []GitHubActionStep{ - { - " - name: Setup Node.js", - " uses: actions/setup-node@v4", - " with:", - " node-version: '24'", - }, - { - " - name: Install GenAIScript", - fmt.Sprintf(" run: %s", installCmd), - }, - } -} - -func (e *GenAIScriptEngine) GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig) ExecutionConfig { - // Build the genaiscript command - // Based on comment: genaiscript run prompt.md --mcps ./mcpservers.json --out-output $GITHUB_STEP_SUMMARY - command := fmt.Sprintf(`# Create log directory outside git repo -mkdir -p /tmp/aw-logs - -# Run GenAIScript with MCP config and log capture -genaiscript run /tmp/aw-prompts/prompt.txt \ - --mcps /tmp/mcp-config/mcp-servers.json \ - --out-output $GITHUB_STEP_SUMMARY 2>&1 | tee /tmp/aw-logs/%s.log`, logFile) - - config := ExecutionConfig{ - StepName: "Run GenAIScript", - Command: command, - Environment: map[string]string{ - "GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}", - }, - } - - // Add model configuration if specified - if engineConfig != nil && engineConfig.Model != "" { - // GenAIScript supports model specification via environment or CLI args - config.Environment["GENAISCRIPT_MODEL"] = engineConfig.Model - } - - return config -} - -func (e *GenAIScriptEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]any, mcpTools []string) { - // GenAIScript uses Claude-compatible MCP configuration format - // Generate mcp-servers.json in the same format as Claude engine - - yaml.WriteString(" # Create MCP configuration directory\n") - yaml.WriteString(" mkdir -p /tmp/mcp-config\n") - yaml.WriteString(" \n") - yaml.WriteString(" # Generate MCP servers configuration for GenAIScript\n") - yaml.WriteString(" cat > /tmp/mcp-config/mcp-servers.json << 'EOF'\n") - yaml.WriteString(" {\n") - yaml.WriteString(" \"mcpServers\": {\n") - - // Process tools and generate MCP configuration - mcpServerCount := 0 - - for i, toolName := range mcpTools { - if toolConfig, ok := tools[toolName].(map[string]any); ok { - if toolName == "github" { - e.renderGitHubGenAIScriptMCPConfig(yaml, toolConfig, i == len(mcpTools)-1) - mcpServerCount++ - } else { - // Handle custom MCP tools - if hasMcp, _ := hasMCPConfig(toolConfig); hasMcp { - if err := e.renderGenAIScriptMCPConfig(yaml, toolName, toolConfig); err == nil { - if i < len(mcpTools)-1 { - yaml.WriteString(",\n") - } - mcpServerCount++ - } - } - } - } - } - - yaml.WriteString("\n }\n") - yaml.WriteString(" }\n") - yaml.WriteString(" EOF\n") - yaml.WriteString(" \n") -} - -// renderGitHubGenAIScriptMCPConfig generates GitHub MCP server configuration for GenAIScript -// Uses the same format as Claude since GenAIScript supports Claude MCP config format -func (e *GenAIScriptEngine) renderGitHubGenAIScriptMCPConfig(yaml *strings.Builder, githubTool any, isLast bool) { - yaml.WriteString(" \"github\": {\n") - yaml.WriteString(" \"command\": \"docker\",\n") - yaml.WriteString(" \"args\": [\n") - yaml.WriteString(" \"run\", \"--rm\",\n") - yaml.WriteString(" \"-e\", \"GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}\",\n") - yaml.WriteString(" \"ghcr.io/modelcontextprotocol/servers/github:latest\"\n") - yaml.WriteString(" ]\n") - yaml.WriteString(" }") - - if !isLast { - yaml.WriteString(",") - } - yaml.WriteString("\n") -} - -// renderGenAIScriptMCPConfig generates custom MCP server configuration for a single tool in GenAIScript workflow -func (e *GenAIScriptEngine) renderGenAIScriptMCPConfig(yaml *strings.Builder, toolName string, toolConfig map[string]any) error { - yaml.WriteString(fmt.Sprintf(" \"%s\": {\n", toolName)) - - // Use the shared MCP config renderer with JSON format - renderer := MCPConfigRenderer{ - IndentLevel: " ", - Format: "json", - } - - if err := renderSharedMCPConfig(yaml, toolName, toolConfig, true, renderer); err != nil { - return err - } - - yaml.WriteString(" }") - return nil -} diff --git a/pkg/workflow/genaiscript_engine_test.go b/pkg/workflow/genaiscript_engine_test.go deleted file mode 100644 index d4d87390c5..0000000000 --- a/pkg/workflow/genaiscript_engine_test.go +++ /dev/null @@ -1,287 +0,0 @@ -package workflow - -import ( - "os" - "strings" - "testing" -) - -func TestGenAIScriptEngine(t *testing.T) { - engine := NewGenAIScriptEngine() - - // Test basic properties - if engine.GetID() != "genaiscript" { - t.Errorf("Expected engine ID 'genaiscript', got '%s'", engine.GetID()) - } - - if engine.GetDisplayName() != "GenAIScript" { - t.Errorf("Expected display name 'GenAIScript', got '%s'", engine.GetDisplayName()) - } - - if !engine.IsExperimental() { - t.Error("Expected GenAIScript engine to be experimental") - } - - if !engine.SupportsToolsWhitelist() { - t.Error("Expected GenAIScript engine to support tools whitelist") - } - - // Test installation steps - steps := engine.GetInstallationSteps(nil) - if len(steps) != 2 { - t.Errorf("Expected 2 installation steps, got %d", len(steps)) - } - - // Verify Node.js setup step - nodeSetupFound := false - genaiscriptInstallFound := false - for _, step := range steps { - stepContent := strings.Join(step, "\n") - if strings.Contains(stepContent, "Setup Node.js") && strings.Contains(stepContent, "actions/setup-node@v4") { - nodeSetupFound = true - } - if strings.Contains(stepContent, "Install GenAIScript") && strings.Contains(stepContent, "npm install -g genaiscript") { - genaiscriptInstallFound = true - } - } - - if !nodeSetupFound { - t.Error("Expected Node.js setup step") - } - if !genaiscriptInstallFound { - t.Error("Expected GenAIScript installation step") - } - - // Test execution config - config := engine.GetExecutionConfig("test-workflow", "test.log", nil) - if config.StepName != "Run GenAIScript" { - t.Errorf("Expected step name 'Run GenAIScript', got '%s'", config.StepName) - } - - if config.Action != "" { - t.Error("Expected empty action for CLI-based engine") - } - - if !strings.Contains(config.Command, "genaiscript run") { - t.Error("Expected command to contain 'genaiscript run'") - } - - if !strings.Contains(config.Command, "/tmp/aw-prompts/prompt.txt") { - t.Error("Expected command to use prompt file") - } - - if !strings.Contains(config.Command, "--mcps /tmp/mcp-config/mcp-servers.json") { - t.Error("Expected command to use MCP config") - } - - if !strings.Contains(config.Command, "--out-output $GITHUB_STEP_SUMMARY") { - t.Error("Expected command to output to GITHUB_STEP_SUMMARY") - } - - // Test with model configuration - engineConfig := &EngineConfig{Model: "gpt-4"} - configWithModel := engine.GetExecutionConfig("test-workflow", "test.log", engineConfig) - if configWithModel.Environment["GENAISCRIPT_MODEL"] != "gpt-4" { - t.Error("Expected GENAISCRIPT_MODEL environment variable to be set") - } -} - -func TestGenAIScriptEngineWithVersion(t *testing.T) { - engine := NewGenAIScriptEngine() - - // Test installation steps without version - stepsNoVersion := engine.GetInstallationSteps(nil) - foundNoVersionInstall := false - for _, step := range stepsNoVersion { - for _, line := range step { - if strings.Contains(line, "npm install -g genaiscript") && !strings.Contains(line, "@") { - foundNoVersionInstall = true - break - } - } - } - if !foundNoVersionInstall { - t.Error("Expected default npm install command without version") - } - - // Test installation steps with version - engineConfig := &EngineConfig{ - ID: "genaiscript", - Version: "1.2.3", - } - stepsWithVersion := engine.GetInstallationSteps(engineConfig) - foundVersionInstall := false - for _, step := range stepsWithVersion { - for _, line := range step { - if strings.Contains(line, "npm install -g genaiscript@1.2.3") { - foundVersionInstall = true - break - } - } - } - if !foundVersionInstall { - t.Error("Expected versioned npm install command with genaiscript@1.2.3") - } -} - -func TestGenAIScriptMCPConfigGeneration(t *testing.T) { - // Create temporary directory for test files - tmpDir, err := os.MkdirTemp("", "genaiscript-mcp-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - compiler := NewCompiler(false, "", "test") - - tests := []struct { - name string - frontmatter string - expectedAI string - expectMcpServersJson bool - }{ - { - name: "genaiscript with github tools generates mcp-servers.json", - frontmatter: `--- -engine: genaiscript -tools: - github: - allowed: [get_issue, create_issue] ----`, - expectedAI: "genaiscript", - expectMcpServersJson: true, - }, - { - name: "genaiscript with custom MCP tool", - frontmatter: `--- -engine: genaiscript -tools: - github: - allowed: [get_issue] - custom-tool: - allowed: [custom_function] - mcp: - type: stdio - command: "node" - args: ["custom-server.js"] ----`, - expectedAI: "genaiscript", - expectMcpServersJson: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create test markdown file - mdPath := tmpDir + "/test-workflow.md" - mdContent := tt.frontmatter + "\n\n# Test Workflow\n\nTest content" - - if err := os.WriteFile(mdPath, []byte(mdContent), 0644); err != nil { - t.Fatal(err) - } - - // Compile workflow - if err := compiler.CompileWorkflow(mdPath); err != nil { - t.Fatalf("Compilation failed: %v", err) - } - - // Read generated lock file - lockPath := strings.TrimSuffix(mdPath, ".md") + ".lock.yml" - lockContent, err := os.ReadFile(lockPath) - if err != nil { - t.Fatalf("Failed to read lock file: %v", err) - } - lockContentStr := string(lockContent) - - // Test MCP servers configuration - if tt.expectMcpServersJson { - if !strings.Contains(lockContentStr, "mcp-servers.json") { - t.Errorf("Expected mcp-servers.json generation but didn't find it in:\n%s", lockContentStr) - } - if !strings.Contains(lockContentStr, "mcpServers") { - t.Errorf("Expected mcpServers section but didn't find it in:\n%s", lockContentStr) - } - } - - // Verify AI type - if tt.expectedAI == "genaiscript" { - if !strings.Contains(lockContentStr, "genaiscript run") { - t.Errorf("Expected genaiscript run command but didn't find it in:\n%s", lockContentStr) - } - if !strings.Contains(lockContentStr, "npm install -g genaiscript") { - t.Errorf("Expected GenAIScript installation but didn't find it in:\n%s", lockContentStr) - } - } - }) - } -} - -func TestGenAIScriptCustomMCPConfig(t *testing.T) { - engine := NewGenAIScriptEngine() - var yaml strings.Builder - - // Test with custom MCP tool - tools := map[string]any{ - "github": map[string]any{ - "allowed": []string{"get_issue"}, - }, - "custom-tool": map[string]any{ - "allowed": []string{"custom_function"}, - "mcp": map[string]any{ - "type": "stdio", - "command": "node", - "args": []string{"custom-server.js"}, - }, - }, - } - mcpTools := []string{"github", "custom-tool"} - - engine.RenderMCPConfig(&yaml, tools, mcpTools) - result := yaml.String() - - // Check that MCP configuration is generated - if !strings.Contains(result, "mcp-servers.json") { - t.Error("Expected mcp-servers.json configuration") - } - - if !strings.Contains(result, "mcpServers") { - t.Error("Expected mcpServers section") - } - - if !strings.Contains(result, "github") { - t.Error("Expected github MCP server configuration") - } - - if !strings.Contains(result, "custom-tool") { - t.Error("Expected custom-tool MCP server configuration") - } -} - -func TestGenAIScriptHTTPMCPConfig(t *testing.T) { - engine := NewGenAIScriptEngine() - var yaml strings.Builder - - // Test with HTTP-based MCP tool - tools := map[string]any{ - "http-tool": map[string]any{ - "allowed": []string{"http_function"}, - "mcp": map[string]any{ - "type": "http", - "url": "http://localhost:3000/mcp", - }, - }, - } - mcpTools := []string{"http-tool"} - - engine.RenderMCPConfig(&yaml, tools, mcpTools) - result := yaml.String() - - // Check that HTTP MCP configuration is generated - if !strings.Contains(result, "http-tool") { - t.Error("Expected http-tool MCP server configuration") - } - - if !strings.Contains(result, "http://localhost:3000/mcp") { - t.Error("Expected HTTP URL in configuration") - } -} diff --git a/pkg/workflow/opencode_engine.go b/pkg/workflow/opencode_engine.go deleted file mode 100644 index 3eeb4c4aef..0000000000 --- a/pkg/workflow/opencode_engine.go +++ /dev/null @@ -1,159 +0,0 @@ -package workflow - -import ( - "fmt" - "strings" -) - -// OpenCodeEngine represents the OpenCode agentic engine (experimental) -type OpenCodeEngine struct { - BaseEngine -} - -func NewOpenCodeEngine() *OpenCodeEngine { - return &OpenCodeEngine{ - BaseEngine: BaseEngine{ - id: "opencode", - displayName: "OpenCode", - description: "Uses OpenCode AI coding assistant (experimental)", - experimental: true, - supportsToolsWhitelist: true, - }, - } -} - -func (e *OpenCodeEngine) GetInstallationSteps(engineConfig *EngineConfig) []GitHubActionStep { - // Build the npm install command, optionally with version - installCmd := "npm install -g opencode" - if engineConfig != nil && engineConfig.Version != "" { - installCmd = fmt.Sprintf("npm install -g opencode@%s", engineConfig.Version) - } - - return []GitHubActionStep{ - { - " - name: Setup Node.js", - " uses: actions/setup-node@v4", - " with:", - " node-version: '24'", - " cache: 'npm'", - }, - { - " - name: Install OpenCode", - fmt.Sprintf(" run: %s", installCmd), - }, - } -} - -func (e *OpenCodeEngine) GetExecutionConfig(workflowName string, logFile string, engineConfig *EngineConfig) ExecutionConfig { - // Configure model and API settings based on engineConfig - modelConfig := "" - apiKeyEnv := "OPENCODE_API_KEY" - apiKeySecret := "${{ secrets.OPENCODE_API_KEY }}" - - if engineConfig != nil && engineConfig.Model != "" { - // If a specific model is configured, use it - modelConfig = fmt.Sprintf("--model %s", engineConfig.Model) - - // For Claude models, use Anthropic API key - if strings.HasPrefix(engineConfig.Model, "claude") || strings.HasPrefix(engineConfig.Model, "anthropic/") { - apiKeyEnv = "ANTHROPIC_API_KEY" - apiKeySecret = "${{ secrets.ANTHROPIC_API_KEY }}" - } - } - - command := fmt.Sprintf(`INSTRUCTION=$(cat /tmp/aw-prompts/prompt.txt) -export OPENCODE_CONFIG=/tmp/mcp-config - -# Create log directory outside git repo -mkdir -p /tmp/aw-logs - -# Run opencode with log capture -opencode exec \ - --config /tmp/mcp-config/opencode.json \ - %s \ - --auto "$INSTRUCTION" 2>&1 | tee /tmp/aw-logs/%s.log`, modelConfig, logFile) - - return ExecutionConfig{ - StepName: "Run OpenCode", - Command: command, - Environment: map[string]string{ - apiKeyEnv: apiKeySecret, - }, - } -} - -func (e *OpenCodeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]any, mcpTools []string) { - yaml.WriteString(" cat > /tmp/mcp-config/opencode.json << 'EOF'\n") - yaml.WriteString(" {\n") - yaml.WriteString(" \"mcpServers\": {\n") - - // Generate configuration for each MCP tool - for i, toolName := range mcpTools { - isLast := i == len(mcpTools)-1 - - switch toolName { - case "github": - githubTool := tools["github"] - e.renderGitHubOpenCodeMCPConfig(yaml, githubTool, isLast) - default: - // Handle custom MCP tools (those with MCP-compatible type) - if toolConfig, ok := tools[toolName].(map[string]any); ok { - if hasMcp, _ := hasMCPConfig(toolConfig); hasMcp { - if err := e.renderOpenCodeMCPConfig(yaml, toolName, toolConfig, isLast); err != nil { - fmt.Printf("Error generating custom MCP configuration for %s: %v\n", toolName, err) - } - } - } - } - } - - yaml.WriteString(" }\n") - yaml.WriteString(" }\n") - yaml.WriteString(" EOF\n") -} - -// renderGitHubOpenCodeMCPConfig generates the GitHub MCP server configuration -// Uses Docker MCP as the default for OpenCode -func (e *OpenCodeEngine) renderGitHubOpenCodeMCPConfig(yaml *strings.Builder, githubTool any, isLast bool) { - yaml.WriteString(" \"github\": {\n") - yaml.WriteString(" \"command\": \"docker\",\n") - yaml.WriteString(" \"args\": [\n") - yaml.WriteString(" \"run\",\n") - yaml.WriteString(" \"--rm\",\n") - yaml.WriteString(" \"-e\", \"GITHUB_TOKEN\",\n") - yaml.WriteString(" \"ghcr.io/githubnext/github-mcp-server:latest\"\n") - yaml.WriteString(" ],\n") - yaml.WriteString(" \"env\": {\n") - yaml.WriteString(" \"GITHUB_TOKEN\": \"${{ secrets.GITHUB_TOKEN }}\"\n") - yaml.WriteString(" }\n") - - if isLast { - yaml.WriteString(" }\n") - } else { - yaml.WriteString(" },\n") - } -} - -// renderOpenCodeMCPConfig generates custom MCP server configuration for a single tool in OpenCode workflow opencode.json -func (e *OpenCodeEngine) renderOpenCodeMCPConfig(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { - yaml.WriteString(fmt.Sprintf(" \"%s\": {\n", toolName)) - - // Use the shared MCP config renderer with JSON format - renderer := MCPConfigRenderer{ - IndentLevel: " ", - Format: "json", - } - - err := renderSharedMCPConfig(yaml, toolName, toolConfig, isLast, renderer) - if err != nil { - return err - } - - if isLast { - yaml.WriteString(" }\n") - } else { - yaml.WriteString(" },\n") - } - - return nil -} diff --git a/pkg/workflow/opencode_engine_test.go b/pkg/workflow/opencode_engine_test.go deleted file mode 100644 index d0988f20c6..0000000000 --- a/pkg/workflow/opencode_engine_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package workflow - -import ( - "strings" - "testing" -) - -func TestOpenCodeEngine(t *testing.T) { - engine := NewOpenCodeEngine() - - // Test basic properties - if engine.GetID() != "opencode" { - t.Errorf("Expected ID 'opencode', got '%s'", engine.GetID()) - } - - if engine.GetDisplayName() != "OpenCode" { - t.Errorf("Expected display name 'OpenCode', got '%s'", engine.GetDisplayName()) - } - - if !engine.IsExperimental() { - t.Error("OpenCode engine should be experimental") - } - - if !engine.SupportsToolsWhitelist() { - t.Error("OpenCode engine should support MCP tools") - } - - // Test installation steps - steps := engine.GetInstallationSteps(nil) - expectedStepCount := 2 // Setup Node.js and Install OpenCode - if len(steps) != expectedStepCount { - t.Errorf("Expected %d installation steps, got %d", expectedStepCount, len(steps)) - } - - // Verify first step is Setup Node.js - if len(steps) > 0 && len(steps[0]) > 0 { - if !strings.Contains(steps[0][0], "Setup Node.js") { - t.Errorf("Expected first step to contain 'Setup Node.js', got '%s'", steps[0][0]) - } - } - - // Verify second step is Install OpenCode - if len(steps) > 1 && len(steps[1]) > 0 { - if !strings.Contains(steps[1][0], "Install OpenCode") { - t.Errorf("Expected second step to contain 'Install OpenCode', got '%s'", steps[1][0]) - } - } - - // Test execution config - config := engine.GetExecutionConfig("test-workflow", "test-log", nil) - if config.StepName != "Run OpenCode" { - t.Errorf("Expected step name 'Run OpenCode', got '%s'", config.StepName) - } - - if config.Action != "" { - t.Errorf("Expected empty action for OpenCode (uses command), got '%s'", config.Action) - } - - if !strings.Contains(config.Command, "opencode exec") { - t.Errorf("Expected command to contain 'opencode exec', got '%s'", config.Command) - } - - if !strings.Contains(config.Command, "test-log.log") { - t.Errorf("Expected command to contain log file name, got '%s'", config.Command) - } - - // Check environment variables - if config.Environment["OPENCODE_API_KEY"] != "${{ secrets.OPENCODE_API_KEY }}" { - t.Errorf("Expected OPENCODE_API_KEY environment variable, got '%s'", config.Environment["OPENCODE_API_KEY"]) - } -} - -func TestOpenCodeMCPConfigGeneration(t *testing.T) { - engine := NewOpenCodeEngine() - - // Test MCP config generation with GitHub tool - tools := map[string]any{ - "github": map[string]any{ - "allowed": []string{"get_issue", "add_issue_comment"}, - }, - } - mcpTools := []string{"github"} - - var yaml strings.Builder - engine.RenderMCPConfig(&yaml, tools, mcpTools) - - config := yaml.String() - - // Check that opencode.json is generated (not mcp-servers.json or config.toml) - if !strings.Contains(config, "cat > /tmp/mcp-config/opencode.json") { - t.Errorf("Expected config to contain opencode.json generation for OpenCode but it didn't.\nContent:\n%s", config) - } - - // Check for GitHub MCP configuration - if !strings.Contains(config, "\"github\": {") { - t.Errorf("Expected config to contain GitHub MCP server configuration but it didn't.\nContent:\n%s", config) - } - - // Check for Docker-based GitHub MCP server (following pattern) - if !strings.Contains(config, "\"command\": \"docker\"") { - t.Errorf("Expected config to contain Docker command for GitHub MCP server but it didn't.\nContent:\n%s", config) - } - - // Check JSON structure - if !strings.Contains(config, "\"mcpServers\": {") { - t.Errorf("Expected config to contain 'mcpServers' section but it didn't.\nContent:\n%s", config) - } -} - -func TestOpenCodeCustomMCPConfig(t *testing.T) { - engine := NewOpenCodeEngine() - - // Test custom MCP tool configuration - tools := map[string]any{ - "custom-tool": map[string]any{ - "mcp": map[string]any{ - "type": "stdio", - "command": "node", - "args": []any{"custom-server.js"}, - "env": map[string]any{ - "API_KEY": "test-key", - }, - }, - "allowed": []string{"custom_function"}, - }, - } - mcpTools := []string{"custom-tool"} - - var yaml strings.Builder - engine.RenderMCPConfig(&yaml, tools, mcpTools) - - config := yaml.String() - - // Check that custom tool is configured - if !strings.Contains(config, "\"custom-tool\": {") { - t.Errorf("Expected config to contain custom-tool configuration but it didn't.\nContent:\n%s", config) - } - - // Check command configuration - if !strings.Contains(config, "\"command\": \"node\"") { - t.Errorf("Expected config to contain node command but it didn't.\nContent:\n%s", config) - } - - // Check args configuration - if !strings.Contains(config, "\"custom-server.js\"") { - t.Errorf("Expected config to contain custom-server.js in args but it didn't.\nContent:\n%s", config) - } - - // Check env configuration - if !strings.Contains(config, "\"API_KEY\": \"test-key\"") { - t.Errorf("Expected config to contain API_KEY environment variable but it didn't.\nContent:\n%s", config) - } -} - -func TestOpenCodeHTTPMCPConfig(t *testing.T) { - engine := NewOpenCodeEngine() - - // Test HTTP MCP tool configuration - tools := map[string]any{ - "http-tool": map[string]any{ - "mcp": map[string]any{ - "type": "http", - "url": "http://localhost:3000", - "headers": map[string]any{ - "Authorization": "Bearer token", - }, - }, - "allowed": []string{"http_function"}, - }, - } - mcpTools := []string{"http-tool"} - - var yaml strings.Builder - engine.RenderMCPConfig(&yaml, tools, mcpTools) - - config := yaml.String() - - // Check that HTTP tool is configured - if !strings.Contains(config, "\"http-tool\": {") { - t.Errorf("Expected config to contain http-tool configuration but it didn't.\nContent:\n%s", config) - } - - // Check URL configuration - if !strings.Contains(config, "\"url\": \"http://localhost:3000\"") { - t.Errorf("Expected config to contain URL but it didn't.\nContent:\n%s", config) - } - - // Check headers configuration - if !strings.Contains(config, "\"Authorization\": \"Bearer token\"") { - t.Errorf("Expected config to contain Authorization header but it didn't.\nContent:\n%s", config) - } -} - -func TestOpenCodeEngineWithVersion(t *testing.T) { - engine := NewOpenCodeEngine() - - // Test installation steps without version - stepsNoVersion := engine.GetInstallationSteps(nil) - foundNoVersionInstall := false - for _, step := range stepsNoVersion { - for _, line := range step { - if strings.Contains(line, "npm install -g opencode") && !strings.Contains(line, "@") { - foundNoVersionInstall = true - break - } - } - } - if !foundNoVersionInstall { - t.Error("Expected default npm install command without version") - } - - // Test installation steps with version - engineConfig := &EngineConfig{ - ID: "opencode", - Version: "2.1.0", - } - stepsWithVersion := engine.GetInstallationSteps(engineConfig) - foundVersionInstall := false - for _, step := range stepsWithVersion { - for _, line := range step { - if strings.Contains(line, "npm install -g opencode@2.1.0") { - foundVersionInstall = true - break - } - } - } - if !foundVersionInstall { - t.Error("Expected versioned npm install command with opencode@2.1.0") - } -}