Skip to content

Commit f5568b6

Browse files
authored
docs: add Session god-object decomposition design sketch (#33)
* docs: remove generic LLM boilerplate ai_passage.md ai_passage.md was a 53-line, ~1000-word essay on the history and ethics of AI in general — entirely unrelated to the hawk project, no README/AGENTS.md/CHANGELOG.md reference to it. It looks like LLM-generated filler committed in '99261ca Fix CI formatting and toolchain hygiene' to satisfy a 'must have an essay' requirement that no longer applies. Untrack and delete. * fix(engine): respect ctx cancel on stream retry, surface self-review revert failure Three related fixes in the hawk agent loop: 1. **stream.go Stream retry no longer blocks ctx cancellation.** Previously: `time.Sleep(time.Duration(streamAttempt+1) * time.Second)` — a user-initiated cancel during a reasoning-only retry's backoff was ignored for up to 3s. Now uses a select with ctx.Done() so cancel is observed immediately, the in-flight stream is closed, and the loop exits with a structured error event. 2. **stream_tool_exec.go Self-review-before-apply now returns a hard error when the revert itself fails.** Previously: if the LLM said 'this diff is bad' and we tried to revert (os.Remove or os.WriteFile) and that syscall failed, the code only logged s.log.Warn and proceeded with the rejected diff still on disk. Now: capture the revert error, log it at Error level, and surface a hard tool error ('Self-review rejected the change AND the revert failed: <err>. Manual intervention required.') so the LLM can't continue building on top of code it just flagged as broken. 3. **internal/tool/tool.go now exports ReadOnlyTools + IsReadOnly() so the safe-concurrent allowlist has a single source of truth.** The map was previously duplicated in stream.go:716 and stream_tool_exec.go:29 with identical content — drift waiting to happen. Both call sites now go through tool.IsReadOnly(name), which canonicalises aliases (read/file_read, ls, etc.) before lookup. A new test (TestIsReadOnly, TestReadOnlyToolsSetContainsExpectedNames) locks the contract so a future removal of e.g. 'Read' from the allowlist fails CI in internal/tool/ rather than silently breaking concurrency classification in internal/engine/. * docs: add Session god-object decomposition design sketch hawk/internal/engine/session.go has 35 collaborators, 30 of them optional *T pointer fields. The agentLoop is an 833-line function that reads ~25 of these fields, creating a hidden web of dependencies. New contributors can't tell which field is required vs optional vs diagnostic, and unit tests can't construct a Session without wiring all 30 fields. This is a design proposal (NOT yet implemented) for breaking Session into 6 cohesive sub-services: - ChatService (150 LOC) — LLM transport, retry, circuit-breaker - MemoryService (200 LOC) — yaad bridge, recall, sleeptime, skill distill - ToolService (400 LOC) — registry, 15-stage post-call pipeline - PermissionService (150 LOC) — guardian, approval, budget tracking - LifecycleService (250 LOC) — self-improvement, beliefs, doom-loop - PersistenceService (300 LOC) — checkpoint, compaction, branching DAG The proposed 7-PR migration sequence is documented in the doc. Phases 2-7 are each independent and additive; each gets its own focused test suite. The 4 concrete fixes in the current PR (time.Sleep, ReadOnlyTools, AGENTS.md, self-review revert) are independent of this refactor and ship ahead of it. See: docs/session-decomposition.md (this file)
1 parent 2b1bf28 commit f5568b6

5 files changed

Lines changed: 384 additions & 23 deletions

File tree

docs/session-decomposition.md

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# Session God-Object Decomposition — Design Sketch
2+
3+
> Status: **DRAFT / NOT YET IMPLEMENTED**
4+
> Author: opencode session
5+
> Date: 2026-06-12
6+
> Scope: `hawk/internal/engine/session.go` (the 35-collaborator `Session` struct)
7+
8+
## Problem
9+
10+
`Session` at `hawk/internal/engine/session.go:42-141` is a god object:
11+
12+
- 35 fields, ~30 of them `*T` pointer collaborators with optional behavior
13+
- Every new feature adds a field (Beliefs, Critic, Trajectory, Steer­ing, etc.)
14+
- Construction requires wiring 30+ fields; unit testing is impractical
15+
- `NewSessionWithClient` (lines 149-187) already has a 35-line constructor body
16+
- The `AgentLoop` 833-line function reads/writes ~25 of these fields, creating a hidden web of dependencies
17+
- New contributors can't tell which field is required vs optional vs diagnostic
18+
19+
## Goal
20+
21+
Break `Session` into ~6 cohesive sub-services, each with:
22+
- a single responsibility
23+
- an interface (so tests can use stubs)
24+
- a clear lifecycle (created once in `NewSessionWithClient`, passed to `agentLoop`)
25+
26+
The `agentLoop` should consume these sub-services as named dependencies — no implicit `s.Beliefs.Size()` reach-throughs.
27+
28+
## Proposed Decomposition
29+
30+
### 1. `ChatService` — owns the LLM transport
31+
32+
**Owns:** `client ChatClient`, `provider string`, `model string`, `apiKeys map[string]string`, `Router *modelPkg.Router`, `DeploymentRouting bool`, `RateLimiter *ratelimit.Limiter`, `OutputSchema string`, `GLMThinkingEnabled *bool`, `ContCfg types.ContinuationConfig`, `ChatOptions{}` builder.
33+
34+
**Methods:**
35+
- `BuildOptions(systemPrompt string, tools []client.EyrieTool, activeModel string, maxTokens int) types.ChatOptions`
36+
- `Chat(ctx, msgs, opts) (*types.EyrieResponse, error)` — non-streaming, used by background goroutines
37+
- `Stream(ctx, msgs, opts) (*types.StreamResult, error)` — with retry, circuit-breaker record, rate-limit wait
38+
- `RecordSuccess(latency time.Duration)` / `RecordFailure(err error)`
39+
- `BuildContinuationConfig() types.ContinuationConfig`
40+
41+
**File:** `hawk/internal/engine/chat_service.go` (~150 LOC)
42+
43+
### 2. `MemoryService` — owns yaad bridge + recall/remember
44+
45+
**Owns:** `Memory MemoryRecaller`, `YaadBridge *memory.YaadBridge`, `EnhancedMemory *memory.EnhancedMemoryManager`, `SkillDistiller *memory.SkillDistiller`, `Sleeptime *memory.SleeptimeAgent`, `Activity *memory.ActivityTracker`, `AgentsAccum *prompts.AgentsAccum`, `FewShotStore *FewShotStore`, `AdaptivePrompt *AdaptivePrompt`.
46+
47+
**Methods:**
48+
- `RecallContext(ctx, lastUserMsg string, budget int) (string, error)` — unifies yaad + few-shot + agents-accum
49+
- `Remember(ctx, content, category string)` — wraps memory.Remember
50+
- `RunSleeptimeConsolidation(ctx, provider ChatService, messages []types.EyrieMessage)` — background
51+
- `RunSkillDistillation(ctx, provider ChatService, ...)` — background
52+
- `RecordFeedback(ctx, content string, action string)` — wraps EnhancedMemory feedback
53+
- `ShouldRemember(text string) bool` — heuristic for auto-remember
54+
55+
**File:** `hawk/internal/engine/memory_service.go` (~200 LOC)
56+
57+
### 3. `ToolService` — owns the registry + tool execution
58+
59+
**Owns:** `registry *tool.Registry`, `ContainerExecutor tool.ContainerExecutor`, `ContainerRequired bool`, `Snapshots SnapshotTracker`, `BackgroundManager *tool.BackgroundAgentManager`, `AllowedDirs []string`, `Protected PathProtector`.
60+
61+
**Methods:**
62+
- `Classify(calls []types.ToolCall) (concurrent, sequential []types.ToolCall)` — uses `tool.IsReadOnly`
63+
- `ExecuteAll(ctx, calls []types.ToolCall, ch chan<- StreamEvent, turn int, intent string) []toolExecResult` — the 15-stage post-call pipeline moved out of `stream_tool_exec.go`
64+
- `EstimateBlastRadius(calls []types.ToolCall) BlastReport`
65+
- `SpawnBackgroundAgent(ctx, prompt string) (id string, err error)`
66+
- `PollBackgroundAgent(id string) (output string, done bool, err error)`
67+
68+
**File:** `hawk/internal/engine/tool_service.go` (~400 LOC — biggest, owns the 15-stage pipeline)
69+
70+
### 4. `PermissionService` — owns the safety layer
71+
72+
**Owns:** `Perm *PermissionEngine`, `Permissions *PermissionMemory`, `AutoMode *permissions.AutoModeState`, `Classifier *permissions.Classifier`, `BypassKill *permissions.BypassKillswitch`, `Mode PermissionMode`, `Autonomy AutonomyLevel`, `PermissionFn func(PermissionRequest)`, `Approval *ApprovalGate`, `MaxBudgetUSD float64`.
73+
74+
**Methods:**
75+
- `CheckTool(ctx, info ToolCallInfo) (granted bool, denyMsg string)` — wraps Perm.CheckTool + Approval.CheckApproval
76+
- `ExceedsBudget(cost float64) bool`
77+
- `RecordToolCall(name string)`
78+
- `RecordCost(cost float64)`
79+
- `SetMode(mode string) error` — validates + applies
80+
- `SetMaxTurns(n int)`, `SetMaxBudgetUSD(usd float64)`
81+
82+
**File:** `hawk/internal/engine/permission_service.go` (~150 LOC)
83+
84+
### 5. `LifecycleService` — owns the self-improvement loop
85+
86+
**Owns:** `Lifecycle *SessionLifecycle`, `Reflector *Reflector`, `Beliefs *BeliefState`, `Backtrack *BacktrackEngine`, `Limits *LimitTracker`, `Critic *Critic`, `Backtrack`, `Trajectory *TrajectoryDistiller`, `Shadow *branching.ShadowWorkspace`, `Cascade *branching.CascadeRouter`, `Steering *SteeringQueue`, `LoopDet *LoopDetector`, `Snowball *branching.SnowballDetector`.
87+
88+
**Methods:**
89+
- `OnSessionStart(ctx, lastUserMsg string) (learnedCtx string)`
90+
- `OnSessionEnd(ctx, success bool, duration time.Duration, messages []types.EyrieMessage)`
91+
- `SelectModel(taskType, currentModel, hint string) string` — wraps Cascade
92+
- `OnToolFailure(ctx, intent string, history []types.EyrieMessage, errOutput string) (*Reflection, error)`
93+
- `RecordDecision(turn int, toolNames string, msgs []types.EyrieMessage)`
94+
- `CheckLimits(turnCount int) (cont bool, reason string)` — wraps Limits
95+
- `RecordTurn(turn int)`, `RecordToolCall(name string)`
96+
- `DetectDoomLoop(steps []ToolStep) bool`
97+
- `InjectSteering(messages []types.EyrieMessage) []types.EyrieMessage`
98+
99+
**File:** `hawk/internal/engine/lifecycle_service.go` (~250 LOC)
100+
101+
### 6. `PersistenceService` — owns checkpoint, session, yaad snapshot
102+
103+
**Owns:** `persistID string`, `checkpointMgr *session.CheckpointManager`, `lastPromptTokens int`, `lastCompletionTokens int`, `ConvoDAG *storage.DAG`, `OnCompaction OnCompaction`, `PinnedMessages int`, `AutoCompactThresholdPct int`, `ContextWindowCached int`, `AutoCompactor *AutoCompactor`, `CompactSplit`, `CompactProviderNative`, `CompactStrategyEngine`, `Files *FileTracker`.
104+
105+
**Methods:**
106+
- `PersistID() string`, `SetPersistID(id string)`
107+
- `ManageContextBeforeTurn(ctx) (strategy string, compacted bool)`
108+
- `Compact()`, `SmartCompact()`, `ShouldAutoCompact()`, `ShouldCompactByBudget()`
109+
- `AddUser(content string)`, `AddAssistant(content string)`, `AddUserWithImage(...)`, `AddUserWithAttachment(...)`
110+
- `AddUserWithDocumentText(content, label, extracted string)`
111+
- `AppendSystemContext(content string)`, `ReplaceSystemContextSection(header, content string)`
112+
- `Messages() []types.EyrieMessage`, `RawMessages()`, `MessageCount()`, `LoadMessages([]types.EyrieMessage)`
113+
- `RemoveLastExchange()`
114+
- `ConvoHead() string`, `ForkConversation(nodeID string) (string, error)`, `SwitchBranch(nodeID string) error`, `ListBranches(nodeID string) ([]*storage.DAGNode, error)`
115+
- `CheckpointOnCompaction(strategy string, before, after int, manual bool)`
116+
- `TokenTracking() (prompt, completion int)`, `RecordAPIUsage(prompt, completion int)`
117+
118+
**File:** `hawk/internal/engine/persistence_service.go` (~300 LOC)
119+
120+
## What stays on `Session`
121+
122+
After the refactor, `Session` becomes a thin facade:
123+
124+
```go
125+
type Session struct {
126+
// Identity (still here)
127+
ID string
128+
Provider string
129+
Model string
130+
SystemPrompt string
131+
// The 6 sub-services
132+
Chat *ChatService
133+
Memory *MemoryService
134+
Tools *ToolService
135+
Permission *PermissionService
136+
Lifecycle *LifecycleService
137+
Persistence *PersistenceService
138+
// Observability pass-through
139+
Tracer *oteltrace.Tracer
140+
Metrics *metrics.Registry
141+
Log *logger.Logger
142+
// Loop control (still on Session because the agent loop reads them every turn)
143+
Sandbox *DiffSandbox
144+
Plan *PlanState
145+
OutputSchema string
146+
Teach TeachConfig
147+
}
148+
```
149+
150+
~15 fields total, down from 35. The 6 sub-services are constructed once in `NewSessionWithClient` and stay for the session's lifetime.
151+
152+
## `agentLoop` becomes a 250-line orchestrator
153+
154+
```go
155+
func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) {
156+
defer close(ch)
157+
defer s.Lifecycle.OnSessionEnd(ctx, ...)
158+
159+
hooks.ExecuteAsync(ctx, hooks.EventSessionStart, ...)
160+
s.Lifecycle.OnSessionStart(ctx, s.lastUserMessage())
161+
162+
s.Persistence.ManageContextBeforeTurn(ctx)
163+
msgs := s.Persistence.Messages()
164+
sysCtx, err := s.Memory.RecallContext(ctx, s.lastUserMessage(), 2000)
165+
if err == nil && sysCtx != "" {
166+
s.Persistence.AppendSystemContext(sysCtx)
167+
}
168+
169+
activeModel := s.Lifecycle.SelectModel(taskType, s.Model, "")
170+
171+
for turn := 0; ; turn++ {
172+
if !s.Lifecycle.CheckLimits(turn) { return }
173+
174+
opts := s.Chat.BuildOptions(s.system, s.Tools.Registry().EyrieTools(), activeModel, maxTok)
175+
result, err := s.Chat.Stream(ctx, s.Persistence.Messages(), opts)
176+
if err != nil { /* error path */ }
177+
defer result.Close()
178+
179+
// Consume stream: textContent, toolCalls, sawThinking, stopReason
180+
...
181+
182+
if len(toolCalls) == 0 {
183+
s.Persistence.AddAssistant(textContent)
184+
s.Memory.AutoRemember(ctx, textContent)
185+
ch <- StreamEvent{Type: "done"}
186+
return
187+
}
188+
s.Persistence.AddAssistantWithToolUse(textContent, toolCalls)
189+
results := s.Tools.ExecuteAll(ctx, toolCalls, ch, turn, textContent)
190+
s.Persistence.AddToolResults(results)
191+
}
192+
}
193+
```
194+
195+
The loop body shrinks from 800 lines to ~250 because:
196+
- 15-stage tool pipeline moves to `ToolService.ExecuteAll`
197+
- 20+ post-call side effects (beliefs, memory, critic, shadow, sandbox, snapshot, agents-accum) move to sub-service methods
198+
- Stream retry with ctx-cancel moves to `ChatService.Stream`
199+
- Permission/approval flow moves to `PermissionService.CheckTool`
200+
201+
## Migration Plan
202+
203+
1. **Phase 1** (this PR's 4 fixes): just the time.Sleep, ReadOnlyTools, AGENTS.md, self-review fixes. **No Session refactor yet.**
204+
2. **Phase 2** (separate PR): extract `ChatService` first because it has the fewest field touches. Verify `agentLoop` builds and tests pass.
205+
3. **Phase 3**: extract `MemoryService` (most fields, but only consumed in the preamble/postamble of `agentLoop`).
206+
4. **Phase 4**: extract `PermissionService` (smallest surface).
207+
5. **Phase 5**: extract `LifecycleService` (biggest behavioral surface, but mostly reads `messages` and `tools`).
208+
6. **Phase 6**: extract `PersistenceService` (last because it owns the `messages []types.EyrieMessage` field which is touched everywhere).
209+
7. **Phase 7**: extract `ToolService` (depends on 4, 5; do last).
210+
8. **Phase 8**: delete fields from `Session`, add backwards-compatible shims that delegate to the new services, mark for removal in v0.2.0.
211+
212+
Each phase is a separate PR with a focused test suite.
213+
214+
## Testing Strategy
215+
216+
After the refactor, the following tests should be trivial (impossible to write today):
217+
218+
```go
219+
func TestAgentLoopRespectsContextCancellation(t *testing.T) {
220+
chat := &mockChatService{streamErr: ctx.Err()}
221+
session := &Session{Chat: chat, /* no other services wired */}
222+
stream, _ := session.Stream(ctx)
223+
// Assert: loop exits on first event
224+
}
225+
226+
func TestToolExecutionRejectsWithoutPermission(t *testing.T) {
227+
perm := &mockPermissionService{granted: false, denyMsg: "nope"}
228+
session := &Session{Permission: perm}
229+
result := session.Tools.ExecuteAll(ctx, calls, ch, 0, "")
230+
// Assert: all results are isErr=true with "nope" content
231+
}
232+
233+
func TestMemoryRecallReturnsEmptyWhenNoBridge(t *testing.T) {
234+
session := &Session{Memory: &MemoryService{}} // no YaadBridge
235+
ctx, _ := session.Memory.RecallContext(ctx, "test", 100)
236+
assert.Equal(t, "", ctx)
237+
}
238+
```
239+
240+
These tests don't need to construct a `Session` anymore; they can construct just the sub-service under test.
241+
242+
## Open Questions
243+
244+
1. Should `SubSession` (the sub-agent factory at `session.go:207-216`) become a method on `Session` or move to a dedicated `SubAgent` service? Currently it copies the parent's LLM transport — that's a `ChatService` concern, not a `Session` concern. Proposal: move to `SubAgentService`.
245+
2. Should the sub-service interfaces be exported (so external test packages can mock them) or kept internal? The `MockChatService` test above would require either exporting or building test-helper shims.
246+
3. Where do `OutputSchema`, `GLMThinkingEnabled`, `TeachConfig`, `Sandbox`, `Plan` belong? They're all chat/options concerns. Proposal: fold into `ChatService` or extract a `SessionOptions` struct that `ChatService.BuildOptions` reads.
247+
248+
## Estimated Effort
249+
250+
- Phase 1: ✅ done (this PR)
251+
- Phases 2-7: ~6 PRs, ~1500 LOC moved, ~800 LOC of `agentLoop` removed, ~600 LOC of new sub-service files added
252+
- Total: ~5 days of focused refactor work + tests
253+
254+
## Status
255+
256+
**NOT YET IMPLEMENTED.** The above is a design proposal pending review. The 4 concrete fixes (time.Sleep, ReadOnlyTools, AGENTS.md, self-review revert) are merged independently of this refactor.

internal/engine/stream.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/GrayCodeAI/hawk/internal/hooks"
1515
"github.com/GrayCodeAI/hawk/internal/observability/oteltrace"
1616
"github.com/GrayCodeAI/hawk/internal/resilience/retry"
17+
"github.com/GrayCodeAI/hawk/internal/tool"
1718
)
1819

1920
// Stream runs the agentic loop: LLM → tool_use → execute → loop.
@@ -476,7 +477,13 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) {
476477
"reason": retryReason,
477478
"error": streamErr.Error(),
478479
})
479-
time.Sleep(time.Duration(streamAttempt+1) * time.Second)
480+
select {
481+
case <-time.After(time.Duration(streamAttempt+1) * time.Second):
482+
case <-ctx.Done():
483+
ch <- StreamEvent{Type: "error", Content: "stream retry cancelled: " + ctx.Err().Error()}
484+
result.Close()
485+
return
486+
}
480487

481488
// Notify consumer to discard previously streamed content for this turn.
482489
ch <- StreamEvent{Type: "retry", Content: fmt.Sprintf("retrying after %s (attempt %d)", retryReason, streamAttempt+2)}
@@ -713,9 +720,8 @@ func (s *Session) agentLoop(ctx context.Context, ch chan<- StreamEvent) {
713720
// Auto-snapshot after write operations for granular undo
714721
if s.Snapshots != nil && len(toolCalls) > 0 {
715722
var writeNames []string
716-
safeConcurrent := map[string]bool{"Read": true, "Grep": true, "Glob": true, "LS": true, "WebSearch": true, "WebFetch": true, "ToolSearch": true}
717723
for _, tc := range toolCalls {
718-
if !safeConcurrent[tc.Name] {
724+
if !tool.IsReadOnly(tc.Name) {
719725
writeNames = append(writeNames, tc.Name)
720726
}
721727
}

internal/engine/stream_tool_exec.go

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ type toolExecResult struct {
2626

2727
// classifyToolCalls splits tool calls into concurrent (read-only) and sequential (write) batches.
2828
func classifyToolCalls(calls []types.ToolCall) (concurrent, sequential []types.ToolCall) {
29-
safeConcurrent := map[string]bool{"Read": true, "Grep": true, "Glob": true, "LS": true, "WebSearch": true, "WebFetch": true, "ToolSearch": true}
3029
for _, tc := range calls {
31-
if safeConcurrent[tc.Name] {
30+
if tool.IsReadOnly(tc.Name) {
3231
concurrent = append(concurrent, tc)
3332
} else {
3433
sequential = append(sequential, tc)
@@ -207,28 +206,33 @@ func (s *Session) executeSingleTool(ctx context.Context, tc types.ToolCall, ch c
207206
if newContent, readErr := readFileContent(preEditPath); readErr == nil && newContent != preEditContent {
208207
reviewResult, reviewErr := ReviewBeforeWrite(ctx, s.client, s.model, intentText, preEditPath, preEditContent, newContent)
209208
if reviewErr == nil && reviewResult != nil && !reviewResult.Approved {
210-
// Revert the file to its original state
209+
// Revert the file to its original state. If revert fails we
210+
// MUST surface that as a hard tool error: silently leaving
211+
// the rejected diff on disk would let a downstream turn
212+
// build on top of code the LLM just said was wrong.
213+
var revertErr error
211214
if preEditContent == "" {
212-
if removeErr := os.Remove(preEditPath); removeErr != nil {
213-
s.log.Warn("failed to remove file during self-review revert", map[string]interface{}{
214-
"path": preEditPath,
215-
"error": removeErr.Error(),
216-
})
217-
}
215+
revertErr = os.Remove(preEditPath)
218216
} else {
219-
if writeErr := os.WriteFile(preEditPath, []byte(preEditContent), 0o644); writeErr != nil {
220-
s.log.Warn("failed to revert file during self-review", map[string]interface{}{
221-
"path": preEditPath,
222-
"error": writeErr.Error(),
223-
})
224-
}
217+
revertErr = os.WriteFile(preEditPath, []byte(preEditContent), 0o644)
225218
}
226-
issueStr := "Self-review found issues: " + strings.Join(reviewResult.Issues, "; ")
227-
if len(reviewResult.Suggestions) > 0 {
228-
issueStr += ". Suggestions: " + strings.Join(reviewResult.Suggestions, "; ")
219+
if revertErr != nil {
220+
s.log.Error("self-review revert failed; rejecting diff loudly", map[string]interface{}{
221+
"path": preEditPath,
222+
"error": revertErr.Error(),
223+
})
224+
output = fmt.Sprintf("Self-review rejected the change AND the revert failed: %s. "+
225+
"Original review issues: %s. Manual intervention required.",
226+
revertErr.Error(), strings.Join(reviewResult.Issues, "; "))
227+
isErr = true
228+
} else {
229+
issueStr := "Self-review found issues: " + strings.Join(reviewResult.Issues, "; ")
230+
if len(reviewResult.Suggestions) > 0 {
231+
issueStr += ". Suggestions: " + strings.Join(reviewResult.Suggestions, "; ")
232+
}
233+
output = issueStr + ". Please fix these issues and try again."
234+
isErr = true
229235
}
230-
output = issueStr + ". Please fix these issues and try again."
231-
isErr = true
232236
} else if reviewErr == nil && reviewResult != nil && reviewResult.Approved {
233237
// Append diff summary to output for TUI display
234238
diffSummary := generateDiffSummary(preEditContent, newContent, preEditPath)

0 commit comments

Comments
 (0)