Skip to content
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9253,6 +9253,11 @@
"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."
},
"bare": {
"type": "boolean",
"description": "When true, disables automatic loading of context and custom instructions by the AI engine. The engine-specific flag depends on the engine: copilot uses --no-custom-instructions (suppresses .github/AGENTS.md and user-level custom instructions), claude uses --bare (suppresses CLAUDE.md memory files), codex uses --no-system-prompt (suppresses the default system prompt), gemini sets GEMINI_SYSTEM_MD=/dev/null (overrides the built-in system prompt with an empty one). Defaults to false.",
"default": false
}
Comment on lines +9257 to 9261
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema adds engine.bare only to the { id: ... } engine config shape. Inline engine definitions (engine: { runtime: { ... } }) still cannot specify bare, and the compiler currently ignores bare for inline definitions as well. If bare mode should be supported uniformly, add bare to the inline-engine schema branch too (and ensure the compiler parses it). Otherwise, consider explicitly documenting this limitation in the schema description to avoid confusing users.

Copilot uses AI. Check for mistakes.
},
"required": ["id"],
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str
// This format is compatible with the log parser which expects either JSON array or JSONL
claudeArgs = append(claudeArgs, "--output-format", "stream-json")

// Add --bare when bare mode is enabled to suppress automatic loading of memory
// files (CLAUDE.md, ~/.claude/) and other context injections.
if workflowData.EngineConfig != nil && workflowData.EngineConfig.Bare {
claudeLog.Print("Bare mode enabled: adding --bare")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --bare flag placement looks correct — added before custom args, which is the right order for Claude CLI. One minor nit: the log message says "adding --bare" but it might be clearer to say "suppressing CLAUDE.md context via --bare" to match the intent documented in the schema description.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice clean implementation! The nil-guard workflowData.EngineConfig != nil is consistent with the pattern used for Args below. One minor observation: the debug log here uses claudeLog.Print (without f), which is fine for a static string. The pattern is consistent across engines. ✅

claudeArgs = append(claudeArgs, "--bare")
}

// Add custom args from engine configuration before the prompt
if workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Args) > 0 {
claudeArgs = append(claudeArgs, workflowData.EngineConfig.Args...)
Expand Down
16 changes: 12 additions & 4 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri
customArgsParam += customArgsParamSb.String()
}

// Build bare mode parameter: --no-system-prompt is a global Codex flag placed before the
// "exec" subcommand to suppress loading of the default system prompt/instructions.
bareGlobalParam := ""
if workflowData.EngineConfig != nil && workflowData.EngineConfig.Bare {
codexEngineLog.Print("Bare mode enabled: adding --no-system-prompt")
bareGlobalParam = "--no-system-prompt "
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bareGlobalParam string uses a trailing space as a separator ("--no-system-prompt "). This is a bit fragile — if the format string ever changes, a stray space or a double-space could appear. Consider appending "--no-system-prompt" as a separate element in a slice, consistent with how claudeArgs handles this in claude_engine.go.

}

// Build the Codex command
// Determine which command to use
var commandName string
Expand All @@ -201,8 +209,8 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri
commandName = "codex"
}

codexCommand := fmt.Sprintf("%s %sexec%s%s%s%s\"$INSTRUCTION\"",
commandName, modelParam, webSearchParam, webFetchParam, fullAutoParam, customArgsParam)
codexCommand := fmt.Sprintf("%s %s%sexec%s%s%s%s\"$INSTRUCTION\"",
commandName, modelParam, bareGlobalParam, webSearchParam, webFetchParam, fullAutoParam, customArgsParam)

// Build the full command with agent file handling and AWF wrapping if enabled
var command string
Expand Down Expand Up @@ -268,13 +276,13 @@ touch %s
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"
%s %sexec%s%s%s%s"$INSTRUCTION" 2>&1 | tee %s`, AgentStepSummaryPath, agentPath, commandName, modelParam, webSearchParam, webFetchParam, fullAutoParam, customArgsParam, logFile)
%s 2>&1 | tee %s`, AgentStepSummaryPath, agentPath, codexCommand, logFile)
} else {
command = fmt.Sprintf(`set -o pipefail
touch %s
INSTRUCTION="$(cat "$GH_AW_PROMPT")"
mkdir -p "$CODEX_HOME/logs"
%s %sexec%s%s%s%s"$INSTRUCTION" 2>&1 | tee %s`, AgentStepSummaryPath, commandName, modelParam, webSearchParam, webFetchParam, fullAutoParam, customArgsParam, logFile)
%s 2>&1 | tee %s`, AgentStepSummaryPath, codexCommand, logFile)
}
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/copilot_engine_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st
copilotArgs = append(copilotArgs, "--allow-all-paths")
}

// Add --no-custom-instructions when bare mode is enabled to suppress automatic
// loading of custom instructions from .github/AGENTS.md and user-level configs.
if workflowData.EngineConfig != nil && workflowData.EngineConfig.Bare {
copilotExecLog.Print("Bare mode enabled: adding --no-custom-instructions")
copilotArgs = append(copilotArgs, "--no-custom-instructions")
}

// Add custom args from engine configuration before the prompt
if workflowData.EngineConfig != nil && len(workflowData.EngineConfig.Args) > 0 {
copilotArgs = append(copilotArgs, workflowData.EngineConfig.Args...)
Expand Down
9 changes: 9 additions & 0 deletions pkg/workflow/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type EngineConfig struct {
Args []string
Agent string // Agent identifier for copilot --agent flag (copilot engine only)
APITarget string // Custom API endpoint hostname (e.g., "api.acme.ghe.com" or "api.enterprise.githubcopilot.com")
Bare bool // When true, disables automatic loading of context/instructions (copilot: --no-custom-instructions, claude: --bare, codex: --no-system-prompt, gemini: GEMINI_SYSTEM_MD=/dev/null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment on Bare is comprehensive and cross-references all four engines — good for maintainers discovering this field later. Consider also adding a reference to the frontmatter schema so readers can find the user-facing docs from the struct definition.

// TokenWeights provides custom model cost data for effective token computation.
// When set, overrides or extends the built-in model_multipliers.json values.
TokenWeights *types.TokenWeights
Expand Down Expand Up @@ -288,6 +289,14 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng
}
}

// Extract optional 'bare' field (disable automatic context/instruction loading)
if bare, hasBare := engineObj["bare"]; hasBare {
if bareBool, ok := bare.(bool); ok {
config.Bare = bareBool
engineLog.Printf("Extracted bare mode: %v", config.Bare)
}
}
Comment on lines +300 to +306
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

engine.bare is only extracted in the non-inline (engine.id) parsing path. When engine.runtime is used (inline engine definition), ExtractEngineConfig returns early before reaching this block, so bare: true will be silently ignored for inline definitions. If bare mode is intended to work for inline engines too, parse/apply bare before the early return (or in the runtime branch as well) and add a regression test for engine: { runtime: {…}, bare: true }. If it is intentionally unsupported for inline engines, it should be documented and ideally rejected via schema validation to avoid a silent no-op.

Copilot uses AI. Check for mistakes.

// Extract optional 'token-weights' field (custom model cost data)
if tokenWeightsRaw, hasTokenWeights := engineObj["token-weights"]; hasTokenWeights {
if tw := parseEngineTokenWeights(tokenWeightsRaw); tw != nil {
Expand Down
149 changes: 149 additions & 0 deletions pkg/workflow/engine_args_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,152 @@ This is a test workflow to verify codex engine args injection.
t.Error("Expected --custom-flag to come before $INSTRUCTION in compiled YAML")
}
}

func TestEngineBareModeCopilotIntegration(t *testing.T) {
tmpDir := testutil.TempDir(t, "test-*")

workflowContent := `---
on: workflow_dispatch
engine:
id: copilot
bare: true
---

# Test Bare Mode Copilot
`

workflowPath := filepath.Join(tmpDir, "test-workflow.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

compiler := NewCompiler()
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}

content, err := os.ReadFile(filepath.Join(tmpDir, "test-workflow.lock.yml"))
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
result := string(content)

if !strings.Contains(result, "--no-custom-instructions") {
t.Errorf("Expected --no-custom-instructions in compiled output when bare=true, got:\n%s", result)
}
}

func TestEngineBareModeClaudeIntegration(t *testing.T) {
tmpDir := testutil.TempDir(t, "test-*")

workflowContent := `---
on: workflow_dispatch
engine:
id: claude
bare: true
---

# Test Bare Mode Claude
`

workflowPath := filepath.Join(tmpDir, "test-workflow.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

compiler := NewCompiler()
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}

content, err := os.ReadFile(filepath.Join(tmpDir, "test-workflow.lock.yml"))
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
result := string(content)

if !strings.Contains(result, "--bare") {
t.Errorf("Expected --bare in compiled output when bare=true, got:\n%s", result)
}
}

func TestEngineBareModeCodexIntegration(t *testing.T) {
tmpDir := testutil.TempDir(t, "test-*")

workflowContent := `---
on: workflow_dispatch
engine:
id: codex
bare: true
---

# Test Bare Mode Codex
`

workflowPath := filepath.Join(tmpDir, "test-workflow.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

compiler := NewCompiler()
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}

content, err := os.ReadFile(filepath.Join(tmpDir, "test-workflow.lock.yml"))
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
result := string(content)

if !strings.Contains(result, "--no-system-prompt") {
t.Errorf("Expected --no-system-prompt in compiled output when bare=true, got:\n%s", result)
}

// Verify --no-system-prompt appears before exec
noSysPromptIdx := strings.Index(result, "--no-system-prompt")
execIdx := strings.Index(result, "exec")
if noSysPromptIdx == -1 || execIdx == -1 {
t.Fatal("Could not find both --no-system-prompt and exec in compiled YAML")
}
if noSysPromptIdx > execIdx {
t.Error("Expected --no-system-prompt to come before 'exec' subcommand")
}
}

func TestEngineBareModeGeminiIntegration(t *testing.T) {
tmpDir := testutil.TempDir(t, "test-*")

workflowContent := `---
on: workflow_dispatch
engine:
id: gemini
bare: true
---

# Test Bare Mode Gemini
`

workflowPath := filepath.Join(tmpDir, "test-workflow.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

compiler := NewCompiler()
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}

content, err := os.ReadFile(filepath.Join(tmpDir, "test-workflow.lock.yml"))
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}
result := string(content)

if !strings.Contains(result, "GEMINI_SYSTEM_MD") {
t.Errorf("Expected GEMINI_SYSTEM_MD in compiled output when bare=true, got:\n%s", result)
}
if !strings.Contains(result, "/dev/null") {
t.Errorf("Expected /dev/null in GEMINI_SYSTEM_MD value, got:\n%s", result)
}
}
Loading
Loading