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
12 changes: 8 additions & 4 deletions internal/engine/conversation_adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func TestExportMessagesFormat(t *testing.T) {
func TestTokenEstimateRoughAccuracy(t *testing.T) {
cm := NewConversationManager(ConversationConfig{})

// estimateTokens uses len(text)/4
// estimateTokens uses the BPE-based tok tokenizer.
content := "this is a test string that should produce some tokens"
cm.AddMessage("user", content)

Expand All @@ -283,9 +283,13 @@ func TestTokenEstimateRoughAccuracy(t *testing.T) {
t.Errorf("expected token estimate %d, got %d", expected, got)
}

// Verify the formula: roughly 1 token per 4 characters.
if got != len(content)/4 {
t.Errorf("token estimate should be len/4 = %d, got %d", len(content)/4, got)
// The estimate must be non-zero and stay within a sane range for English
// text (BPE typically lands between ~3 and ~7 chars per token).
if got <= 0 {
t.Errorf("token estimate should be positive, got %d", got)
}
if cpt := float64(len(content)) / float64(got); cpt < 3 || cpt > 7 {
t.Errorf("chars-per-token %0.2f outside expected range (3-7) for %d chars and %d tokens", cpt, len(content), got)
}
}

Expand Down
7 changes: 5 additions & 2 deletions internal/engine/selective_rag.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"strings"
"sync"
"time"

"github.com/GrayCodeAI/hawk/internal/engine/token"
)

// SelectiveRAG implements the Repoformer-style selective retrieval mechanism.
Expand Down Expand Up @@ -171,8 +173,9 @@ func (r *SelectiveRAG) Stats() map[string]interface{} {
// Helper functions for query classification

func estimateTokens(text string) int {
// Rough estimate: 1 token per 4 chars
return len(text) / 4
// BPE-based estimate via the tok tokenizer instead of the len/4 char
// heuristic, which systematically undercounts code-heavy text.
return token.CountTokensFast(text)
}

func isNavigationQuery(query string) bool {
Expand Down
21 changes: 20 additions & 1 deletion internal/multiagent/parallel/parallel.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package parallel

import (
"context"
"errors"
"fmt"
"sync"
)
Expand Down Expand Up @@ -87,7 +88,9 @@ func (p *Pool) AddTask(description string) *Task {

// Run executes all queued tasks in parallel, each in its own git worktree.
// workFn receives the worktree path and task, and returns a result summary.
// Tasks that fail do not prevent other tasks from completing.
// Tasks that fail do not prevent other tasks from completing; the returned
// error aggregates every failed task so callers are not left with silent
// failures.
func (p *Pool) Run(ctx context.Context, workFn func(ctx context.Context, worktreePath string, task *Task) (string, error)) error {
p.mu.Lock()
tasks := make([]*Task, len(p.tasks))
Expand Down Expand Up @@ -161,6 +164,22 @@ func (p *Pool) Run(ctx context.Context, workFn func(ctx context.Context, worktre
}

wg.Wait()

// Aggregate failures instead of returning nil unconditionally, so a run
// where every task failed is distinguishable from a fully successful run.
var failed []*Task
for _, t := range tasks {
if t.Status == StatusFailed {
failed = append(failed, t)
}
}
if len(failed) > 0 {
errs := make([]error, 0, len(failed))
for _, t := range failed {
errs = append(errs, fmt.Errorf("task %q failed: %w", t.Description, t.Error))
}
return errors.Join(errs...)
}
return nil
}

Expand Down
12 changes: 8 additions & 4 deletions internal/multiagent/parallel/parallel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -202,8 +203,11 @@ func TestErrorHandling(t *testing.T) {
}
return "success", nil
})
if err != nil {
t.Fatalf("Run: %v", err)
if err == nil {
t.Fatal("Run should return an aggregate error when a task fails")
}
if !strings.Contains(err.Error(), "fail-1") || !strings.Contains(err.Error(), "intentional failure") {
t.Errorf("aggregate error should name the failed task and its error, got: %v", err)
}
defer pool.Cleanup()

Expand Down Expand Up @@ -310,8 +314,8 @@ func TestContextCancellation(t *testing.T) {
return "", ctx.Err()
}
})
if err != nil {
t.Fatalf("Run: %v", err)
if err == nil {
t.Fatal("Run should report an aggregate error when tasks fail")
}
defer pool.Cleanup()

Expand Down
21 changes: 19 additions & 2 deletions internal/sandbox/seatbelt.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,25 @@ func GenerateSeatbeltProfile(policy *SeatbeltPolicy) string {
b.WriteString("(version 1)\n")
b.WriteString("(deny default)\n")

// Always allow basic mach-lookup for system functionality.
b.WriteString("(allow mach-lookup)\n")
// mach-lookup grants access to macOS XPC services. The legacy TierOff
// behavior allows every service; all other tiers are restricted to the
// minimal set of services required for normal tooling to work. Network
// resolution services are only reachable when network is allowed.
switch policy.Tier {
case TierOff:
b.WriteString("(allow mach-lookup)\n")
default:
b.WriteString("(allow mach-lookup\n")
b.WriteString(" (global-name \"com.apple.system.opendirectoryd.api\")\n")
b.WriteString(" (global-name \"com.apple.system.notification_center\")\n")
b.WriteString(" (global-name \"com.apple.cfprefsd\")\n")
b.WriteString(" (global-name \"com.apple.system.logger\")\n")
if policy.AllowNetwork {
b.WriteString(" (global-name \"com.apple.mDNSResponder\")\n")
b.WriteString(" (global-name \"com.apple.system.systemconfigurationd\")\n")
}
b.WriteString(")\n")
}

// Sysctl read for basic system queries.
if policy.AllowSysctl {
Expand Down
29 changes: 29 additions & 0 deletions internal/sandbox/seatbelt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,35 @@ func TestDefaultHawkPolicy_ProfileProducesValidSBPL(t *testing.T) {
}
}

func TestGenerateSeatbeltProfile_MachLookupTiered(t *testing.T) {
// TierOff keeps the legacy broad mach-lookup rule.
off := GenerateSeatbeltProfile(&SeatbeltPolicy{Tier: TierOff})
if !strings.Contains(off, "(allow mach-lookup)\n") {
t.Error("TierOff profile should allow all mach-lookup services")
}

// All other tiers restrict mach-lookup to the service allowlist.
strict := GenerateSeatbeltProfile(&SeatbeltPolicy{Tier: TierStrict})
if strings.Contains(strict, "(allow mach-lookup)\n") {
t.Error("restricted profile should not contain the broad mach-lookup rule")
}
if !strings.Contains(strict, `(global-name "com.apple.system.opendirectoryd.api")`) {
t.Error("restricted profile should allow opendirectoryd.api")
}
if strings.Contains(strict, `(global-name "com.apple.mDNSResponder")`) {
t.Error("network service should not be reachable when AllowNetwork is false")
}

// Network-enabled tiers additionally allow resolution services.
net := GenerateSeatbeltProfile(&SeatbeltPolicy{Tier: TierWorkspace, AllowNetwork: true})
if !strings.Contains(net, `(global-name "com.apple.mDNSResponder")`) {
t.Error("network-enabled profile should allow mDNSResponder")
}
if !strings.Contains(net, `(global-name "com.apple.system.systemconfigurationd")`) {
t.Error("network-enabled profile should allow systemconfigurationd")
}
}

func TestRunSeatbelted_ReturnsCmd(t *testing.T) {
policy := &SeatbeltPolicy{
AllowNetwork: false,
Expand Down
Loading