diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 95cadd5..9354484 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -22,9 +22,10 @@ jobs: uses: actions/checkout@v4 - name: Check conventional commit title + env: + PR_TITLE: ${{ github.event.pull_request.title }} run: | - title="${{ github.event.pull_request.title }}" - if ! echo "$title" | grep -qP '^(\p{So} )?(feat|fix|docs|chore|ci|refactor|test|perf|build|style|revert)(\(.+\))?!?: .+'; then + if ! echo "$PR_TITLE" | grep -qP '^(\p{So}\x{FE0F}? )?(feat|fix|docs|chore|ci|refactor|test|perf|build|style|revert)(\(.+\))?!?: .+'; then echo "::error::PR title must follow: [emoji] type: description" echo "Valid types: feat, fix, docs, chore, ci, refactor, test, perf, build, style, revert" echo "Examples: '✨ feat: Add new feature' or 'fix: Correct bug'" diff --git a/README.md b/README.md index 22f0f1d..4eab216 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ Sweeper dispatches automated Claude sub-agents. To stay within [Anthropic's usag - **Rate limiting**: agents are dispatched with a configurable delay between each (default 2s, `--rate-limit`) - **Concurrency cap**: hard maximum of 5 parallel agents regardless of flags -- **Scoped permissions**: sub-agents use `--allowedTools` with a narrow whitelist (Read, Write, Edit, Glob, Grep, limited Bash) instead of `--dangerously-skip-permissions` +- **Skip permissions**: sub-agents use `--dangerously-skip-permissions` for non-interactive automated operation - **Backoff**: exponential delay between retry rounds (5s, 10s, 20s, ... capped at 60s) - **Agent identification**: all prompts identify the sub-agent as an automated tool with human oversight diff --git a/cmd/run.go b/cmd/run.go index 9645f57..1210716 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -22,7 +22,6 @@ func newRunCmd() *cobra.Command { var dryRun bool var maxRounds int var staleThreshold int - var allowedTools []string var useVM bool var vmName string var vmJcard string @@ -47,15 +46,10 @@ Examples: if clamped != concurrency { fmt.Printf("Concurrency clamped to %d (max %d)\n", clamped, config.MaxConcurrency) } - tools := append([]string{}, config.DefaultAllowedTools...) - if len(allowedTools) > 0 { - tools = append(tools, allowedTools...) - } cfg := config.Config{ TargetDir: targetDir, Concurrency: clamped, RateLimit: rateLimit, - AllowedTools: tools, TelemetryDir: ".sweeper/telemetry", DryRun: dryRun, NoTapes: noTapes, @@ -156,7 +150,6 @@ Examples: return nil }, } - cmd.Flags().StringSliceVar(&allowedTools, "allowed-tools", nil, "additional tools for sub-agents (e.g. 'Bash(npm:*),Bash(cargo:*)')") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be fixed without making changes") cmd.Flags().IntVar(&maxRounds, "max-rounds", 1, "maximum retry rounds (1 = single pass)") cmd.Flags().IntVar(&staleThreshold, "stale-threshold", 2, "consecutive non-improving rounds before exploration mode") diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 0899a4b..26a335b 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -77,14 +77,13 @@ func New(cfg config.Config, opts ...Option) *Agent { if p, err := provider.Get(provName); err == nil { a.providerKind = p.Kind a.executor = p.NewExec(provider.Config{ - Model: cfg.ProviderModel, - APIBase: cfg.ProviderAPI, - AllowedTools: cfg.AllowedTools, + Model: cfg.ProviderModel, + APIBase: cfg.ProviderAPI, }) } else { fmt.Printf("Warning: unknown provider %q, falling back to claude\n", provName) a.providerKind = provider.KindCLI - a.executor = worker.NewClaudeExecutor(cfg.AllowedTools) + a.executor = worker.NewClaudeExecutor() } for _, opt := range opts { diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index f72ad73..34b8ee5 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -903,7 +903,6 @@ func TestNewAgentFallbackOnUnknownProvider(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "nonexistent-provider-xyz", - AllowedTools: []string{"Read"}, } a := New(cfg) // Should fall back to KindCLI and Claude executor without panicking @@ -921,7 +920,6 @@ func TestNewAgentWithProviderFromRegistry(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "claude", - AllowedTools: []string{"Read"}, } a := New(cfg) if a.providerKind != provider.KindCLI { @@ -935,7 +933,6 @@ func TestNewAgentEmptyProviderDefaultsToClaude(t *testing.T) { Concurrency: 1, TelemetryDir: t.TempDir(), Provider: "", - AllowedTools: []string{"Read"}, } a := New(cfg) if a.providerKind != provider.KindCLI { diff --git a/pkg/config/config.go b/pkg/config/config.go index f977113..3d7fbb2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,21 +2,10 @@ package config import "time" -// DefaultAllowedTools is the baseline set of tools sweeper agents can use. -// Users can extend this via --allowed-tools without reverting to a blanket bypass. -var DefaultAllowedTools = []string{ - "Read", - "Write", - "Edit", - "Glob", - "Grep", -} - type Config struct { TargetDir string Concurrency int RateLimit time.Duration // minimum delay between agent dispatches - AllowedTools []string // tools sub-agents are permitted to use TelemetryDir string DryRun bool NoTapes bool @@ -41,7 +30,6 @@ func Default() Config { TargetDir: ".", Concurrency: 2, RateLimit: 2 * time.Second, - AllowedTools: append([]string{}, DefaultAllowedTools...), TelemetryDir: ".sweeper/telemetry", DryRun: false, MaxRounds: 1, diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 922672b..ae3080d 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -51,18 +51,6 @@ func TestClampConcurrency(t *testing.T) { } } -func TestDefaultAllowedTools(t *testing.T) { - cfg := Default() - if len(cfg.AllowedTools) != len(DefaultAllowedTools) { - t.Errorf("expected %d default allowed tools, got %d", len(DefaultAllowedTools), len(cfg.AllowedTools)) - } - // Verify it's a copy, not a shared slice - cfg.AllowedTools = append(cfg.AllowedTools, "Bash(npm:*)") - if len(Default().AllowedTools) != len(DefaultAllowedTools) { - t.Error("modifying config should not mutate DefaultAllowedTools") - } -} - func TestDefaultConfigHasVMDisabled(t *testing.T) { cfg := Default() if cfg.VM { diff --git a/pkg/provider/claude.go b/pkg/provider/claude.go index 0ca78a1..955fc65 100644 --- a/pkg/provider/claude.go +++ b/pkg/provider/claude.go @@ -7,7 +7,7 @@ func init() { Name: "claude", Kind: KindCLI, NewExec: func(cfg Config) worker.Executor { - return worker.NewClaudeExecutor(cfg.AllowedTools) + return worker.NewClaudeExecutor() }, }) } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 99565a5..2e1d192 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -13,10 +13,9 @@ const ( // Config holds provider-specific settings passed when constructing an executor. type Config struct { - Model string // model name (e.g. "qwen2.5-coder:7b") - APIBase string // base URL for API providers (e.g. "http://localhost:11434") - AllowedTools []string // tools for CLI harnesses - ExtraArgs []string // additional CLI arguments + Model string // model name (e.g. "qwen2.5-coder:7b") + APIBase string // base URL for API providers (e.g. "http://localhost:11434") + ExtraArgs []string // additional CLI arguments } // Provider describes a registered AI backend. diff --git a/pkg/worker/claude.go b/pkg/worker/claude.go index d4545ea..db425f0 100644 --- a/pkg/worker/claude.go +++ b/pkg/worker/claude.go @@ -3,15 +3,12 @@ package worker import ( "context" "os/exec" - "strings" "time" ) -// NewClaudeExecutor returns an Executor that invokes claude with the given -// allowed tools. This lets callers configure which tools sub-agents can use -// without reverting to --dangerously-skip-permissions. -func NewClaudeExecutor(allowedTools []string) Executor { - toolsArg := strings.Join(allowedTools, ",") +// NewClaudeExecutor returns an Executor that invokes claude with +// --dangerously-skip-permissions for non-interactive sub-agent use. +func NewClaudeExecutor() Executor { return func(ctx context.Context, task Task) Result { start := time.Now() prompt := task.Prompt @@ -20,7 +17,7 @@ func NewClaudeExecutor(allowedTools []string) Executor { } cmd := exec.CommandContext(ctx, "claude", "--print", - "--allowedTools", toolsArg, + "--dangerously-skip-permissions", prompt, ) cmd.Dir = task.Dir diff --git a/pkg/worker/claude_test.go b/pkg/worker/claude_test.go index 89a65df..2c4ce21 100644 --- a/pkg/worker/claude_test.go +++ b/pkg/worker/claude_test.go @@ -51,7 +51,7 @@ func TestClaudeExecutorUsesTaskPrompt(t *testing.T) { dir := t.TempDir() fakeClaude := filepath.Join(dir, "claude") // Script echoes the prompt argument (last arg) so we can verify it - // Args: --print --allowedTools + // Args: --print --dangerously-skip-permissions if err := os.WriteFile(fakeClaude, []byte("#!/bin/sh\necho \"$@\""), 0o755); err != nil { t.Fatal(err) } @@ -67,7 +67,7 @@ func TestClaudeExecutorUsesTaskPrompt(t *testing.T) { }, Prompt: customPrompt, } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if !result.Success { t.Fatalf("expected success, got error: %s", result.Error) } @@ -93,7 +93,7 @@ func TestClaudeExecutorFallsBackToBuildPrompt(t *testing.T) { }, // Prompt intentionally left empty } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if !result.Success { t.Fatalf("expected success, got error: %s", result.Error) } @@ -119,7 +119,7 @@ func TestClaudeExecutorSuccess(t *testing.T) { {File: "test.go", Line: 1, Message: "unused var", Linter: "revive"}, }, } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if !result.Success { t.Errorf("expected success, got error: %s", result.Error) } @@ -147,7 +147,7 @@ func TestClaudeExecutorError(t *testing.T) { {File: "test.go", Line: 1, Message: "unused var", Linter: "revive"}, }, } - result := NewClaudeExecutor([]string{"Read", "Edit"})(context.Background(), task) + result := NewClaudeExecutor()(context.Background(), task) if result.Success { t.Error("expected failure") }