Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 0 additions & 7 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down
7 changes: 3 additions & 4 deletions pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 0 additions & 3 deletions pkg/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
12 changes: 0 additions & 12 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
12 changes: 0 additions & 12 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ func init() {
Name: "claude",
Kind: KindCLI,
NewExec: func(cfg Config) worker.Executor {
return worker.NewClaudeExecutor(cfg.AllowedTools)
return worker.NewClaudeExecutor()
},
})
}
7 changes: 3 additions & 4 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 4 additions & 7 deletions pkg/worker/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions pkg/worker/claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tools> <prompt>
// Args: --print --dangerously-skip-permissions <prompt>
if err := os.WriteFile(fakeClaude, []byte("#!/bin/sh\necho \"$@\""), 0o755); err != nil {
t.Fatal(err)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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")
}
Expand Down
Loading