Skip to content
Merged
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ refactor: extract context packing logic
test: add coverage for guardian
```

### Commit Signing

- Signed commits are required in this repo.
- Git is configured for SSH signing with `commit.gpgsign=true` and the user's
SSH signing key.
- In sandboxed agent sessions, `git commit` may fail even when the key is
unlocked because the sandbox cannot access `SSH_AUTH_SOCK`.
- When that happens, run `git commit` outside the sandbox or with an
unsandboxed/escalated execution path so git can talk to the host SSH agent.

### Code Style

- `gofmt` and `go vet` are mandatory (enforced by CI)
Expand Down
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ only the human author. Hooks are auto-installed by `make setup` via
make hooks
```

## Commit signing

Signed commits are required in this repo.

- Git is configured for SSH commit signing.
- In sandboxed agent sessions, `git commit` can fail even when the signing key
is unlocked because the sandbox cannot access the host `SSH_AUTH_SOCK`.
- When signing is required, run `git commit` outside the sandbox or through an
unsandboxed/escalated execution path.

## Pull request checklist

Before requesting review:
Expand Down
46 changes: 44 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,37 @@ hawk skills install go-review # Install from GitHub
hawk skills audit # Security scan installed skills
```

### Permission System
### Permission Center

hawk now exposes one visible permission command center in chat:

```text
/permissions
/permissions tier <scout|builder|operator|autonomous>
/permissions sandbox <strict|workspace|off>
/permissions mode <default|edits|bypass|dontask|plan>
/permissions allow <rule>
/permissions deny <rule>
/permissions rules
/permissions reset
/permissions save [project|global]
```

The model is:

hawk asks before running dangerous tools. Auto-mode learns from your decisions, with emergency killswitch support.
- `Tier` controls autonomy:
- `Scout`
- `Builder`
- `Operator`
- `Autonomous`
- `Sandbox` controls the execution boundary:
- `strict`
- `workspace`
- `off`
- `Rules` control explicit allow/deny exceptions.

For normal chat usage, `/permissions` is the main control surface. Older
permission chat commands have been removed in favor of this single flow.

### MCP & LSP Support

Expand Down Expand Up @@ -170,11 +198,25 @@ hawk -c # Continue latest session
hawk --provider openai --model gpt-4o # Override provider
```

### Permission Examples

```bash
# Inside the TUI
/permissions
/permissions tier builder
/permissions sandbox workspace
/permissions mode plan
/permissions allow Bash(git:*)
/permissions deny Bash(rm -rf *)
/permissions save project
```

### Non-Interactive Mode

```bash
hawk -p "explain this repo" # Print response, exit
hawk -p "fix tests" --allowed-tools "Bash(go test:*) Edit Read"
hawk -p "review this repo" --permission-mode plan --sandbox workspace
hawk exec "refactor auth module" # Full engine, non-interactive
hawk exec --auto full "add error handling" # Full autonomy
hawk exec --worktree "add rate limiting" # Isolated branch
Expand Down
4 changes: 2 additions & 2 deletions cmd/autocomplete.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,13 @@ func (ac *Autocompleter) completeFlags(prefix string) []Suggestion {
{"--max-budget-usd", "Maximum API spend"},
{"--system-prompt", "System prompt to use"},
{"--output-format", "Output format"},
{"--sandbox", "Sandbox mode"},
{"--sandbox", "Permission sandbox"},
{"--auto-commit", "Auto-commit changes"},
{"--watch", "Watch for file changes"},
{"--vibe", "Vibe coding mode"},
{"--power", "Power level 1-10"},
{"--timeout", "Time budget"},
{"--permission-mode", "Permission mode"},
{"--permission-mode", "Advanced permission mode"},
{"--session-id", "Session ID"},
}

Expand Down
32 changes: 16 additions & 16 deletions cmd/autonomy_tiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/charmbracelet/lipgloss"
)

// Four container autonomy tiers (InspectEditRunTrust).
// Four container autonomy tiers (ScoutBuilderOperatorAutonomous).
var containerAutonomyTiers = []engine.AutonomyLevel{
engine.AutonomyBasic,
engine.AutonomySemi,
Expand All @@ -17,10 +17,10 @@ var containerAutonomyTiers = []engine.AutonomyLevel{
}

var containerAutonomyTierNames = []string{
"Inspect",
"Edit",
"Run",
"Trust",
"Scout",
"Builder",
"Operator",
"Autonomous",
}

// DefaultContainerAutonomy is the tier applied when the sandbox becomes ready.
Expand All @@ -32,7 +32,7 @@ func autonomyTierName(level engine.AutonomyLevel) string {
return containerAutonomyTierNames[i]
}
}
return "Inspect"
return "Builder"
}

func autonomyTierIndex(level engine.AutonomyLevel) int {
Expand All @@ -41,7 +41,7 @@ func autonomyTierIndex(level engine.AutonomyLevel) int {
return i
}
}
return 1 // default Edit
return 1 // default Builder
}

func nextAutonomyTier(level engine.AutonomyLevel) engine.AutonomyLevel {
Expand All @@ -52,15 +52,15 @@ func nextAutonomyTier(level engine.AutonomyLevel) engine.AutonomyLevel {
func autonomyTierDescription(level engine.AutonomyLevel) string {
switch level {
case engine.AutonomyBasic:
return "Look only — edits & shell ask first"
return "Explore only — edits and commands ask first"
case engine.AutonomySemi:
return "Auto edits — shell asks first"
return "File changes auto-approve — commands ask first"
case engine.AutonomyFull:
return "Auto shell — risky ops ask first"
return "Commands auto-run — risky actions ask first"
case engine.AutonomyYOLO:
return "Few askstrust this session"
return "Minimal promptsonly the highest-risk actions stop"
default:
return "Auto edits — shell asks first"
return "File changes auto-approve — commands ask first"
}
}

Expand Down Expand Up @@ -98,13 +98,13 @@ func formatSandboxReadyAutonomyMessage(level engine.AutonomyLevel) string {

func autonomyLevelForTierName(name string) engine.AutonomyLevel {
switch strings.TrimSpace(name) {
case "Inspect":
case "Scout":
return engine.AutonomyBasic
case "Edit":
case "Builder":
return engine.AutonomySemi
case "Run":
case "Operator":
return engine.AutonomyFull
case "Trust":
case "Autonomous":
return engine.AutonomyYOLO
default:
return DefaultContainerAutonomy
Expand Down
10 changes: 5 additions & 5 deletions cmd/autonomy_tiers_copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ func TestAutonomyTierDescriptions_PlainLanguage(t *testing.T) {
level engine.AutonomyLevel
need []string
}{
{engine.AutonomyBasic, []string{"Look only", "shell ask"}},
{engine.AutonomySemi, []string{"Auto edits", "shell asks"}},
{engine.AutonomyFull, []string{"Auto shell", "risky"}},
{engine.AutonomyYOLO, []string{"Few asks", "trust"}},
{engine.AutonomyBasic, []string{"Explore only", "commands ask"}},
{engine.AutonomySemi, []string{"File changes auto-approve", "commands ask"}},
{engine.AutonomyFull, []string{"Commands auto-run", "risky"}},
{engine.AutonomyYOLO, []string{"Minimal prompts", "highest-risk"}},
}
for _, tc := range cases {
desc := autonomyTierDescription(tc.level)
Expand All @@ -35,7 +35,7 @@ func TestFormatAutonomyTierMessage_NoArrowJargon(t *testing.T) {
if len(msg) > 120 {
t.Fatalf("message too long (%d chars): %q", len(msg), msg)
}
if !strings.Contains(msg, "Run") {
if !strings.Contains(msg, "Operator") {
t.Fatalf("expected tier name in message, got %q", msg)
}
}
22 changes: 11 additions & 11 deletions cmd/autonomy_tiers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,32 @@ import (
)

func TestAutonomyTierNames(t *testing.T) {
if got := autonomyTierName(engine.AutonomyBasic); got != "Inspect" {
t.Fatalf("Basic = %q, want Inspect", got)
if got := autonomyTierName(engine.AutonomyBasic); got != "Scout" {
t.Fatalf("Basic = %q, want Scout", got)
}
if got := autonomyTierName(engine.AutonomySemi); got != "Edit" {
t.Fatalf("Semi = %q, want Edit", got)
if got := autonomyTierName(engine.AutonomySemi); got != "Builder" {
t.Fatalf("Semi = %q, want Builder", got)
}
if got := autonomyTierName(engine.AutonomyFull); got != "Run" {
t.Fatalf("Full = %q, want Run", got)
if got := autonomyTierName(engine.AutonomyFull); got != "Operator" {
t.Fatalf("Full = %q, want Operator", got)
}
if got := autonomyTierName(engine.AutonomyYOLO); got != "Trust" {
t.Fatalf("YOLO = %q, want Trust", got)
if got := autonomyTierName(engine.AutonomyYOLO); got != "Autonomous" {
t.Fatalf("YOLO = %q, want Autonomous", got)
}
}

func TestNextAutonomyTier(t *testing.T) {
if nextAutonomyTier(engine.AutonomyYOLO) != engine.AutonomyBasic {
t.Fatal("expected Trust → Inspect wrap")
t.Fatal("expected Autonomous -> Scout wrap")
}
if nextAutonomyTier(engine.AutonomySemi) != engine.AutonomyFull {
t.Fatal("expected Edit → Run")
t.Fatal("expected Builder -> Operator")
}
}

func TestAutonomyFromSettings(t *testing.T) {
if autonomyFromSettings(2) != engine.AutonomySemi {
t.Fatal("settings autonomy 2 should map to Edit/Semi")
t.Fatal("settings autonomy 2 should map to Builder/Semi")
}
if autonomyFromSettings(0) != 0 {
t.Fatal("settings 0 should leave unset")
Expand Down
17 changes: 17 additions & 0 deletions cmd/cascade_diag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cmd

import (
"testing"

hawkbranch "github.com/GrayCodeAI/hawk/internal/engine/branching"
"github.com/GrayCodeAI/hawk/internal/provider/routing"
)

func TestCascadeSelectsForOpenCodeGoHi(t *testing.T) {
roles := routing.DefaultRoles("opencodego/minimax-m2.5")
t.Logf("cheapest=%s commit=%s", routing.CheapestForProvider("opencodego", "minimax-m2.5"), roles.Commit)
cr := hawkbranch.NewCascadeRouter("opencodego/minimax-m2.5", roles)
cr.Enabled = true
got := cr.SelectModel("Hi", "opencodego/minimax-m2.5", "")
t.Logf("selected=%s", got)
}
Loading
Loading