Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b6d14b1
refactor: establish architecture baseline and session ownership
Patel230 Aug 4, 2026
453357e
refactor: make chat service transport owner
Patel230 Aug 4, 2026
f8a36ce
refactor: centralize session tool registry ownership
Patel230 Aug 4, 2026
0bc434c
refactor: move smart skill cache to lifecycle service
Patel230 Aug 4, 2026
a91cfcf
refactor: centralize tool working directory state
Patel230 Aug 4, 2026
52a2e20
refactor: move token budget tracking to lifecycle service
Patel230 Aug 4, 2026
2ededa6
refactor: centralize read-only Bash execution policy
Patel230 Aug 4, 2026
02b404e
refactor: make chat service metrics owner
Patel230 Aug 4, 2026
5277dd1
refactor: centralize session logger ownership
Patel230 Aug 4, 2026
d66f1f9
docs: record session cost compatibility boundary
Patel230 Aug 4, 2026
2287eb2
refactor: centralize noninteractive session composition
Patel230 Aug 4, 2026
91f6a88
refactor: route print and mission through session composer
Patel230 Aug 4, 2026
a4cc36b
docs: record noninteractive composition boundary
Patel230 Aug 4, 2026
f8f6686
refactor: name interactive session startup composition
Patel230 Aug 4, 2026
52b29fb
fix: synchronize container execution state
Patel230 Aug 4, 2026
90cbde5
refactor: use core contract for native compaction
Patel230 Aug 4, 2026
11193b4
docs: record provider boundary hardening
Patel230 Aug 4, 2026
77cc3aa
refactor: route graph budget through yaad bridge
Patel230 Aug 4, 2026
70acec1
refactor: route code memory links through yaad bridge
Patel230 Aug 4, 2026
cda5a29
docs: record yaad facade progress
Patel230 Aug 4, 2026
cf1c60e
chore: consolidate agent instructions
Patel230 Aug 4, 2026
3ad19f6
chore: refresh gitnexus index metadata
Patel230 Aug 4, 2026
6341d10
refactor: route cross-project memory through yaad bridge
Patel230 Aug 4, 2026
bb58b42
refactor: route confidence through yaad bridge
Patel230 Aug 4, 2026
6406a04
refactor: route shared memory through yaad bridge
Patel230 Aug 4, 2026
461cf31
refactor: route compaction through token facade
Patel230 Aug 4, 2026
fc7ce68
refactor: isolate tok usage tracking behind facade
Patel230 Aug 4, 2026
04027d4
refactor: route token consumers through hawk facade
Patel230 Aug 4, 2026
33147f8
refactor: route response redaction through token facade
Patel230 Aug 4, 2026
b6c7d9a
refactor: place tok adapter at internal boundary
Patel230 Aug 4, 2026
3461f32
fix: synchronize lazy session persistence initialization
Patel230 Aug 4, 2026
ba45e58
fix: synchronize session cost snapshots
Patel230 Aug 4, 2026
138d3d0
fix: avoid session lock around persistence writes
Patel230 Aug 4, 2026
4e22f4a
fix: surface WAL recovery I/O errors
Patel230 Aug 4, 2026
f477970
docs: clarify architecture persistence status
Patel230 Aug 4, 2026
5e7db3b
refactor: share noninteractive session composition
Patel230 Aug 4, 2026
eb1716b
docs: accept file-first session persistence
Patel230 Aug 4, 2026
44432f1
chore: update eyrie Nemotron compatibility
Patel230 Aug 4, 2026
2b084c1
chore: update eyrie OpenGateway error handling
Patel230 Aug 4, 2026
ade734f
chore: update eyrie response compatibility
Patel230 Aug 4, 2026
e53d62b
fix: identify as Hawk Sandbox Coding Agents and skip tools on greetings
Patel230 Aug 4, 2026
c1d46e0
fix: chat footer and status bar layout
Patel230 Aug 4, 2026
9b10bc7
fix(docs): clean markdownlint on baseline
Patel230 Aug 5, 2026
330e22c
Merge branch 'main' into chore/architecture-phase0-baseline
Patel230 Aug 5, 2026
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ with its native Responses API (`/v1/responses`) under the
<!-- gitnexus:start -->
## GitNexus — Code Intelligence

This project is indexed by GitNexus as **hawk** (86489 symbols, 267606 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **hawk** (88034 symbols, 273602 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).

Expand Down
44 changes: 0 additions & 44 deletions CLAUDE.md

This file was deleted.

7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ GORELEASER := $(GOBIN_DIR)/goreleaser
# ---------------------------------------------------------------------------
# Phony declarations (alphabetical).
# ---------------------------------------------------------------------------
.PHONY: all bench boundaries build check-replace ci clean contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard submodule-release-parity cover cover-new fmt help install lint lint-fix \
.PHONY: all bench boundaries build check-replace ci clean contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard package-boundaries-guard submodule-release-parity cover cover-new fmt help install lint lint-fix \
release security setup smoke path sync-external test test-10x test-live test-new test-race tidy version vet

check-replace: ## Fail if go.mod has local replace directives (run before tagging)
Expand Down Expand Up @@ -116,7 +116,10 @@ peer-guard: ## Fail if support engines import each other instead of depending on
internal-layers-guard: ## Enforce one-way dependencies across stable Hawk internal layers.
bash ./scripts/check-internal-layer-imports.sh

boundaries: contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard ## Alias for all boundary guards (matches `make boundaries` in engine repos).
package-boundaries-guard: ## Enforce AST/package-graph boundaries with file/line diagnostics.
bash ./scripts/check-package-boundaries.sh

boundaries: contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard package-boundaries-guard ## Alias for all boundary guards (matches `make boundaries` in engine repos).

submodule-release-parity: ## Verify every go.mod ecosystem version resolves to its pinned Gitlink.
bash ./scripts/check-submodule-release-parity.sh
Expand Down
13 changes: 2 additions & 11 deletions cmd/acp.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,15 @@ func init() {

func runACP(cmd *cobra.Command, _ []string) error {
settings := hawkconfig.LoadSettings()
newSession := newConfiguredHawkSessionFactory(settings, logger.New(io.Discard, logger.Error))

factory := func() (*engine.Session, error) {
systemPrompt, err := buildSystemPrompt()
if err != nil {
return nil, err
}
registry, err := defaultRegistry(settings)
if err != nil {
return nil, err
}
effectiveModel, effectiveProvider := effectiveModelAndProvider(settings)
sess := newHawkSession(settings, effectiveProvider, effectiveModel, systemPrompt, registry)
// stdout is the JSON-RPC channel; keep logs off it.
sess.SetLogger(logger.New(io.Discard, logger.Error))
if err := configureSession(sess, settings); err != nil {
return nil, err
}
return sess, nil
return newSession(systemPrompt, "")
}

ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
Expand Down
5 changes: 1 addition & 4 deletions cmd/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
"github.com/GrayCodeAI/hawk/internal/feature/taste"
"github.com/GrayCodeAI/hawk/internal/intelligence/memory"
"github.com/GrayCodeAI/hawk/internal/intelligence/repomap"
"github.com/GrayCodeAI/hawk/internal/observability/logger"
"github.com/GrayCodeAI/hawk/internal/plugin"
"github.com/GrayCodeAI/hawk/internal/session"
"github.com/GrayCodeAI/hawk/internal/startup"
Expand Down Expand Up @@ -168,9 +167,7 @@ func newChatModelWithRegistry(ref *progRef, systemPrompt string, settings hawkco
startup.EndPhase("newChatModel:newHawkSession")

startup.MarkPhase("newChatModel:configureSession")
syncSessionFromPersistedSelection(sess)
sess.SetLogger(logger.New(io.Discard, logger.Error))
if cfgErr := configureSessionStartup(sess, settings); cfgErr != nil {
if cfgErr := prepareInteractiveSessionStartup(sess, settings); cfgErr != nil {
return chatModel{}, cfgErr
}
startup.EndPhase("newChatModel:configureSession")
Expand Down
4 changes: 2 additions & 2 deletions cmd/chat_config_gateways_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func TestConfigGatewayRefreshTargetIndex_UsesSelectedRow(t *testing.T) {
_ = store.Set(t.Context(), gateway.AccountForEnv("OPENROUTER_API_KEY"), "sk-or-test-key-1234567890")
hawkconfig.InvalidateConfigUICache()

sess := &engine.Session{}
sess := engine.NewSession("", "", "", nil)
sess.SetProvider("openrouter")
m := chatModel{
configTab: configTabGateways,
Expand Down Expand Up @@ -168,7 +168,7 @@ func TestFocusConfigActiveGateway_SelectsActiveRow(t *testing.T) {
_ = store.Set(t.Context(), gateway.AccountForEnv("OPENROUTER_API_KEY"), "sk-or-test-key-1234567890")
hawkconfig.InvalidateConfigUICache()

sess := &engine.Session{}
sess := engine.NewSession("", "", "", nil)
sess.SetProvider("openrouter")
m := chatModel{
configTab: configTabGateways,
Expand Down
5 changes: 3 additions & 2 deletions cmd/chat_layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ func (m chatModel) withSyncedLayout() chatModel {
if m.configOpen {
bottomH = 0
}
// Viewport takes all available space above the bottom bar.
vpH := m.height - bottomH
if vpH < minChatViewportLines {
if vpH < minChatViewportLines && m.height >= minChatViewportLines+bottomH {
vpH = minChatViewportLines
} else if vpH < 1 {
vpH = 1
}
if m.viewport.Height() != vpH {
m.viewport.SetHeight(vpH)
Expand Down
45 changes: 43 additions & 2 deletions cmd/chat_layout_mouse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ func TestView_LineCountMatchesHeight(t *testing.T) {
if m.footerTopY() <= m.chatPaneTopY() {
t.Fatalf("footerTopY %d must be below chat top %d", m.footerTopY(), m.chatPaneTopY())
}
// Footer must start on the same row View() renders the Docker line.
// Footer must start on the same row View() renders the top footer line.
footerIdx := -1
for i, line := range lines {
if strings.Contains(line, "Docker:") {
footerIdx = i
footerIdx = i - 1
break
}
}
Expand All @@ -47,6 +47,47 @@ func TestView_LineCountMatchesHeight(t *testing.T) {
}
}

func TestView_FooterVisibleWhenOutputAreaIsLargeOrMultiline(t *testing.T) {
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(10))
vp.SetContent(strings.Repeat("Output line\n", 100))
vp.SetYOffset(10) // Scroll down so AtTop() is false and sticky header activates

inp := textarea.New()
inp.SetValue("First line\nSecond line") // Multiline prompt

m := chatModel{
height: 24,
width: 80,
viewport: vp,
input: inp,
messages: []displayMsg{
{role: "user", content: "Previous user prompt that is scrolled up"},
{role: "assistant", content: "Assistant response"},
},
}
m = m.withSyncedLayout()
got := m.View().Content
lines := strings.Split(strings.TrimRight(got, "\n"), "\n")

if len(lines) > m.height {
t.Fatalf("total rendered view lines (%d) exceeded terminal height (%d)", len(lines), m.height)
}

// Verify footer is rendered within the visible terminal height
footerFound := false
for i, line := range lines {
if strings.Contains(line, "Docker:") || strings.Contains(line, "tokens") || strings.Contains(line, "cost") {
footerFound = true
if i >= m.height {
t.Fatalf("footer line at index %d is beyond terminal height %d", i, m.height)
}
}
}
if !footerFound {
t.Fatal("expected footer in view output")
}
}

func TestMouseWheelDelta_SGRUsesZeroBasedY(t *testing.T) {
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(14))
vp.SetContent(strings.Repeat("line\n", 40))
Expand Down
10 changes: 4 additions & 6 deletions cmd/chat_print.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ func runPrint(text string) error {
return err
}

sess := newHawkSession(settings, effectiveProvider, effectiveModel, systemPrompt, registry)
sess.SetLogger(logger.New(io.Discard, logger.Error))
if cfgErr := configureSession(sess, settings); cfgErr != nil {
sess, cfgErr := newConfiguredHawkSession(settings, effectiveProvider, effectiveModel, systemPrompt, registry, logger.New(io.Discard, logger.Error))
if cfgErr != nil {
return cfgErr
}
projectDir, err := os.Getwd()
Expand Down Expand Up @@ -281,9 +280,8 @@ func runRepl() error {
return err
}

sess := newHawkSession(settings, effectiveProvider, effectiveModel, systemPrompt, registry)
sess.SetLogger(logger.New(io.Discard, logger.Error))
if cfgErr := configureSession(sess, settings); cfgErr != nil {
sess, cfgErr := newConfiguredHawkSession(settings, effectiveProvider, effectiveModel, systemPrompt, registry, logger.New(io.Discard, logger.Error))
if cfgErr != nil {
return cfgErr
}
projectDir, err := os.Getwd()
Expand Down
22 changes: 11 additions & 11 deletions cmd/chat_scrollbar.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,27 +149,27 @@ func padToHeight(s string, height int) string {
// prepended showing the most recent out-of-view prompt.
func (m chatModel) renderChatPane() string {
chatView := m.viewport.View()
vpH := m.viewport.Height()
origVpH := m.viewport.Height()

// Prepend sticky header when scrolled up.
sticky := m.renderStickyHeader(m.viewport.Width())
if sticky != "" {
chatView = sticky + "\n" + chatView
// Reduce viewport height by the sticky header height (header text
// + separator line = 2 rows) so the overall pane height stays
// consistent with the layout.
if vpH > stickyHeaderHeight {
vpH -= stickyHeaderHeight
}
}

lines := strings.Split(chatView, "\n")
if origVpH > 0 && len(lines) > origVpH {
lines = lines[:origVpH]
}
chatView = strings.Join(lines, "\n")

if !m.chatScrollbarVisible() {
return padToHeight(chatView, vpH)
return padToHeight(chatView, origVpH)
}

scrollbar := m.renderScrollbarHeight(vpH)
scrollbar := m.renderScrollbarHeight(origVpH)
if scrollbar == "" {
return padToHeight(chatView, vpH)
return padToHeight(chatView, origVpH)
}

targetW := m.viewport.Width()
Expand All @@ -179,7 +179,7 @@ func (m chatModel) renderChatPane() string {

// Join each line of the chat view with the corresponding scrollbar row.
chatLines := strings.Split(chatView, "\n")
for len(chatLines) < vpH {
for len(chatLines) < origVpH {
chatLines = append(chatLines, "")
}
barLines := strings.Split(scrollbar, "\n")
Expand Down
6 changes: 3 additions & 3 deletions cmd/chat_status_metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestPlatformContextForNativeModel_MimoV25Pro(t *testing.T) {
}

func TestConnectionStatusParts_OmitsDefault128kPlaceholder(t *testing.T) {
m := chatModel{session: &engine.Session{}}
m := chatModel{session: engine.NewSession("", "", "", nil)}
m.session.SetModel("mimo-v2.5-pro")
m.session.SetProvider("xiaomi_mimo_token_plan")
_, _, ctxLabel := m.connectionStatusParts()
Expand All @@ -64,7 +64,7 @@ func TestConnectionStatusParts_MimoShowsPlatformContext(t *testing.T) {
invalidatePlatformContextCache()
seedPlatformContextCacheForTest(map[string]int{"mimo-v2.5-pro": 1_048_576})
t.Cleanup(invalidatePlatformContextCache)
sess := &engine.Session{}
sess := engine.NewSession("", "", "", nil)
sess.SetProvider("xiaomi_mimo_token_plan")
sess.SetModel("mimo-v2.5-pro")
applyLiveModelMetadata(sess, "xiaomi_mimo_token_plan", "mimo-v2.5-pro")
Expand Down Expand Up @@ -120,7 +120,7 @@ func TestConnectionStatusParts_MimoShowsPlatformContext_HyphenProvider(t *testin
invalidatePlatformContextCache()
seedPlatformContextCacheForTest(map[string]int{"mimo-v2.5-pro": 1_048_576})
t.Cleanup(invalidatePlatformContextCache)
sess := &engine.Session{}
sess := engine.NewSession("", "", "", nil)
sess.SetProvider("xiaomi-mimo-token-plan") // hyphenated as normalized at runtime
sess.SetModel("mimo-v2.5-pro")
applyLiveModelMetadata(sess, "xiaomi-mimo-token-plan", "mimo-v2.5-pro")
Expand Down
6 changes: 2 additions & 4 deletions cmd/chat_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestFormatConnectionContextLabel(t *testing.T) {
}
}

sess := &engine.Session{}
sess := engine.NewSession("", "", "", nil)
sess.AddUser(strings.Repeat("a", 4000))
m.session = sess
got = ansi.Strip(formatConnectionContextLabel(m, "131k"))
Expand Down Expand Up @@ -108,9 +108,7 @@ func TestChatConnectionStatus_WithModel(t *testing.T) {
_ = hawkconfig.SetActiveModel(ctx, "moonshotai/kimi-k2.6")
hawkconfig.RefreshConfigCredSnapshot(ctx)

sess := &engine.Session{}
sess.SetProvider("openrouter")
sess.SetModel("moonshotai/kimi-k2.6")
sess := engine.NewSession("openrouter", "moonshotai/kimi-k2.6", "", nil)

m := chatModel{session: sess}
got := m.chatConnectionStatus()
Expand Down
3 changes: 0 additions & 3 deletions cmd/chat_sticky_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import (
lipgloss "charm.land/lipgloss/v2"
)

// stickyHeaderHeight is the maximum number of lines the sticky header occupies.
const stickyHeaderHeight = 2

// lastUserPromptBeforeScroll finds the content of the most recent user message
// that has scrolled above the visible viewport area. Returns empty if the
// viewport is at the top or no user message is found.
Expand Down
12 changes: 6 additions & 6 deletions cmd/chat_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,18 +227,17 @@ func (m chatModel) computeChatBottomBarLines() int {
footerW = 80
}
inputBoxLines := m.measureInputBoxLines(footerW)
lines := 1 + inputBoxLines // container/model row + input box (measured)
lines := 1 + 1 + inputBoxLines // 1 top chrome divider + 1 container/model row + input box (measured)
if val := m.input.Value(); strings.Count(val, "\n") > 0 {
lines++ // multiline indicator row ("¶ N lines (Shift+Enter for newline)")
}
if m.ghostText != nil {
if ghost := m.ghostText.Get(); ghost != "" && m.input.Value() == "" {
lines++
}
}
lines += m.visibleSlashSuggestionLines()
lines++ // primary session stats row (tokens · cost · duration)
if footerW >= 120 {
// Wide terminal: second stats row (autonomy, container, session ID, hints)
lines++
}
lines += len(renderStatusBar(&m, footerW)) // exact status bar line count
if m.manualCompacting {
lines += 2 // "Compacting conversation..." + progress bar
}
Expand Down Expand Up @@ -362,6 +361,7 @@ func (m chatModel) View() tea.View {
}
slashOpen := m.slashMenuOpen()
footerW := m.footerContentWidth(totalW)
bottomBar.WriteString(m.finishFooterLine("", totalW) + "\n")
leftRendered := renderContainerFooterLeft(m)
modelRendered, _, ctxRendered, ctxVisLen := m.renderConnectionStatusSplit()
rightLine := modelRendered
Expand Down
Loading
Loading