From 6271f7574ce74a580f289ffc195d1ee706afa27b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:11:55 +0000 Subject: [PATCH 1/6] Initial plan From c6d27b9a861d3b2d32bc0b5fd22efa721c508f12 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:18:42 +0000 Subject: [PATCH 2/6] Initial analysis - understanding current custom-agent implementation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/unbloat-docs.lock.yml | 2 +- pkg/workflow/schemas/github-workflow.json | 76 ++++++++++++++++++++--- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 07e4b38944..0c8bd30d6b 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -846,7 +846,7 @@ jobs: - name: Checkout repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 with: cache: npm cache-dependency-path: docs/package-lock.json diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index 6b93ceff0b..160824c1de 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -983,9 +983,19 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": ["created", "rerequested", "completed", "requested_action"] + "enum": [ + "created", + "rerequested", + "completed", + "requested_action" + ] }, - "default": ["created", "rerequested", "completed", "requested_action"] + "default": [ + "created", + "rerequested", + "completed", + "requested_action" + ] } } }, @@ -1197,7 +1207,13 @@ "type": "string", "enum": ["created", "closed", "opened", "edited", "deleted"] }, - "default": ["created", "closed", "opened", "edited", "deleted"] + "default": [ + "created", + "closed", + "opened", + "edited", + "deleted" + ] } } }, @@ -1215,9 +1231,23 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": ["created", "updated", "closed", "reopened", "edited", "deleted"] + "enum": [ + "created", + "updated", + "closed", + "reopened", + "edited", + "deleted" + ] }, - "default": ["created", "updated", "closed", "reopened", "edited", "deleted"] + "default": [ + "created", + "updated", + "closed", + "reopened", + "edited", + "deleted" + ] } } }, @@ -1230,9 +1260,21 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": ["created", "moved", "converted", "edited", "deleted"] + "enum": [ + "created", + "moved", + "converted", + "edited", + "deleted" + ] }, - "default": ["created", "moved", "converted", "edited", "deleted"] + "default": [ + "created", + "moved", + "converted", + "edited", + "deleted" + ] } } }, @@ -1516,9 +1558,25 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": ["published", "unpublished", "created", "edited", "deleted", "prereleased", "released"] + "enum": [ + "published", + "unpublished", + "created", + "edited", + "deleted", + "prereleased", + "released" + ] }, - "default": ["published", "unpublished", "created", "edited", "deleted", "prereleased", "released"] + "default": [ + "published", + "unpublished", + "created", + "edited", + "deleted", + "prereleased", + "released" + ] } } }, From b2283f0ff2918e7ed9731e0080acb362298c6197 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:31:35 +0000 Subject: [PATCH 3/6] Implement imports-based agent file support - Remove custom-agent field from engine configuration - Add support for .agent.md files in imports - Only one agent file allowed per workflow - Agent files detected by .agent.md extension - Update all engines to use AgentFile from WorkflowData - Update tests to reflect new import-based approach - Update technical-doc-writer workflow to use imports - Update JSON schema to remove custom-agent field Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...riter.md => technical-doc-writer.agent.md} | 0 .../workflows/technical-doc-writer.lock.yml | 123 +++++++++++++- .github/workflows/technical-doc-writer.md | 2 +- pkg/parser/frontmatter.go | 35 ++++ pkg/parser/schemas/main_workflow_schema.json | 11 +- pkg/workflow/claude_engine.go | 6 +- pkg/workflow/codex_engine.go | 6 +- pkg/workflow/compiler.go | 2 + pkg/workflow/compiler_jobs.go | 4 +- pkg/workflow/copilot_engine.go | 6 +- pkg/workflow/engine.go | 8 - ...ld_test.go => engine_agent_import_test.go} | 157 ++++++------------ pkg/workflow/validation.go | 64 ++----- 13 files changed, 239 insertions(+), 185 deletions(-) rename .github/agents/{technical-doc-writer.md => technical-doc-writer.agent.md} (100%) rename pkg/workflow/{engine_agent_field_test.go => engine_agent_import_test.go} (63%) diff --git a/.github/agents/technical-doc-writer.md b/.github/agents/technical-doc-writer.agent.md similarity index 100% rename from .github/agents/technical-doc-writer.md rename to .github/agents/technical-doc-writer.agent.md diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index d127ab1d6a..2e98eb2d01 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -6,6 +6,7 @@ # Resolved workflow manifest: # Imports: # - ../instructions/documentation.instructions.md +# - ../agents/technical-doc-writer.agent.md # # Job Dependency Graph: # ```mermaid @@ -1693,6 +1694,124 @@ jobs: - **Concepts**: Explanation format, building understanding - **API Reference**: Reference format, complete API documentation + # Technical Documentation Writer for GitHub Actions + + You are an AI technical documentation writer that produces developer-focused documentation for a **GitHub Actions library**. + Your docs use **Astro Starlight** and follow the **GitHub Docs voice**. + You apply user-research–backed best practices to ensure clarity, discoverability, and developer experience (DX). + + ## Core Principles + + ### Framework + - Output uses **Astro Starlight** features: + - Markdown/MDX with headings, sidebars, and TOC. + - Autogenerated navigation by directory (`getting-started/`, `guides/`, `reference/`). + - Admonitions (`:::note`, `:::tip`, `:::caution`) for key callouts. + - Frontmatter metadata (`title`, `description`) for each page. + + ### Style & Tone (GitHub Docs) + - Clear, concise, approachable English. + - Active voice; address reader as "you". + - Friendly, empathetic, trustworthy tone. + - Prioritize clarity over rigid grammar rules. + - Consistent terminology across all docs. + - Inclusive, globally understandable (avoid slang/idioms). + + ### Structure (Diátaxis-inspired) + - **Getting Started** → prerequisites, install, first example. + - **How-to Guides** → task-based, step-by-step workflows. + - **Reference** → full breakdown of inputs, outputs, options. + - **Concepts/FAQs** → background explanations. + + ### Developer Experience (DX) + - Runnable, copy-paste–ready code blocks. + - Prerequisites clearly listed. + - Minimal setup friction. + - Early "Hello World" example. + - Optimized headings for search. + + ## Navigation & Linking + - Sidebar auto-generated by folder structure. + - Per-page TOC built from headings. + - Descriptive internal links (`See [Getting Started](/docs/getting-started)`). + - Relative links within docs; clear labels for external references. + + ## Code Guidelines + - Use fenced code blocks with language tags: + ```yaml + name: CI + on: [push] + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: my-org/my-action@v1 + ``` + - Do **not** include `$` prompts. + - Use ALL_CAPS placeholders (e.g. `USERNAME`). + - Keep lines ~60 chars wide. + - Comment out command outputs. + + ## Alerts & Callouts + Use Starlight admonition syntax sparingly: + + :::note + This is optional context. + ::: + + :::tip + This is a recommended best practice. + ::: + + :::warning + This step may cause irreversible changes. + ::: + + :::caution + This action could result in data loss. + ::: + + ## Behavior Rules + - Optimize for clarity and user goals. + - Check factual accuracy (syntax, versions). + - Maintain voice and consistency. + - Anticipate pitfalls and explain fixes empathetically. + - Use alerts only when necessary. + + ## Example Document Skeleton + ```md + --- + title: Getting Started + description: Quickstart for using the GitHub Actions library + --- + + # Getting Started + + ## Prerequisites + - Node.js ≥ 20 + - GitHub account + + ## Installation + ```bash + pnpm add @my-org/github-action + ``` + + ## Quick Example + ```yaml + name: CI + on: [push] + jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: my-org/my-action@v1 + ``` + + --- + ``` + ## Your Task This workflow is triggered manually via workflow_dispatch with a documentation topic. @@ -2018,7 +2137,7 @@ jobs: mkdir -p /tmp/gh-aw/agent/ mkdir -p /tmp/gh-aw/cache-memory/ mkdir -p /tmp/gh-aw/.copilot/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --agent technical-doc-writer.md --allow-tool 'github(add_reaction)' --allow-tool 'github(get_file_contents)' --allow-tool 'github(get_pull_request)' --allow-tool 'github(issue_read)' --allow-tool 'github(list_commits)' --allow-tool 'github(pull_request_read)' --allow-tool safeoutputs --allow-tool shell --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/agent-stdio.log + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --agent /home/runner/work/gh-aw/gh-aw/.github/agents/technical-doc-writer.agent.md --allow-tool 'github(add_reaction)' --allow-tool 'github(get_file_contents)' --allow-tool 'github(get_pull_request)' --allow-tool 'github(issue_read)' --allow-tool 'github(list_commits)' --allow-tool 'github(pull_request_read)' --allow-tool safeoutputs --allow-tool shell --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg" @@ -4885,7 +5004,7 @@ jobs: mkdir -p /tmp/gh-aw/ mkdir -p /tmp/gh-aw/agent/ mkdir -p /tmp/gh-aw/.copilot/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --agent technical-doc-writer.md --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --allow-tool 'shell(cat)' --allow-tool 'shell(grep)' --allow-tool 'shell(head)' --allow-tool 'shell(jq)' --allow-tool 'shell(ls)' --allow-tool 'shell(tail)' --allow-tool 'shell(wc)' --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt diff --git a/.github/workflows/technical-doc-writer.md b/.github/workflows/technical-doc-writer.md index 38f802a31e..981e860df5 100644 --- a/.github/workflows/technical-doc-writer.md +++ b/.github/workflows/technical-doc-writer.md @@ -11,7 +11,6 @@ permissions: read-all engine: id: copilot - custom-agent: technical-doc-writer.md network: allowed: @@ -20,6 +19,7 @@ network: imports: - ../instructions/documentation.instructions.md + - ../agents/technical-doc-writer.agent.md safe-outputs: add-comment: diff --git a/pkg/parser/frontmatter.go b/pkg/parser/frontmatter.go index 028bb0ed1a..728721a559 100644 --- a/pkg/parser/frontmatter.go +++ b/pkg/parser/frontmatter.go @@ -101,6 +101,7 @@ type ImportsResult struct { MergedNetwork string // Merged network configuration from all imports MergedPermissions string // Merged permissions configuration from all imports ImportedFiles []string // List of imported file paths (for manifest) + AgentFile string // Path to custom agent file (if imported) } // ExtractFrontmatterFromContent parses YAML frontmatter from markdown content string @@ -411,6 +412,7 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD var engines []string var safeOutputs []string var processedFiles []string + var agentFile string // Track custom agent file for _, importPath := range imports { // Handle section references (file.md#Section) @@ -440,6 +442,38 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD // Add to list of processed files (use original importPath for manifest) processedFiles = append(processedFiles, importPath) + // Check if this is a custom agent file (.agent.md) + isAgentFile := strings.HasSuffix(strings.ToLower(fullPath), ".agent.md") + if isAgentFile { + if agentFile != "" { + // Multiple agent files found - error + return nil, fmt.Errorf("multiple agent files found in imports: '%s' and '%s'. Only one agent file is allowed per workflow", agentFile, importPath) + } + agentFile = fullPath + log.Printf("Found agent file: %s", fullPath) + + // For agent files, only extract markdown content + // Extract markdown content from imported file + markdownContent, err := processIncludedFileWithVisited(fullPath, sectionName, false, visited) + if err != nil { + return nil, fmt.Errorf("failed to process markdown from agent file '%s': %w", fullPath, err) + } + if markdownContent != "" { + markdownBuilder.WriteString(markdownContent) + // Add blank line separator between imported files + if !strings.HasSuffix(markdownContent, "\n\n") { + if strings.HasSuffix(markdownContent, "\n") { + markdownBuilder.WriteString("\n") + } else { + markdownBuilder.WriteString("\n\n") + } + } + } + + // Skip other extractions for agent files + continue + } + // Extract tools from imported file toolsContent, err := processIncludedFileWithVisited(fullPath, sectionName, true, visited) if err != nil { @@ -530,6 +564,7 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD MergedNetwork: networkBuilder.String(), MergedPermissions: permissionsBuilder.String(), ImportedFiles: processedFiles, + AgentFile: agentFile, }, nil } diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index de4a100b91..1476c1327d 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -22,15 +22,16 @@ }, "imports": { "type": "array", - "description": "Optional array of workflow specifications to import (similar to @include directives but defined in frontmatter). Format: owner/repo/path@ref (e.g., githubnext/agentics/workflows/shared/common.md@v1.0.0).", + "description": "Optional array of workflow specifications to import (similar to @include directives but defined in frontmatter). Format: owner/repo/path@ref (e.g., githubnext/agentics/workflows/shared/common.md@v1.0.0). Files ending with '.agent.md' under .github/agents are treated as custom agent files and only one agent file is allowed per workflow.", "items": { "type": "string", - "description": "Workflow specification in format owner/repo/path@ref" + "description": "Workflow specification in format owner/repo/path@ref. Files with '.agent.md' extension are treated as agent configuration files." }, "examples": [ ["shared/jqschema.md", "shared/reporting.md"], ["shared/mcp/gh-aw.md", "shared/jqschema.md", "shared/reporting.md"], - ["../instructions/documentation.instructions.md"] + ["../instructions/documentation.instructions.md"], + [".github/agents/my-agent.agent.md"] ] }, "on": { @@ -3171,10 +3172,6 @@ "type": "string" }, "description": "Optional array of command-line arguments to pass to the AI engine CLI. These arguments are injected after all other args but before the prompt." - }, - "custom-agent": { - "type": "string", - "description": "Optional path to a custom agent configuration file. For copilot engine, this is passed as --agent flag. For claude and codex engines, the markdown body from the agent file is injected as a system prompt." } }, "required": ["id"], diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index c1110c5d43..8a20d466e4 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -167,12 +167,12 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str stepLines = append(stepLines, " set -o pipefail") stepLines = append(stepLines, " # Execute Claude Code CLI with prompt from file") - // Build the prompt command - prepend custom agent file content if specified + // Build the prompt command - prepend custom agent file content if specified (via imports) var promptCommand string - if workflowData.EngineConfig != nil && workflowData.EngineConfig.CustomAgent != "" { + if workflowData.AgentFile != "" { // Extract markdown body from custom agent file and prepend to prompt stepLines = append(stepLines, " # Extract markdown body from custom agent file (skip frontmatter)") - stepLines = append(stepLines, fmt.Sprintf(" AGENT_CONTENT=$(awk 'BEGIN{skip=1} /^---$/{if(skip){skip=0;next}else{skip=1;next}} !skip' %s)", workflowData.EngineConfig.CustomAgent)) + stepLines = append(stepLines, fmt.Sprintf(" AGENT_CONTENT=$(awk 'BEGIN{skip=1} /^---$/{if(skip){skip=0;next}else{skip=1;next}} !skip' %s)", workflowData.AgentFile)) stepLines = append(stepLines, " # Combine agent content with prompt") stepLines = append(stepLines, " PROMPT_TEXT=$(printf '%s\\n\\n%s' \"$AGENT_CONTENT\" \"$(cat /tmp/gh-aw/aw-prompts/prompt.txt)\")") promptCommand = "\"$PROMPT_TEXT\"" diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index 2b8b21256d..baadec92f0 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -107,15 +107,15 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri } } - // Build the command with custom agent file prepending if specified + // Build the command with custom agent file prepending if specified (via imports) var instructionCommand string - if workflowData.EngineConfig != nil && workflowData.EngineConfig.CustomAgent != "" { + if workflowData.AgentFile != "" { // Extract markdown body from custom agent file (skip frontmatter) and prepend to prompt instructionCommand = fmt.Sprintf(`set -o pipefail AGENT_CONTENT=$(awk 'BEGIN{skip=1} /^---$/{if(skip){skip=0;next}else{skip=1;next}} !skip' %s) INSTRUCTION=$(printf "%%s\n\n%%s" "$AGENT_CONTENT" "$(cat $GH_AW_PROMPT)") mkdir -p $CODEX_HOME/logs -codex %sexec%s%s%s"$INSTRUCTION" 2>&1 | tee %s`, workflowData.EngineConfig.CustomAgent, modelParam, webSearchParam, fullAutoParam, customArgsParam, logFile) +codex %sexec%s%s%s"$INSTRUCTION" 2>&1 | tee %s`, workflowData.AgentFile, modelParam, webSearchParam, fullAutoParam, customArgsParam, logFile) } else { instructionCommand = fmt.Sprintf(`set -o pipefail INSTRUCTION=$(cat $GH_AW_PROMPT) diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index cd91a81a50..823f41de63 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -168,6 +168,7 @@ type WorkflowData struct { MarkdownContent string AI string // "claude" or "codex" (for backwards compatibility) EngineConfig *EngineConfig // Extended engine configuration + AgentFile string // Path to custom agent file (from imports) StopTime string Command string // for /command trigger support CommandEvents []string // events where command should be active (nil = all events) @@ -902,6 +903,7 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error) MarkdownContent: markdownContent, AI: engineSetting, EngineConfig: engineConfig, + AgentFile: importsResult.AgentFile, NetworkPermissions: networkPermissions, NeedsTextOutput: needsTextOutput, SafetyPrompt: safetyPrompt, diff --git a/pkg/workflow/compiler_jobs.go b/pkg/workflow/compiler_jobs.go index 91d6b231b0..6e85b02026 100644 --- a/pkg/workflow/compiler_jobs.go +++ b/pkg/workflow/compiler_jobs.go @@ -746,8 +746,8 @@ func (c *Compiler) shouldAddCheckoutStep(data *WorkflowData) bool { return false // Custom steps already have checkout } - // Check condition 2: If custom agent file is specified, checkout is required - if data.EngineConfig != nil && data.EngineConfig.CustomAgent != "" { + // Check condition 2: If custom agent file is specified (via imports), checkout is required + if data.AgentFile != "" { return true // Custom agent file requires checkout to access the file } diff --git a/pkg/workflow/copilot_engine.go b/pkg/workflow/copilot_engine.go index 18c12ef28d..d7bec4c68c 100644 --- a/pkg/workflow/copilot_engine.go +++ b/pkg/workflow/copilot_engine.go @@ -138,9 +138,9 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st copilotArgs = append(copilotArgs, "--model", workflowData.EngineConfig.Model) } - // Add --agent flag if custom agent file is specified - if workflowData.EngineConfig != nil && workflowData.EngineConfig.CustomAgent != "" { - copilotArgs = append(copilotArgs, "--agent", workflowData.EngineConfig.CustomAgent) + // Add --agent flag if custom agent file is specified (via imports) + if workflowData.AgentFile != "" { + copilotArgs = append(copilotArgs, "--agent", workflowData.AgentFile) } // Add tool permission arguments based on configuration diff --git a/pkg/workflow/engine.go b/pkg/workflow/engine.go index 5b193db0da..70b3b25e7b 100644 --- a/pkg/workflow/engine.go +++ b/pkg/workflow/engine.go @@ -17,7 +17,6 @@ type EngineConfig struct { MaxTurns string Concurrency string // Agent job-level concurrency configuration (YAML format) UserAgent string - CustomAgent string // Path to custom agent configuration file Env map[string]string Steps []map[string]any ErrorPatterns []ErrorPattern @@ -120,13 +119,6 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng } } - // Extract optional 'custom-agent' field - if customAgent, hasCustomAgent := engineObj["custom-agent"]; hasCustomAgent { - if customAgentStr, ok := customAgent.(string); ok { - config.CustomAgent = customAgentStr - } - } - // Extract optional 'env' field (object/map of strings) if env, hasEnv := engineObj["env"]; hasEnv { if envMap, ok := env.(map[string]any); ok { diff --git a/pkg/workflow/engine_agent_field_test.go b/pkg/workflow/engine_agent_import_test.go similarity index 63% rename from pkg/workflow/engine_agent_field_test.go rename to pkg/workflow/engine_agent_import_test.go index 7d7971df75..2c0eca1838 100644 --- a/pkg/workflow/engine_agent_field_test.go +++ b/pkg/workflow/engine_agent_import_test.go @@ -7,62 +7,15 @@ import ( "testing" ) -// TestEngineConfigAgentFieldExtraction tests that the custom-agent field is correctly extracted from frontmatter -func TestEngineConfigAgentFieldExtraction(t *testing.T) { - compiler := NewCompiler(false, "", "") - - frontmatter := map[string]any{ - "engine": map[string]any{ - "id": "copilot", - "custom-agent": "/path/to/agent.md", - }, - } - - engineID, config := compiler.ExtractEngineConfig(frontmatter) - - if engineID != "copilot" { - t.Errorf("Expected engine ID 'copilot', got '%s'", engineID) - } - - if config == nil { - t.Fatal("Expected non-nil engine config") - } - - if config.CustomAgent != "/path/to/agent.md" { - t.Errorf("Expected custom agent path '/path/to/agent.md', got '%s'", config.CustomAgent) - } -} - -// TestEngineConfigAgentFieldEmpty tests that empty custom-agent field is handled correctly -func TestEngineConfigAgentFieldEmpty(t *testing.T) { - compiler := NewCompiler(false, "", "") - - frontmatter := map[string]any{ - "engine": map[string]any{ - "id": "claude", - }, - } - - _, config := compiler.ExtractEngineConfig(frontmatter) - - if config == nil { - t.Fatal("Expected non-nil engine config") - } - - if config.CustomAgent != "" { - t.Errorf("Expected empty custom agent path, got '%s'", config.CustomAgent) - } -} - -// TestCopilotEngineWithAgentFlag tests that copilot engine includes --agent flag when custom-agent is specified -func TestCopilotEngineWithAgentFlag(t *testing.T) { +// TestCopilotEngineWithAgentFromImports tests that copilot engine includes --agent flag when agent file is imported +func TestCopilotEngineWithAgentFromImports(t *testing.T) { engine := NewCopilotEngine() workflowData := &WorkflowData{ Name: "test-workflow", EngineConfig: &EngineConfig{ - ID: "copilot", - CustomAgent: "/path/to/agent.md", + ID: "copilot", }, + AgentFile: "/path/to/agent.md", } steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") @@ -78,7 +31,7 @@ func TestCopilotEngineWithAgentFlag(t *testing.T) { } } -// TestCopilotEngineWithoutAgentFlag tests that copilot engine works without custom-agent flag +// TestCopilotEngineWithoutAgentFlag tests that copilot engine works without agent file func TestCopilotEngineWithoutAgentFlag(t *testing.T) { engine := NewCopilotEngine() workflowData := &WorkflowData{ @@ -97,19 +50,19 @@ func TestCopilotEngineWithoutAgentFlag(t *testing.T) { stepContent := strings.Join([]string(steps[0]), "\n") if strings.Contains(stepContent, "--agent") { - t.Errorf("Did not expect '--agent' flag when custom-agent is not specified, got:\n%s", stepContent) + t.Errorf("Did not expect '--agent' flag when agent file is not specified, got:\n%s", stepContent) } } -// TestClaudeEngineWithAgentFile tests that claude engine prepends custom agent file content to prompt -func TestClaudeEngineWithAgentFile(t *testing.T) { +// TestClaudeEngineWithAgentFromImports tests that claude engine prepends agent file content to prompt +func TestClaudeEngineWithAgentFromImports(t *testing.T) { engine := NewClaudeEngine() workflowData := &WorkflowData{ Name: "test-workflow", EngineConfig: &EngineConfig{ - ID: "claude", - CustomAgent: "/path/to/agent.md", + ID: "claude", }, + AgentFile: "/path/to/agent.md", } steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") @@ -122,21 +75,21 @@ func TestClaudeEngineWithAgentFile(t *testing.T) { // Check that custom agent content extraction is present if !strings.Contains(stepContent, "AGENT_CONTENT=$(awk") { - t.Errorf("Expected custom agent content extraction in claude command, got:\n%s", stepContent) + t.Errorf("Expected agent content extraction in claude command, got:\n%s", stepContent) } - // Check that custom agent file path is referenced + // Check that agent file path is referenced if !strings.Contains(stepContent, "/path/to/agent.md") { - t.Errorf("Expected custom agent file path in claude command, got:\n%s", stepContent) + t.Errorf("Expected agent file path in claude command, got:\n%s", stepContent) } - // Check that custom agent content is prepended to prompt + // Check that agent content is prepended to prompt if !strings.Contains(stepContent, "$AGENT_CONTENT") { t.Errorf("Expected $AGENT_CONTENT variable in claude command, got:\n%s", stepContent) } } -// TestClaudeEngineWithoutAgentFile tests that claude engine works without custom agent file +// TestClaudeEngineWithoutAgentFile tests that claude engine works without agent file func TestClaudeEngineWithoutAgentFile(t *testing.T) { engine := NewClaudeEngine() workflowData := &WorkflowData{ @@ -154,9 +107,9 @@ func TestClaudeEngineWithoutAgentFile(t *testing.T) { stepContent := strings.Join([]string(steps[0]), "\n") - // Should not have custom agent content extraction + // Should not have agent content extraction if strings.Contains(stepContent, "AGENT_CONTENT") { - t.Errorf("Did not expect AGENT_CONTENT when custom-agent is not specified, got:\n%s", stepContent) + t.Errorf("Did not expect AGENT_CONTENT when agent file is not specified, got:\n%s", stepContent) } // Should still have the standard prompt @@ -165,15 +118,15 @@ func TestClaudeEngineWithoutAgentFile(t *testing.T) { } } -// TestCodexEngineWithAgentFile tests that codex engine prepends custom agent file content to prompt -func TestCodexEngineWithAgentFile(t *testing.T) { +// TestCodexEngineWithAgentFromImports tests that codex engine prepends agent file content to prompt +func TestCodexEngineWithAgentFromImports(t *testing.T) { engine := NewCodexEngine() workflowData := &WorkflowData{ Name: "test-workflow", EngineConfig: &EngineConfig{ - ID: "codex", - CustomAgent: "/path/to/agent.md", + ID: "codex", }, + AgentFile: "/path/to/agent.md", } steps := engine.GetExecutionSteps(workflowData, "/tmp/gh-aw/test.log") @@ -184,17 +137,17 @@ func TestCodexEngineWithAgentFile(t *testing.T) { stepContent := strings.Join([]string(steps[0]), "\n") - // Check that custom agent content extraction is present + // Check that agent content extraction is present if !strings.Contains(stepContent, "AGENT_CONTENT=$(awk") { - t.Errorf("Expected custom agent content extraction in codex command, got:\n%s", stepContent) + t.Errorf("Expected agent content extraction in codex command, got:\n%s", stepContent) } - // Check that custom agent file path is referenced + // Check that agent file path is referenced if !strings.Contains(stepContent, "/path/to/agent.md") { - t.Errorf("Expected custom agent file path in codex command, got:\n%s", stepContent) + t.Errorf("Expected agent file path in codex command, got:\n%s", stepContent) } - // Check that custom agent content is prepended to prompt using printf + // Check that agent content is prepended to prompt using printf if !strings.Contains(stepContent, "INSTRUCTION=$(printf") { t.Errorf("Expected printf with INSTRUCTION in codex command, got:\n%s", stepContent) } @@ -204,7 +157,7 @@ func TestCodexEngineWithAgentFile(t *testing.T) { } } -// TestCodexEngineWithoutAgentFile tests that codex engine works without custom agent file +// TestCodexEngineWithoutAgentFile tests that codex engine works without agent file func TestCodexEngineWithoutAgentFile(t *testing.T) { engine := NewCodexEngine() workflowData := &WorkflowData{ @@ -222,9 +175,9 @@ func TestCodexEngineWithoutAgentFile(t *testing.T) { stepContent := strings.Join([]string(steps[0]), "\n") - // Should not have custom agent content extraction + // Should not have agent content extraction if strings.Contains(stepContent, "AGENT_CONTENT") { - t.Errorf("Did not expect AGENT_CONTENT when custom-agent is not specified, got:\n%s", stepContent) + t.Errorf("Did not expect AGENT_CONTENT when agent file is not specified, got:\n%s", stepContent) } // Should have the standard instruction reading @@ -233,7 +186,7 @@ func TestCodexEngineWithoutAgentFile(t *testing.T) { } } -// TestAgentFileValidation tests compile-time validation of custom agent file existence +// TestAgentFileValidation tests compile-time validation of agent file existence func TestAgentFileValidation(t *testing.T) { // Create a temporary directory structure that mimics a repository tmpDir, err := os.MkdirTemp("", "agent-validation-test") @@ -252,7 +205,7 @@ func TestAgentFileValidation(t *testing.T) { t.Fatalf("Failed to create workflows directory: %v", err) } - // Create a valid custom agent file + // Create a valid agent file agentContent := `--- title: Test Agent --- @@ -261,48 +214,48 @@ title: Test Agent This is a test agent file. ` - validAgentFilePath := filepath.Join(agentsDir, "valid-agent.md") + validAgentFilePath := filepath.Join(agentsDir, "valid-agent.agent.md") if err := os.WriteFile(validAgentFilePath, []byte(agentContent), 0644); err != nil { - t.Fatalf("Failed to create valid custom agent file: %v", err) + t.Fatalf("Failed to create valid agent file: %v", err) } - // Test 1: Valid custom agent file (using relative path) + // Test 1: Valid agent file t.Run("valid_agent_file", func(t *testing.T) { compiler := NewCompiler(false, "", "") workflowData := &WorkflowData{ EngineConfig: &EngineConfig{ - ID: "copilot", - CustomAgent: "valid-agent.md", // Relative path + ID: "copilot", }, + AgentFile: validAgentFilePath, } workflowPath := filepath.Join(workflowsDir, "test.md") err := compiler.validateAgentFile(workflowData, workflowPath) if err != nil { - t.Errorf("Expected no error for valid custom agent file, got: %v", err) + t.Errorf("Expected no error for valid agent file, got: %v", err) } }) - // Test 2: Non-existent custom agent file + // Test 2: Non-existent agent file t.Run("nonexistent_agent_file", func(t *testing.T) { compiler := NewCompiler(false, "", "") workflowData := &WorkflowData{ EngineConfig: &EngineConfig{ - ID: "copilot", - CustomAgent: "nonexistent.md", // Relative path to non-existent file + ID: "copilot", }, + AgentFile: filepath.Join(agentsDir, "nonexistent.agent.md"), } workflowPath := filepath.Join(workflowsDir, "test.md") err := compiler.validateAgentFile(workflowData, workflowPath) if err == nil { - t.Error("Expected error for non-existent custom agent file, got nil") + t.Error("Expected error for non-existent agent file, got nil") } else if !strings.Contains(err.Error(), "does not exist") { t.Errorf("Expected 'does not exist' error, got: %v", err) } }) - // Test 3: No custom agent file specified + // Test 3: No agent file specified t.Run("no_agent_file", func(t *testing.T) { compiler := NewCompiler(false, "", "") workflowData := &WorkflowData{ @@ -314,7 +267,7 @@ This is a test agent file. workflowPath := filepath.Join(workflowsDir, "test.md") err := compiler.validateAgentFile(workflowData, workflowPath) if err != nil { - t.Errorf("Expected no error when custom-agent not specified, got: %v", err) + t.Errorf("Expected no error when agent file not specified, got: %v", err) } }) @@ -331,21 +284,21 @@ This is a test agent file. }) } -// TestCheckoutWithAgent tests that checkout step is added when custom-agent is specified -func TestCheckoutWithAgent(t *testing.T) { +// TestCheckoutWithAgentFromImports tests that checkout step is added when agent file is imported +func TestCheckoutWithAgentFromImports(t *testing.T) { t.Run("checkout_added_with_agent", func(t *testing.T) { compiler := NewCompiler(false, "", "") workflowData := &WorkflowData{ EngineConfig: &EngineConfig{ - ID: "copilot", - CustomAgent: "/path/to/agent.md", + ID: "copilot", }, + AgentFile: "/path/to/agent.md", Permissions: "permissions:\n contents: read\n", } shouldCheckout := compiler.shouldAddCheckoutStep(workflowData) if !shouldCheckout { - t.Error("Expected checkout to be added when custom-agent is specified") + t.Error("Expected checkout to be added when agent file is specified") } }) @@ -353,15 +306,15 @@ func TestCheckoutWithAgent(t *testing.T) { compiler := NewCompiler(false, "", "") workflowData := &WorkflowData{ EngineConfig: &EngineConfig{ - ID: "copilot", - CustomAgent: "/path/to/agent.md", + ID: "copilot", }, + AgentFile: "/path/to/agent.md", Permissions: "permissions:\n issues: read\n", } shouldCheckout := compiler.shouldAddCheckoutStep(workflowData) if !shouldCheckout { - t.Error("Expected checkout to be added when custom-agent is specified, even without contents permission") + t.Error("Expected checkout to be added when agent file is specified, even without contents permission") } }) @@ -376,7 +329,7 @@ func TestCheckoutWithAgent(t *testing.T) { shouldCheckout := compiler.shouldAddCheckoutStep(workflowData) if shouldCheckout { - t.Error("Expected checkout NOT to be added without custom-agent and without contents permission") + t.Error("Expected checkout NOT to be added without agent file and without contents permission") } }) @@ -384,15 +337,15 @@ func TestCheckoutWithAgent(t *testing.T) { compiler := NewCompiler(false, "", "") workflowData := &WorkflowData{ EngineConfig: &EngineConfig{ - ID: "copilot", - CustomAgent: "/path/to/agent.md", + ID: "copilot", }, + AgentFile: "/path/to/agent.md", CustomSteps: "steps:\n - uses: actions/checkout@v4\n", } shouldCheckout := compiler.shouldAddCheckoutStep(workflowData) if shouldCheckout { - t.Error("Expected checkout NOT to be added when custom steps already contain checkout, even with custom-agent") + t.Error("Expected checkout NOT to be added when custom steps already contain checkout, even with agent file") } }) } diff --git a/pkg/workflow/validation.go b/pkg/workflow/validation.go index 2552cc51f3..79f53bbbe3 100644 --- a/pkg/workflow/validation.go +++ b/pkg/workflow/validation.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "path/filepath" "regexp" "strings" "sync" @@ -640,62 +639,19 @@ func (c *Compiler) validateWebSearchSupport(tools map[string]any, engine CodingA } } -// validateAgentFile validates that the custom agent file specified in engine config exists +// validateAgentFile validates that the custom agent file specified in imports exists func (c *Compiler) validateAgentFile(workflowData *WorkflowData, markdownPath string) error { - // Check if custom-agent field is specified in engine config - if workflowData.EngineConfig == nil || workflowData.EngineConfig.CustomAgent == "" { - return nil // No custom agent file specified, no validation needed + // Check if agent file is specified in imports + if workflowData.AgentFile == "" { + return nil // No agent file specified, no validation needed } - agentPath := workflowData.EngineConfig.CustomAgent - validationLog.Printf("Validating custom agent file exists: %s", agentPath) + agentPath := workflowData.AgentFile + validationLog.Printf("Validating agent file exists: %s", agentPath) - // Refuse absolute file paths - if filepath.IsAbs(agentPath) { - formattedErr := console.FormatError(console.CompilerError{ - Position: console.ErrorPosition{ - File: markdownPath, - Line: 1, - Column: 1, - }, - Type: "error", - Message: fmt.Sprintf("custom-agent file path '%s' must be relative, not absolute", agentPath), - }) - return errors.New(formattedErr) - } - - // Resolve agent path - // 1. Try .github/agents/{path} first (default location) - // 2. Fall back to treating as relative to repository root - markdownDir := filepath.Dir(markdownPath) - // Workflow is in .github/workflows, so go up two levels to get to repo root - repoRoot := filepath.Join(markdownDir, "../..") - - var resolvedAgentPath string - var foundInDefaultLocation bool - - // First, try the default .github/agents/ location - defaultAgentPath := filepath.Join(repoRoot, ".github/agents", agentPath) - defaultAgentPath = filepath.Clean(defaultAgentPath) - if _, err := os.Stat(defaultAgentPath); err == nil { - resolvedAgentPath = defaultAgentPath - foundInDefaultLocation = true - validationLog.Printf("Found agent in default location: %s -> %s", agentPath, resolvedAgentPath) - } else { - // Fall back to treating as relative to repository root - resolvedAgentPath = filepath.Join(repoRoot, agentPath) - resolvedAgentPath = filepath.Clean(resolvedAgentPath) - validationLog.Printf("Resolved agent path relative to repo root: %s -> %s", agentPath, resolvedAgentPath) - } - - // Check if the file exists - if _, err := os.Stat(resolvedAgentPath); err != nil { + // Check if the file exists (it should already be resolved by imports processor) + if _, err := os.Stat(agentPath); err != nil { if os.IsNotExist(err) { - var suggestion string - if !foundInDefaultLocation { - // Suggest checking the default location - suggestion = " Tip: Custom agent files are typically stored in .github/agents/." - } formattedErr := console.FormatError(console.CompilerError{ Position: console.ErrorPosition{ File: markdownPath, @@ -703,7 +659,7 @@ func (c *Compiler) validateAgentFile(workflowData *WorkflowData, markdownPath st Column: 1, }, Type: "error", - Message: fmt.Sprintf("custom-agent file '%s' does not exist. Ensure the file exists in the repository and the path is correct.%s", agentPath, suggestion), + Message: fmt.Sprintf("agent file '%s' does not exist. Ensure the file exists in the repository and is properly imported.", agentPath), }) return errors.New(formattedErr) } @@ -715,7 +671,7 @@ func (c *Compiler) validateAgentFile(workflowData *WorkflowData, markdownPath st Column: 1, }, Type: "error", - Message: fmt.Sprintf("failed to access custom-agent file '%s': %v", agentPath, err), + Message: fmt.Sprintf("failed to access agent file '%s': %v", agentPath, err), }) return errors.New(formattedErr) } From 24d0f6832ee1ad8cbfa15677f31aad0090931f1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:36:03 +0000 Subject: [PATCH 4/6] Update documentation for imports-based agent files - Update custom-agents.instructions.md to reflect imports approach - Remove custom-agent references from frontmatter docs - Document .agent.md extension requirement - Document one agent file per workflow limitation - Update example workflows to use imports Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../custom-agents.instructions.md | 27 ++++--- .../docs/reference/frontmatter-full.md | 13 ++-- pkg/parser/frontmatter.go | 4 +- pkg/workflow/schemas/github-workflow.json | 76 +++---------------- 4 files changed, 32 insertions(+), 88 deletions(-) diff --git a/.github/instructions/custom-agents.instructions.md b/.github/instructions/custom-agents.instructions.md index 9a9555501c..700ba7fb29 100644 --- a/.github/instructions/custom-agents.instructions.md +++ b/.github/instructions/custom-agents.instructions.md @@ -28,10 +28,11 @@ Custom agent files can be placed in several locations, each serving a different - **Use case**: Task-specific agents (documentation, testing, refactoring) ### 4. agentic workflow integration -- **Location**: Specified in workflow frontmatter via `engine.custom-agent` field -- **Pattern**: Any markdown file path relative to repository root +- **Location**: Imported via `imports` field in workflow frontmatter +- **Pattern**: Files ending with `.agent.md` (e.g., `my-agent.agent.md`) - **Scope**: Custom agent for specific agentic workflow execution - **Use case**: Workflow-specific agent configuration +- **Important**: Only one agent file (`.agent.md`) is allowed per workflow ## File Format @@ -366,7 +367,7 @@ You are an experienced code reviewer focused on code quality, security, and best ## Integration with gh-aw -The gh-aw (GitHub Agentic Workflows) tool supports custom agent files through the `engine.custom-agent` field in workflow frontmatter. +The gh-aw (GitHub Agentic Workflows) tool supports custom agent files through the `imports` field in workflow frontmatter. Agent files must have the `.agent.md` extension and be imported like other workflow components. ### Configuration @@ -375,7 +376,8 @@ The gh-aw (GitHub Agentic Workflows) tool supports custom agent files through th on: issues engine: id: copilot - custom-agent: .github/agents/my-agent.md +imports: + - .github/agents/my-agent.agent.md --- # My Workflow @@ -393,10 +395,11 @@ Custom agent files are supported by the following engines: ### File Path Resolution -- Paths are relative to the repository root -- Must point to an existing markdown file +- Agent files are imported via the `imports` field +- Must end with `.agent.md` extension +- Only one agent file is allowed per workflow - File is validated during workflow compilation -- Checkout step is automatically added if custom agent is specified +- Checkout step is automatically added if agent file is imported ### Example Workflow with Custom Agent @@ -410,7 +413,8 @@ permissions: issues: write engine: id: copilot - custom-agent: .github/agents/issue-triager.md +imports: + - .github/agents/issue-triager.agent.md tools: github: allowed: @@ -505,10 +509,11 @@ tools: [editFiles, createFile, search, codeSearch] - Ensure `applyTo` is only used in `.instructions.md` files ### Custom Agent File Not Found -- Verify file path is relative to repository root +- Verify agent file is imported in the `imports` field - Ensure file exists and is committed -- Check file extension is `.md` -- Confirm path in `engine.custom-agent` is correct +- Check file extension is `.agent.md` +- Confirm agent file path is correct in imports list +- Remember: only one agent file is allowed per workflow ## References diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index f8cd33abb6..287accc279 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -33,10 +33,13 @@ source: "example-value" # Optional array of workflow specifications to import (similar to @include # directives but defined in frontmatter). Format: owner/repo/path@ref (e.g., -# githubnext/agentics/workflows/shared/common.md@v1.0.0). +# githubnext/agentics/workflows/shared/common.md@v1.0.0). Files ending with +# '.agent.md' are treated as custom agent files (only one agent file allowed +# per workflow). # (optional) imports: [] - # Array of Workflow specification in format owner/repo/path@ref + # Array of Workflow specification in format owner/repo/path@ref. Files with + # '.agent.md' extension are treated as agent configuration files. # Workflow triggers that define when the agentic workflow should run. Supports # standard GitHub Actions trigger events plus special command triggers for @@ -934,12 +937,6 @@ engine: args: [] # Array of strings - # Optional path to a custom agent configuration file. For copilot engine, this is - # passed as --agent flag. For claude and codex engines, the markdown body from the - # agent file is injected as a system prompt. - # (optional) - custom-agent: "example-value" - # MCP server definitions # (optional) mcp-servers: diff --git a/pkg/parser/frontmatter.go b/pkg/parser/frontmatter.go index 728721a559..fd222ca403 100644 --- a/pkg/parser/frontmatter.go +++ b/pkg/parser/frontmatter.go @@ -451,7 +451,7 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD } agentFile = fullPath log.Printf("Found agent file: %s", fullPath) - + // For agent files, only extract markdown content // Extract markdown content from imported file markdownContent, err := processIncludedFileWithVisited(fullPath, sectionName, false, visited) @@ -469,7 +469,7 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD } } } - + // Skip other extractions for agent files continue } diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index 160824c1de..6b93ceff0b 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -983,19 +983,9 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": [ - "created", - "rerequested", - "completed", - "requested_action" - ] + "enum": ["created", "rerequested", "completed", "requested_action"] }, - "default": [ - "created", - "rerequested", - "completed", - "requested_action" - ] + "default": ["created", "rerequested", "completed", "requested_action"] } } }, @@ -1207,13 +1197,7 @@ "type": "string", "enum": ["created", "closed", "opened", "edited", "deleted"] }, - "default": [ - "created", - "closed", - "opened", - "edited", - "deleted" - ] + "default": ["created", "closed", "opened", "edited", "deleted"] } } }, @@ -1231,23 +1215,9 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": [ - "created", - "updated", - "closed", - "reopened", - "edited", - "deleted" - ] + "enum": ["created", "updated", "closed", "reopened", "edited", "deleted"] }, - "default": [ - "created", - "updated", - "closed", - "reopened", - "edited", - "deleted" - ] + "default": ["created", "updated", "closed", "reopened", "edited", "deleted"] } } }, @@ -1260,21 +1230,9 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": [ - "created", - "moved", - "converted", - "edited", - "deleted" - ] + "enum": ["created", "moved", "converted", "edited", "deleted"] }, - "default": [ - "created", - "moved", - "converted", - "edited", - "deleted" - ] + "default": ["created", "moved", "converted", "edited", "deleted"] } } }, @@ -1558,25 +1516,9 @@ "$ref": "#/definitions/types", "items": { "type": "string", - "enum": [ - "published", - "unpublished", - "created", - "edited", - "deleted", - "prereleased", - "released" - ] + "enum": ["published", "unpublished", "created", "edited", "deleted", "prereleased", "released"] }, - "default": [ - "published", - "unpublished", - "created", - "edited", - "deleted", - "prereleased", - "released" - ] + "default": ["published", "unpublished", "created", "edited", "deleted", "prereleased", "released"] } } }, From 2188b18213e54ea62dfe150ef56b80d7117c41a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 01:10:29 +0000 Subject: [PATCH 5/6] Remove .agent.md extension requirement - any .md in .github/agents is an agent Changed agent file detection from checking .agent.md extension to checking if the file is under .github/agents/ directory. This matches the expected behavior where any markdown file under .github/agents is treated as a custom agent file. Changes: - Updated detection logic in ProcessImportsFromFrontmatterWithManifest - Renamed technical-doc-writer.agent.md -> technical-doc-writer.md - Updated all documentation to reflect directory-based detection - Updated schema examples and descriptions - Updated tests to use .md extension Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...oc-writer.agent.md => technical-doc-writer.md} | 0 .../instructions/custom-agents.instructions.md | 15 +++++++-------- .github/workflows/technical-doc-writer.lock.yml | 4 ++-- .github/workflows/technical-doc-writer.md | 2 +- .../content/docs/reference/frontmatter-full.md | 10 +++++----- pkg/parser/frontmatter.go | 4 ++-- pkg/parser/schemas/main_workflow_schema.json | 6 +++--- pkg/workflow/.github/aw/actions-lock.json | 2 +- pkg/workflow/engine_agent_import_test.go | 4 ++-- 9 files changed, 23 insertions(+), 24 deletions(-) rename .github/agents/{technical-doc-writer.agent.md => technical-doc-writer.md} (100%) diff --git a/.github/agents/technical-doc-writer.agent.md b/.github/agents/technical-doc-writer.md similarity index 100% rename from .github/agents/technical-doc-writer.agent.md rename to .github/agents/technical-doc-writer.md diff --git a/.github/instructions/custom-agents.instructions.md b/.github/instructions/custom-agents.instructions.md index 700ba7fb29..9f136eea19 100644 --- a/.github/instructions/custom-agents.instructions.md +++ b/.github/instructions/custom-agents.instructions.md @@ -29,10 +29,10 @@ Custom agent files can be placed in several locations, each serving a different ### 4. agentic workflow integration - **Location**: Imported via `imports` field in workflow frontmatter -- **Pattern**: Files ending with `.agent.md` (e.g., `my-agent.agent.md`) +- **Pattern**: Any markdown files under `.github/agents/` directory - **Scope**: Custom agent for specific agentic workflow execution - **Use case**: Workflow-specific agent configuration -- **Important**: Only one agent file (`.agent.md`) is allowed per workflow +- **Important**: Only one agent file is allowed per workflow ## File Format @@ -367,7 +367,7 @@ You are an experienced code reviewer focused on code quality, security, and best ## Integration with gh-aw -The gh-aw (GitHub Agentic Workflows) tool supports custom agent files through the `imports` field in workflow frontmatter. Agent files must have the `.agent.md` extension and be imported like other workflow components. +The gh-aw (GitHub Agentic Workflows) tool supports custom agent files through the `imports` field in workflow frontmatter. Any markdown files under the `.github/agents/` directory are treated as custom agent files when imported. ### Configuration @@ -377,7 +377,7 @@ on: issues engine: id: copilot imports: - - .github/agents/my-agent.agent.md + - .github/agents/my-agent.md --- # My Workflow @@ -396,7 +396,7 @@ Custom agent files are supported by the following engines: ### File Path Resolution - Agent files are imported via the `imports` field -- Must end with `.agent.md` extension +- Must be markdown files located under `.github/agents/` directory - Only one agent file is allowed per workflow - File is validated during workflow compilation - Checkout step is automatically added if agent file is imported @@ -414,7 +414,7 @@ permissions: engine: id: copilot imports: - - .github/agents/issue-triager.agent.md + - .github/agents/issue-triager.md tools: github: allowed: @@ -510,8 +510,7 @@ tools: [editFiles, createFile, search, codeSearch] ### Custom Agent File Not Found - Verify agent file is imported in the `imports` field -- Ensure file exists and is committed -- Check file extension is `.agent.md` +- Ensure file exists and is committed under `.github/agents/` directory - Confirm agent file path is correct in imports list - Remember: only one agent file is allowed per workflow diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index 2e98eb2d01..54de5b33b7 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -6,7 +6,7 @@ # Resolved workflow manifest: # Imports: # - ../instructions/documentation.instructions.md -# - ../agents/technical-doc-writer.agent.md +# - ../agents/technical-doc-writer.md # # Job Dependency Graph: # ```mermaid @@ -2137,7 +2137,7 @@ jobs: mkdir -p /tmp/gh-aw/agent/ mkdir -p /tmp/gh-aw/cache-memory/ mkdir -p /tmp/gh-aw/.copilot/logs/ - copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --agent /home/runner/work/gh-aw/gh-aw/.github/agents/technical-doc-writer.agent.md --allow-tool 'github(add_reaction)' --allow-tool 'github(get_file_contents)' --allow-tool 'github(get_pull_request)' --allow-tool 'github(issue_read)' --allow-tool 'github(list_commits)' --allow-tool 'github(pull_request_read)' --allow-tool safeoutputs --allow-tool shell --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/agent-stdio.log + copilot --add-dir /tmp/ --add-dir /tmp/gh-aw/ --add-dir /tmp/gh-aw/agent/ --log-level all --log-dir /tmp/gh-aw/.copilot/logs/ --disable-builtin-mcps --agent /home/runner/work/gh-aw/gh-aw/.github/agents/technical-doc-writer.md --allow-tool 'github(add_reaction)' --allow-tool 'github(get_file_contents)' --allow-tool 'github(get_pull_request)' --allow-tool 'github(issue_read)' --allow-tool 'github(list_commits)' --allow-tool 'github(pull_request_read)' --allow-tool safeoutputs --allow-tool shell --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --prompt "$COPILOT_CLI_INSTRUCTION" 2>&1 | tee /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE GH_AW_ASSETS_ALLOWED_EXTS: ".png,.jpg,.jpeg" diff --git a/.github/workflows/technical-doc-writer.md b/.github/workflows/technical-doc-writer.md index 981e860df5..550be931d0 100644 --- a/.github/workflows/technical-doc-writer.md +++ b/.github/workflows/technical-doc-writer.md @@ -19,7 +19,7 @@ network: imports: - ../instructions/documentation.instructions.md - - ../agents/technical-doc-writer.agent.md + - ../agents/technical-doc-writer.md safe-outputs: add-comment: diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 287accc279..ed5c17c389 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -33,13 +33,13 @@ source: "example-value" # Optional array of workflow specifications to import (similar to @include # directives but defined in frontmatter). Format: owner/repo/path@ref (e.g., -# githubnext/agentics/workflows/shared/common.md@v1.0.0). Files ending with -# '.agent.md' are treated as custom agent files (only one agent file allowed -# per workflow). +# githubnext/agentics/workflows/shared/common.md@v1.0.0). Markdown files under +# .github/agents/ directory are treated as custom agent files (only one agent +# file allowed per workflow). # (optional) imports: [] - # Array of Workflow specification in format owner/repo/path@ref. Files with - # '.agent.md' extension are treated as agent configuration files. + # Array of Workflow specification in format owner/repo/path@ref. Markdown + # files under .github/agents/ are treated as agent configuration files. # Workflow triggers that define when the agentic workflow should run. Supports # standard GitHub Actions trigger events plus special command triggers for diff --git a/pkg/parser/frontmatter.go b/pkg/parser/frontmatter.go index fd222ca403..1e32a76961 100644 --- a/pkg/parser/frontmatter.go +++ b/pkg/parser/frontmatter.go @@ -442,8 +442,8 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD // Add to list of processed files (use original importPath for manifest) processedFiles = append(processedFiles, importPath) - // Check if this is a custom agent file (.agent.md) - isAgentFile := strings.HasSuffix(strings.ToLower(fullPath), ".agent.md") + // Check if this is a custom agent file (any markdown file under .github/agents) + isAgentFile := strings.Contains(fullPath, "/.github/agents/") && strings.HasSuffix(strings.ToLower(fullPath), ".md") if isAgentFile { if agentFile != "" { // Multiple agent files found - error diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 1476c1327d..3ad901a6c2 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -22,16 +22,16 @@ }, "imports": { "type": "array", - "description": "Optional array of workflow specifications to import (similar to @include directives but defined in frontmatter). Format: owner/repo/path@ref (e.g., githubnext/agentics/workflows/shared/common.md@v1.0.0). Files ending with '.agent.md' under .github/agents are treated as custom agent files and only one agent file is allowed per workflow.", + "description": "Optional array of workflow specifications to import (similar to @include directives but defined in frontmatter). Format: owner/repo/path@ref (e.g., githubnext/agentics/workflows/shared/common.md@v1.0.0). Any markdown files under .github/agents directory are treated as custom agent files and only one agent file is allowed per workflow.", "items": { "type": "string", - "description": "Workflow specification in format owner/repo/path@ref. Files with '.agent.md' extension are treated as agent configuration files." + "description": "Workflow specification in format owner/repo/path@ref. Markdown files under .github/agents/ are treated as agent configuration files." }, "examples": [ ["shared/jqschema.md", "shared/reporting.md"], ["shared/mcp/gh-aw.md", "shared/jqschema.md", "shared/reporting.md"], ["../instructions/documentation.instructions.md"], - [".github/agents/my-agent.agent.md"] + [".github/agents/my-agent.md"] ] }, "on": { diff --git a/pkg/workflow/.github/aw/actions-lock.json b/pkg/workflow/.github/aw/actions-lock.json index 0fdef00a20..9f773bc229 100644 --- a/pkg/workflow/.github/aw/actions-lock.json +++ b/pkg/workflow/.github/aw/actions-lock.json @@ -11,4 +11,4 @@ "sha": "2028fbc5c25fe9cf00d9f06a71cc4710d4507903" } } -} \ No newline at end of file +} diff --git a/pkg/workflow/engine_agent_import_test.go b/pkg/workflow/engine_agent_import_test.go index 2c0eca1838..241021d0c6 100644 --- a/pkg/workflow/engine_agent_import_test.go +++ b/pkg/workflow/engine_agent_import_test.go @@ -214,7 +214,7 @@ title: Test Agent This is a test agent file. ` - validAgentFilePath := filepath.Join(agentsDir, "valid-agent.agent.md") + validAgentFilePath := filepath.Join(agentsDir, "valid-agent.md") if err := os.WriteFile(validAgentFilePath, []byte(agentContent), 0644); err != nil { t.Fatalf("Failed to create valid agent file: %v", err) } @@ -243,7 +243,7 @@ This is a test agent file. EngineConfig: &EngineConfig{ ID: "copilot", }, - AgentFile: filepath.Join(agentsDir, "nonexistent.agent.md"), + AgentFile: filepath.Join(agentsDir, "nonexistent.md"), } workflowPath := filepath.Join(workflowsDir, "test.md") From 02dc86778354f7541b5babdc8dfd33c6e3d94189 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 31 Oct 2025 02:55:23 +0000 Subject: [PATCH 6/6] Add changeset [skip-ci] --- .changeset/minor-imports-based-agent-files.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/minor-imports-based-agent-files.md diff --git a/.changeset/minor-imports-based-agent-files.md b/.changeset/minor-imports-based-agent-files.md new file mode 100644 index 0000000000..908812d830 --- /dev/null +++ b/.changeset/minor-imports-based-agent-files.md @@ -0,0 +1,5 @@ +--- +"gh-aw": minor +--- + +Replace engine.custom-agent field with imports-based agent files