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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 7 additions & 1 deletion .github/actions/checkout-eyrie/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ runs:
echo "eyrie already present at $dest"
exit 0
fi
git clone --depth=1 --branch "${{ inputs.ref }}" \
ref="${{ inputs.ref }}"
# Fall back to main if the branch doesn't exist on eyrie
if ! git ls-remote --heads "https://github.com/GrayCodeAI/eyrie.git" "$ref" | grep -q .; then
echo "Branch '$ref' not found on eyrie, falling back to main"
ref="main"
fi
git clone --depth=1 --branch "$ref" \
"https://github.com/GrayCodeAI/eyrie.git" "$dest"
3 changes: 2 additions & 1 deletion cmd/bg_sessions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -125,7 +126,7 @@ func StartBGSession(prompt string, args []string) (*BGSessionInfo, error) {

// Build command: hawk --print <prompt> with all inherited flags
cmdArgs := append([]string{"--print", "--session-id", id, prompt}, args...)
cmd := exec.Command("hawk", cmdArgs...)
cmd := exec.CommandContext(context.Background(), "hawk", cmdArgs...)
cmd.Dir = cwd

logF, err := os.Create(logFile)
Expand Down
21 changes: 11 additions & 10 deletions cmd/chat_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/GrayCodeAI/eyrie/client"
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/engine/project"
"github.com/GrayCodeAI/hawk/internal/feature/shellmode"
"github.com/GrayCodeAI/hawk/internal/feature/taste"
"github.com/GrayCodeAI/hawk/internal/intelligence/memory"
Expand Down Expand Up @@ -243,7 +244,7 @@ func applySlashSuggestion(input string) string {
}

func gitOutput(args ...string) (string, error) {
out, err := exec.Command("git", args...).CombinedOutput()
out, err := exec.CommandContext(context.Background(), "git", args...).CombinedOutput()
return strings.TrimSpace(string(out)), err
}

Expand Down Expand Up @@ -1085,12 +1086,12 @@ Generate the recap:`, summary.String())
arg := strings.TrimSpace(strings.TrimPrefix(text, "/context"))
if arg == "init" {
cwd, _ := os.Getwd()
pc := engine.NewProjectContext(cwd)
pc := project.NewProjectContext(cwd)
return m.startPromptCommand("/context init", pc.InitPrompt())
}
if arg == "show" {
cwd, _ := os.Getwd()
pc := engine.NewProjectContext(cwd)
pc := project.NewProjectContext(cwd)
content := pc.Load()
if content == "" {
m.messages = append(m.messages, displayMsg{role: "system", content: "No project context files found. Run /context init to generate."})
Expand Down Expand Up @@ -1871,7 +1872,7 @@ Generate the recap:`, summary.String())
m.messages = append(m.messages, displayMsg{role: "system", content: pluginsSummary(m.pluginRuntime)})
return m, nil
case "/voice":
out, err := exec.Command("which", "whisper").CombinedOutput()
out, err := exec.CommandContext(context.Background(), "which", "whisper").CombinedOutput()
if err != nil || strings.TrimSpace(string(out)) == "" {
m.messages = append(m.messages, displayMsg{role: "error", content: "Voice requires whisper.cpp. Install with: brew install whisper-cpp"})
} else {
Expand Down Expand Up @@ -2216,7 +2217,7 @@ Generate the recap:`, summary.String())
m.messages = append(m.messages, displayMsg{role: "error", content: "Blocked: command fails safety check"})
return m, nil
}
out, err := exec.Command("sh", "-c", cmdStr).CombinedOutput()
out, err := exec.CommandContext(context.Background(), "sh", "-c", cmdStr).CombinedOutput()
result := strings.TrimSpace(string(out))
if err != nil {
result += "\n" + err.Error()
Expand All @@ -2234,7 +2235,7 @@ Generate the recap:`, summary.String())
m.messages = append(m.messages, displayMsg{role: "error", content: "Blocked: command fails safety check"})
return m, nil
}
out, err := exec.Command("sh", "-c", cmdStr).CombinedOutput()
out, err := exec.CommandContext(context.Background(), "sh", "-c", cmdStr).CombinedOutput()
result := strings.TrimSpace(string(out))
if err != nil {
result += "\n" + err.Error()
Expand All @@ -2254,7 +2255,7 @@ Generate the recap:`, summary.String())
m.messages = append(m.messages, displayMsg{role: "error", content: "Blocked: command fails safety check"})
return m, nil
}
out, _ := exec.Command("sh", "-c", cmdStr).CombinedOutput()
out, _ := exec.CommandContext(context.Background(), "sh", "-c", cmdStr).CombinedOutput()
result := strings.TrimSpace(string(out))
if result == "" {
m.messages = append(m.messages, displayMsg{role: "system", content: "No lint issues."})
Expand Down Expand Up @@ -2321,7 +2322,7 @@ Generate the recap:`, summary.String())
func explainCode(path string, line int) (string, error) {
// Step 1: git blame to find the commit
args := []string{"blame", "-L", fmt.Sprintf("%d,%d", line, line), "--porcelain", path}
out, err := exec.Command("git", args...).Output()
out, err := exec.CommandContext(context.Background(), "git", args...).Output()
if err != nil {
return "", fmt.Errorf("git blame failed: %w", err)
}
Expand All @@ -2335,13 +2336,13 @@ func explainCode(path string, line int) (string, error) {
}

// Step 2: get commit info
info, err := exec.Command("git", "log", "-1", "--format=%h %s (%an, %ar)", commitHash).Output()
info, err := exec.CommandContext(context.Background(), "git", "log", "-1", "--format=%h %s (%an, %ar)", commitHash).Output()
if err != nil {
return fmt.Sprintf("Commit: %s (details unavailable)", commitHash[:7]), nil
}

// Step 3: get the diff for context
diff, _ := exec.Command("git", "log", "-1", "--format=", "-p", "--", path, commitHash).Output()
diff, _ := exec.CommandContext(context.Background(), "git", "log", "-1", "--format=", "-p", "--", path, commitHash).Output()
diffStr := string(diff)
if len(diffStr) > 2000 {
diffStr = diffStr[:2000] + "\n... (truncated)"
Expand Down
5 changes: 3 additions & 2 deletions cmd/chat_print.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/GrayCodeAI/eyrie/client"
"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/engine/lifecycle"
"github.com/GrayCodeAI/hawk/internal/observability/logger"
"github.com/GrayCodeAI/hawk/internal/session"
)
Expand Down Expand Up @@ -63,9 +64,9 @@ func runPrint(text string) error {
// Wire timeout if --timeout flag is set
ctx := context.Background()
if timeout > 0 {
cfg := engine.TimeoutConfig{Total: timeout, Countdown: true}
cfg := lifecycle.TimeoutConfig{Total: timeout, Countdown: true}
var cancel context.CancelFunc
ctx, cancel = engine.WithTimeout(ctx, cfg)
ctx, cancel = lifecycle.WithTimeout(ctx, cfg)
defer cancel()
}

Expand Down
17 changes: 9 additions & 8 deletions cmd/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"bytes"
"context"
"fmt"
"os/exec"
"runtime"
Expand All @@ -15,18 +16,18 @@ func copyToClipboard(text string) error {

switch runtime.GOOS {
case "darwin":
cmd = exec.Command("pbcopy")
cmd = exec.CommandContext(context.Background(), "pbcopy")
case "linux":
// Try xclip first, fall back to xsel
if _, err := exec.LookPath("xclip"); err == nil {
cmd = exec.Command("xclip", "-selection", "clipboard")
cmd = exec.CommandContext(context.Background(), "xclip", "-selection", "clipboard")
} else if _, err := exec.LookPath("xsel"); err == nil {
cmd = exec.Command("xsel", "--clipboard", "--input")
cmd = exec.CommandContext(context.Background(), "xsel", "--clipboard", "--input")
} else {
return fmt.Errorf("clipboard not available: install xclip or xsel")
}
case "windows":
cmd = exec.Command("clip.exe")
cmd = exec.CommandContext(context.Background(), "clip.exe")
default:
return fmt.Errorf("clipboard not supported on %s", runtime.GOOS)
}
Expand All @@ -42,17 +43,17 @@ func pasteFromClipboard() (string, error) {

switch runtime.GOOS {
case "darwin":
cmd = exec.Command("pbpaste")
cmd = exec.CommandContext(context.Background(), "pbpaste")
case "linux":
if _, err := exec.LookPath("xclip"); err == nil {
cmd = exec.Command("xclip", "-selection", "clipboard", "-o")
cmd = exec.CommandContext(context.Background(), "xclip", "-selection", "clipboard", "-o")
} else if _, err := exec.LookPath("xsel"); err == nil {
cmd = exec.Command("xsel", "--clipboard", "--output")
cmd = exec.CommandContext(context.Background(), "xsel", "--clipboard", "--output")
} else {
return "", fmt.Errorf("clipboard not available: install xclip or xsel")
}
case "windows":
cmd = exec.Command("powershell.exe", "-command", "Get-Clipboard")
cmd = exec.CommandContext(context.Background(), "powershell.exe", "-command", "Get-Clipboard")
default:
return "", fmt.Errorf("clipboard not supported on %s", runtime.GOOS)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/context_export.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -213,7 +214,7 @@ func gitContextInfo(dir string) string {

// runGit executes a git command in the given directory.
func runGit(dir string, args ...string) (string, error) {
cmd := exec.Command("git", args...)
cmd := exec.CommandContext(context.Background(), "git", args...)
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions cmd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
"github.com/GrayCodeAI/hawk/internal/daemon"
"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/netutil"
"github.com/GrayCodeAI/hawk/internal/observability/logger"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -92,7 +93,7 @@ func runDaemonStart(_ *cobra.Command, _ []string) error {
return sess, nil
}

srv := daemon.New(daemon.Config{Port: daemonPort, Host: "127.0.0.1", APIKey: apiKey}, factory)
srv := daemon.New(daemon.Config{Port: daemonPort, Host: netutil.LoopbackHost, APIKey: apiKey}, factory)
addr, err := srv.Start()
if err != nil {
return err
Expand All @@ -103,7 +104,7 @@ func runDaemonStart(_ *cobra.Command, _ []string) error {
preheater.Start([]string{
"https://api.anthropic.com/v1/messages",
"https://api.openai.com/v1/chat/completions",
fmt.Sprintf("http://127.0.0.1:%d/v1/health", daemonPort),
fmt.Sprintf("http://%s:%d/v1/health", netutil.LoopbackHost, daemonPort),
})
defer preheater.Stop()

Expand Down
6 changes: 3 additions & 3 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,14 @@ func persistExecSession(id, model, provider, userMsg, assistantMsg string) {
}

func createExecWorktree(repoDir, baseBranch, branch string) (string, error) {
cmd := exec.Command("mktemp", "-d")
cmd := exec.CommandContext(context.Background(), "mktemp", "-d")
out, err := cmd.Output()
if err != nil {
return "", err
}
wtPath := strings.TrimSpace(string(out))

gitCmd := exec.Command("git", "worktree", "add", "-b", branch, wtPath, baseBranch)
gitCmd := exec.CommandContext(context.Background(), "git", "worktree", "add", "-b", branch, wtPath, baseBranch)
gitCmd.Dir = repoDir
if errOut, err := gitCmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("%s: %w", strings.TrimSpace(string(errOut)), err)
Expand All @@ -304,7 +304,7 @@ func cleanupExecWorktree(repoDir, wtPath string) {
if wtPath == "" {
return
}
cmd := exec.Command("git", "worktree", "remove", "--force", wtPath)
cmd := exec.CommandContext(context.Background(), "git", "worktree", "remove", "--force", wtPath)
cmd.Dir = repoDir
_ = cmd.Run()
}
7 changes: 4 additions & 3 deletions cmd/feedback.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"net/url"
Expand Down Expand Up @@ -152,11 +153,11 @@ func openFeedbackIssue(report FeedbackReport) error {
func openBrowser(url string) error {
switch runtime.GOOS {
case "darwin":
return exec.Command("open", url).Start()
return exec.CommandContext(context.Background(), "open", url).Start()
case "linux":
return exec.Command("xdg-open", url).Start()
return exec.CommandContext(context.Background(), "xdg-open", url).Start()
case "windows":
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
return exec.CommandContext(context.Background(), "rundll32", "url.dll,FileProtocolHandler", url).Start()
default:
return fmt.Errorf("unsupported platform")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/mission.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func parseFeatures(text string) []mission.Feature {
}

func getCurrentBranch(dir string) string {
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
cmd := exec.CommandContext(context.Background(), "git", "rev-parse", "--abbrev-ref", "HEAD")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions cmd/notifications.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"os/exec"
"runtime"
"time"
Expand All @@ -19,12 +20,13 @@ func notifyCompletion(duration time.Duration) {
switch runtime.GOOS {
case "darwin":
// macOS: use osascript for native notification
_ = exec.Command(
_ = exec.CommandContext(
context.Background(),
"osascript", "-e",
`display notification "`+msg+`" with title "Hawk"`,
).Start()
case "linux":
// Linux: use notify-send if available
_ = exec.Command("notify-send", "Hawk", msg).Start()
_ = exec.CommandContext(context.Background(), "notify-send", "Hawk", msg).Start()
}
}
7 changes: 4 additions & 3 deletions cmd/notify.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -135,10 +136,10 @@ func (n *Notifier) DesktopNotify(title, message string) error {
case "darwin":
script := fmt.Sprintf(`display notification "%s" with title "%s"`,
escapeAppleScript(message), escapeAppleScript(title))
cmd := exec.Command("osascript", "-e", script)
cmd := exec.CommandContext(context.Background(), "osascript", "-e", script)
return cmd.Run()
case "linux":
cmd := exec.Command("notify-send", title, message)
cmd := exec.CommandContext(context.Background(), "notify-send", title, message)
return cmd.Run()
case "windows":
script := fmt.Sprintf(`
Expand All @@ -150,7 +151,7 @@ $textNodes.Item(1).AppendChild($template.CreateTextNode('%s')) | Out-Null
$toast = [Windows.UI.Notifications.ToastNotification]::new($template)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('hawk').Show($toast)`,
escapePowerShell(title), escapePowerShell(message))
cmd := exec.Command("powershell", "-Command", script)
cmd := exec.CommandContext(context.Background(), "powershell", "-Command", script)
return cmd.Run()
default:
return fmt.Errorf("desktop notifications not supported on %s", runtime.GOOS)
Expand Down
10 changes: 6 additions & 4 deletions cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/GrayCodeAI/eyrie/client"
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
"github.com/GrayCodeAI/hawk/internal/engine"
"github.com/GrayCodeAI/hawk/internal/engine/branching"
"github.com/GrayCodeAI/hawk/internal/engine/lifecycle"
"github.com/GrayCodeAI/hawk/internal/eyrieclient"
"github.com/GrayCodeAI/hawk/internal/intelligence/memory"
"github.com/GrayCodeAI/hawk/internal/intelligence/repomap"
Expand Down Expand Up @@ -261,14 +263,14 @@ func configureSession(sess *engine.Session, settings hawkconfig.Settings) error
if settings.ModelRoles != nil {
roles = *settings.ModelRoles
}
sess.Cascade = engine.NewCascadeRouter(sess.Model(), roles)
sess.Cascade = branching.NewCascadeRouter(sess.Model(), roles)
sess.Cascade.Enabled = true
sess.Cascade.FrugalMode = settings.Frugal

// Session lifecycle: self-improvement loop (learn from sessions)
sess.Lifecycle = &engine.SessionLifecycle{
Memory: &engine.EvolvingMemoryAdapter{EM: memory.NewEvolvingMemory()},
SkillStore: &engine.SkillDistillerAdapter{SD: sess.SkillDistiller},
sess.Lifecycle = &lifecycle.SessionLifecycle{
Memory: &lifecycle.EvolvingMemoryAdapter{EM: memory.NewEvolvingMemory()},
SkillStore: &lifecycle.SkillDistillerAdapter{SD: sess.SkillDistiller},
}

return nil
Expand Down
3 changes: 2 additions & 1 deletion cmd/review.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -80,7 +81,7 @@ func runReviewInit(_ *cobra.Command, _ []string) error {
}

func findGitDir() (string, error) {
out, err := exec.Command("git", "rev-parse", "--git-dir").Output()
out, err := exec.CommandContext(context.Background(), "git", "rev-parse", "--git-dir").Output()
if err != nil {
return "", fmt.Errorf("not a git repository (run from inside a git repo)")
}
Expand Down
Loading