Skip to content

Commit bf7bf63

Browse files
authored
feat(storage): centralize hawk data paths and storage policy cleanup (#65)
* feat(storage): add storage policy cleanup and centralize hawk data paths Introduce internal/storage helpers with policy tests, route session and plugin persistence through the shared layout, and trim duplicated path handling across cmd and internal packages. * fix(test): isolate storage dirs in race-sensitive tests Tests that only set HOME were reading real CI state after the storage policy refactor. Route them through HAWK_* dir overrides and bump eyrie. * fix(test): isolate Hawk storage dirs across CI race tests Add testutil.IsolateStorage and route history, config, engine, and analytics tests through HAWK_* overrides so shuffled CI runs do not read shared runner state.
1 parent 625c56e commit bf7bf63

154 files changed

Lines changed: 958 additions & 949 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
var agentCmd = &cobra.Command{
1616
Use: "agent",
1717
Short: "Manage custom agent personas",
18-
Long: "Create, list, and manage custom agent personas stored in ~/.hawk/agents/.",
18+
Long: "Create, list, and manage custom agent personas stored in Hawk user state.",
1919
}
2020

2121
var agentListCmd = &cobra.Command{

cmd/audit.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/GrayCodeAI/hawk/internal/hooks/audit"
13+
"github.com/GrayCodeAI/hawk/internal/storage"
1314
"github.com/spf13/cobra"
1415
)
1516

@@ -175,16 +176,11 @@ type SessionInfo struct {
175176
}
176177

177178
func discoverSessions(days int, projectFilter string) ([]SessionInfo, error) {
178-
home, err := os.UserHomeDir()
179-
if err != nil {
180-
return nil, err
181-
}
182-
183179
cutoff := time.Now().AddDate(0, 0, -days)
184180
var sessions []SessionInfo
185181

186182
// Scan hawk sessions directory
187-
hawkDir := filepath.Join(home, ".hawk", "sessions")
183+
hawkDir := storage.SessionsDir()
188184
entries, err := os.ReadDir(hawkDir)
189185
if err != nil && !os.IsNotExist(err) {
190186
return nil, err

cmd/bg_sessions.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13-
"github.com/GrayCodeAI/hawk/internal/home"
13+
"github.com/GrayCodeAI/hawk/internal/storage"
1414
"github.com/spf13/cobra"
1515
)
1616

@@ -19,8 +19,7 @@ import (
1919
// ─────────────────────────────────────────────────────────────────────────────
2020

2121
func bgSessionsDir() string {
22-
home := home.Dir()
23-
return filepath.Join(home, ".hawk", "bg-sessions")
22+
return filepath.Join(storage.StateDir(), "bg-sessions")
2423
}
2524

2625
// BGSessionInfo tracks a running background session.

cmd/chat.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ import (
3030
"github.com/GrayCodeAI/hawk/internal/engine"
3131
"github.com/GrayCodeAI/hawk/internal/feature/shellmode"
3232
"github.com/GrayCodeAI/hawk/internal/feature/taste"
33-
"github.com/GrayCodeAI/hawk/internal/home"
3433
"github.com/GrayCodeAI/hawk/internal/intelligence/memory"
3534
"github.com/GrayCodeAI/hawk/internal/intelligence/repomap"
3635
"github.com/GrayCodeAI/hawk/internal/observability/logger"
3736
"github.com/GrayCodeAI/hawk/internal/plugin"
3837
"github.com/GrayCodeAI/hawk/internal/sandbox"
3938
"github.com/GrayCodeAI/hawk/internal/session"
4039
"github.com/GrayCodeAI/hawk/internal/startup"
40+
hawkstorage "github.com/GrayCodeAI/hawk/internal/storage"
4141
"github.com/GrayCodeAI/hawk/internal/system/staleness"
4242
"github.com/GrayCodeAI/hawk/internal/ui/icons"
4343
)
@@ -152,11 +152,9 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
152152

153153
// Initialize conversation DAG for branching support
154154
startup.MarkPhase("newChatModel:dag")
155-
if home, err := os.UserHomeDir(); err == nil {
156-
dagPath := filepath.Join(home, ".hawk", "sessions", "convo.db")
157-
if dag, err := storage.NewDAG(dagPath, sid); err == nil {
158-
sess.SetConvoDAG(dag)
159-
}
155+
dagPath := filepath.Join(hawkstorage.SessionsDir(), "convo.db")
156+
if dag, err := storage.NewDAG(dagPath, sid); err == nil {
157+
sess.SetConvoDAG(dag)
160158
}
161159
startup.EndPhase("newChatModel:dag")
162160

@@ -241,8 +239,7 @@ func newChatModel(ref *progRef, systemPrompt string, settings hawkconfig.Setting
241239
// Check for crash recovery
242240
startup.MarkPhase("newChatModel:crash-recovery")
243241
if recovered := session.CheckForRecovery(); len(recovered) > 0 {
244-
home := home.Dir()
245-
walDir := filepath.Join(home, ".hawk", "sessions")
242+
walDir := hawkstorage.SessionsDir()
246243
for _, rid := range recovered {
247244
if rid == sid {
248245
continue // current session WAL

cmd/chat_commands_session.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
tea "github.com/charmbracelet/bubbletea"
1313

1414
"github.com/GrayCodeAI/eyrie/client"
15-
"github.com/GrayCodeAI/hawk/internal/home"
1615
"github.com/GrayCodeAI/hawk/internal/session"
16+
"github.com/GrayCodeAI/hawk/internal/storage"
1717
)
1818

1919
// saveSession persists the current session to disk.
@@ -234,8 +234,7 @@ func (m *chatModel) handleSessionCommand(cmd string, parts []string, text string
234234
return m, nil
235235

236236
case "/export":
237-
homeDir := home.Dir()
238-
exportDir := filepath.Join(homeDir, ".hawk", "exports")
237+
exportDir := filepath.Join(storage.StateDir(), "exports")
239238
_ = os.MkdirAll(exportDir, 0o755)
240239
exportPath := filepath.Join(exportDir, m.sessionID+".md")
241240
var md strings.Builder
@@ -258,8 +257,7 @@ func (m *chatModel) handleSessionCommand(cmd string, parts []string, text string
258257
return m, nil
259258

260259
case "/share":
261-
homeDir := home.Dir()
262-
exportDir := filepath.Join(homeDir, ".hawk", "exports")
260+
exportDir := filepath.Join(storage.StateDir(), "exports")
263261
_ = os.MkdirAll(exportDir, 0o755)
264262
exportPath := filepath.Join(exportDir, m.sessionID+".md")
265263
var md strings.Builder
@@ -286,8 +284,7 @@ func (m *chatModel) handleSessionCommand(cmd string, parts []string, text string
286284
return m, nil
287285
}
288286
newName := parts[1]
289-
homeDir := home.Dir()
290-
sessDir := filepath.Join(homeDir, ".hawk", "sessions")
287+
sessDir := storage.SessionsDir()
291288
oldPath := filepath.Join(sessDir, m.sessionID+".jsonl")
292289
newPath := filepath.Join(sessDir, newName+".jsonl")
293290
if err := os.Rename(oldPath, newPath); err != nil {
@@ -303,8 +300,7 @@ func (m *chatModel) handleSessionCommand(cmd string, parts []string, text string
303300
m.messages = append(m.messages, displayMsg{role: "system", content: "Usage: /tag <label>"})
304301
return m, nil
305302
}
306-
homeDir := home.Dir()
307-
tagFile := filepath.Join(homeDir, ".hawk", "sessions", m.sessionID+".tags")
303+
tagFile := filepath.Join(storage.SessionsDir(), m.sessionID+".tags")
308304
f, err := os.OpenFile(tagFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
309305
if err != nil {
310306
m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()})

cmd/chat_commands_util.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func sessionStats(sess *engine.Session, id string) string {
127127
}
128128

129129
func hooksSummary() string {
130-
return "Hooks: pre_query, post_query, pre_tool, post_tool, session_start, session_end, permission_ask, error\nConfigure in .hawk/settings.json or ~/.hawk/settings.json"
130+
return "Hooks: pre_query, post_query, pre_tool, post_tool, session_start, session_end, permission_ask, error\nConfigure in Hawk user settings"
131131
}
132132

133133
func pluginsSummary(rt *plugin.Runtime) string {

cmd/chat_focus.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
tea "github.com/charmbracelet/bubbletea"
1111

1212
"github.com/GrayCodeAI/hawk/internal/engine"
13-
"github.com/GrayCodeAI/hawk/internal/home"
13+
"github.com/GrayCodeAI/hawk/internal/storage"
1414
"github.com/GrayCodeAI/hawk/internal/ui/icons"
1515
)
1616

@@ -25,7 +25,7 @@ const (
2525
const toolDisplayMaxChars = 12000 // preview cap in scrollback (Grok uses ~20k for bash)
2626

2727
func sessionExportPath(sessionID string) string {
28-
return filepath.Join(home.Dir(), ".hawk", "exports", sessionID+".md")
28+
return filepath.Join(storage.StateDir(), "exports", sessionID+".md")
2929
}
3030

3131
func (m *chatModel) inScrollbackFocus() bool {

cmd/chat_subcommand_memory.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
99
)
1010

11-
// memorySubcommand implements the /memory slash command. It
12-
// prints the project's AGENTS.md (or .hawk/AGENTS.md) content.
11+
// memorySubcommand implements the /memory slash command.
12+
// It prints the project's AGENTS.md content.
1313
type memorySubcommand struct{}
1414

1515
func (m *memorySubcommand) Name() string { return "memory" }
@@ -19,7 +19,7 @@ func (m *memorySubcommand) Usage() string { return "" }
1919
func (m *memorySubcommand) Handle(ml *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
2020
md := strings.TrimSpace(hawkconfig.LoadAgentsMD())
2121
if md == "" {
22-
ml.messages = append(ml.messages, displayMsg{role: "system", content: "No AGENTS.md or .hawk/AGENTS.md project instructions found.\nUse /yaad for persistent graph memory."})
22+
ml.messages = append(ml.messages, displayMsg{role: "system", content: "No AGENTS.md project instructions found.\nUse /yaad for persistent graph memory."})
2323
} else {
2424
ml.messages = append(ml.messages, displayMsg{role: "system", content: "Project instructions (AGENTS.md):\n" + md})
2525
}

cmd/chat_subcommand_recipe.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func (r *recipeSubcommand) Handle(m *chatModel, args []string, text string) (tea
2525
rn := recipe.NewRunner()
2626
recipes := rn.List()
2727
if len(recipes) == 0 {
28-
m.messages = append(m.messages, displayMsg{role: "system", content: "No recipes found in ~/.hawk/recipes/ or .hawk/recipes/"})
28+
m.messages = append(m.messages, displayMsg{role: "system", content: "No recipes found in Hawk user state or .agents/recipes/"})
2929
} else {
3030
var list string
3131
for _, r := range recipes {

cmd/chat_subcommand_simple.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import (
1212
tea "github.com/charmbracelet/bubbletea"
1313

1414
hawkconfig "github.com/GrayCodeAI/hawk/internal/config"
15-
"github.com/GrayCodeAI/hawk/internal/home"
1615
analytics "github.com/GrayCodeAI/hawk/internal/observability"
1716
"github.com/GrayCodeAI/hawk/internal/plugin"
17+
"github.com/GrayCodeAI/hawk/internal/storage"
1818
"github.com/GrayCodeAI/hawk/internal/tool"
1919
)
2020

@@ -645,19 +645,18 @@ func init() {
645645
},
646646
})
647647

648-
// /feedback <msg> — submit feedback saved to ~/.hawk/feedback/
648+
// /feedback <msg> — submit feedback saved to Hawk user state.
649649
subcommandRegistry.Register(&delegatingCommand{
650650
name: "feedback",
651-
description: "submit feedback (saved to ~/.hawk/feedback/)",
651+
description: "submit feedback (saved to Hawk user state)",
652652
usage: "/feedback <message>",
653653
handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) {
654654
body := strings.TrimSpace(strings.TrimPrefix(text, "/feedback"))
655655
if body == "" {
656-
m.messages = append(m.messages, displayMsg{role: "system", content: "Usage: /feedback <message>\nCaptures session context and saves feedback to ~/.hawk/feedback/"})
656+
m.messages = append(m.messages, displayMsg{role: "system", content: "Usage: /feedback <message>\nCaptures session context and saves feedback to Hawk user state."})
657657
return m, nil
658658
}
659-
homeDir := home.Dir()
660-
feedDir := filepath.Join(homeDir, ".hawk", "feedback")
659+
feedDir := filepath.Join(storage.StateDir(), "feedback")
661660
_ = os.MkdirAll(feedDir, 0o755)
662661
report := fmt.Sprintf(`{"timestamp":%q,"version":%q,"model":%q,"provider":%q,"category":"session","body":%q,"session_id":%q}`,
663662
time.Now().Format(time.RFC3339), version, m.session.Model(), m.session.Provider(), body, m.sessionID)

0 commit comments

Comments
 (0)