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
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ Do NOT import legacy UI/server modules into core. Do NOT "fix the UI" during Gol
### ReAct Agent Tools (7)
`read_file`, `edit_file`, `create_file`, `run_command`, `run_tests`, `search_codebase`, `list_files`

### Stall Detection
If the agent makes no tool calls for a configurable duration, the stall monitor kills execution and creates a blocker.

| Setting | Default | Description |
|---------|---------|-------------|
| `--stall-timeout` CLI flag | 300s | Seconds without a tool call before agent is considered stalled (0 = disabled) |
| `agent_budget.stall_timeout_s` in `.codeframe/config.yaml` | 300 | Same, configured via project config |

On stall: a blocker is created with context about the stall, and the task transitions to BLOCKED.

### Model Selection
- **PLANNING** → claude-sonnet-4-20250514 (complex reasoning)
- **EXECUTION** → claude-sonnet-4-20250514 (balanced)
Expand Down
13 changes: 12 additions & 1 deletion codeframe/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1998,6 +1998,11 @@ def work_start(
"--engine",
help="Agent engine: 'react' (default, ReAct tool-use loop) or 'plan' (legacy step-based)",
),
stall_timeout: int = typer.Option(
300,
"--stall-timeout",
help="Seconds without a tool call before agent is considered stalled (0 = disabled)",
),
Comment on lines +2001 to +2005

@coderabbitai coderabbitai Bot Mar 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The CLI default currently overrides workspace config.

Both commands give stall_timeout a concrete default and always pass it downstream, so omitting --stall-timeout is indistinguishable from explicitly choosing 300. That makes .codeframe/config.yaml's agent_budget.stall_timeout_s ineffective on the main CLI execution paths. Use an "unset" sentinel at the CLI boundary and let core resolve config/defaults.

Also applies to: 2068-2070, 2872-2876, 2965-2975

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/cli/app.py` around lines 2001 - 2005, The CLI is forcing a concrete
default for stall_timeout which overrides workspace
config.agent_budget.stall_timeout_s; change the typer Option declarations for
stall_timeout to use an "unset" sentinel (e.g., default None / Optional[int]) so
omitted flags are distinguishable from an explicit 300, and propagate None
downstream so core code can resolve using config defaults
(agent_budget.stall_timeout_s). Update every occurrence of the stall_timeout CLI
Option (the current stall_timeout parameters in app.py and the other occurrences
you noted) to be optional/unset at the boundary and ensure callers that forward
this value treat None as “use config” rather than a numeric timeout.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern. The CLI default pattern matches how --engine works today (also has a concrete default that overrides config). For now this is acceptable — the config precedence layer (issue #399 step 6 in the plan) was intentionally deferred as YAGNI. If users actually set stall_timeout_s in config and expect it to be respected from CLI, we can add sentinel-based detection in a follow-up.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankbria, understood — the concrete default matches the existing --engine precedence pattern and the config-first resolution is a planned future concern. I'll note this for future reviews.

Would you like me to open a follow-up issue to track the sentinel-based CLI/config precedence work (config → CLI default → explicit CLI flag) when the time comes?


✏️ Learnings added
Learnt from: frankbria
Repo: frankbria/codeframe PR: 423
File: codeframe/cli/app.py:2001-2005
Timestamp: 2026-03-08T05:05:03.452Z
Learning: In codeframe/cli/app.py, the `--stall-timeout` CLI option (and `--engine`) intentionally uses a concrete default (300s) that overrides workspace config (`agent_budget.stall_timeout_s`). Config-first precedence resolution (sentinel-based detection where `None` means "use config") is deferred as YAGNI and tracked as a follow-up for a future PR. Do not flag this pattern as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/runtime.py : Default execution engine for agent tasks must be ReAct (Reasoning + Acting) with iterative tool-use loop (observe → think → act). Legacy plan-based engine available via `--engine plan` flag only.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/cli/app.py : Use `--engine react` (default ReAct engine) for most tasks; `--engine plan` (legacy) only for well-defined, predictable tasks

) -> None:
"""Start working on a task.

Expand Down Expand Up @@ -2062,7 +2067,7 @@ def work_start(
try:
state = runtime.execute_agent(
workspace, run, dry_run=dry_run, debug=debug, verbose=verbose,
engine=engine,
engine=engine, stall_timeout_s=stall_timeout,
)

if state.status == AgentStatus.COMPLETED:
Expand Down Expand Up @@ -2864,6 +2869,11 @@ def batch_run(
"--engine",
help="Agent engine: 'react' (default, ReAct tool-use loop) or 'plan' (legacy step-based)",
),
stall_timeout: int = typer.Option(
300,
"--stall-timeout",
help="Seconds without a tool call before agent is considered stalled (0 = disabled)",
),
) -> None:
"""Execute multiple tasks in batch.

Expand Down Expand Up @@ -2961,6 +2971,7 @@ def batch_run(
dry_run=False,
max_retries=max_retries,
engine=engine,
stall_timeout_s=stall_timeout,
)

# Show summary
Expand Down
20 changes: 13 additions & 7 deletions codeframe/core/conductor.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ class BatchRun:
completed_at: Optional[datetime]
results: dict[str, str] = field(default_factory=dict)
engine: str = "react"
stall_timeout_s: int = 300


def start_batch(
Expand All @@ -469,6 +470,7 @@ def start_batch(
max_retries: int = 0,
on_event: Optional[Callable[[str, dict], None]] = None,
engine: str = "react",
stall_timeout_s: int = 300,
) -> BatchRun:
"""Start a batch execution of multiple tasks.

Expand Down Expand Up @@ -515,6 +517,7 @@ def start_batch(
completed_at=None,
results={},
engine=engine,
stall_timeout_s=stall_timeout_s,
)

# Save to database
Expand Down Expand Up @@ -921,7 +924,7 @@ def _execute_serial_resume(
on_event("batch_task_started", {"task_id": task_id, "position": i + 1, "is_retry": True})

# Execute task via subprocess
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine)
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine, stall_timeout_s=batch.stall_timeout_s)

# Record result (overwrites previous result)
batch.results[task_id] = result_status
Expand Down Expand Up @@ -1089,7 +1092,7 @@ def _execute_retries(
print(f" Previous: {previous_status}")

# Execute task
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine)
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine, stall_timeout_s=batch.stall_timeout_s)

# Update result
batch.results[task_id] = result_status
Expand Down Expand Up @@ -1256,15 +1259,15 @@ def _execute_serial(
on_event("batch_task_started", {"task_id": task_id, "position": i + 1})

# Execute task via subprocess
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine)
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine, stall_timeout_s=batch.stall_timeout_s)

# If task is BLOCKED, try supervisor resolution
if result_status == RunStatus.BLOCKED.value:
supervisor = get_supervisor(workspace)
if supervisor.try_resolve_blocked_task(task_id):
# Supervisor resolved the blocker - retry the task
print(" [Supervisor] Retrying task after auto-resolution...")
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine)
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine, stall_timeout_s=batch.stall_timeout_s)

# Record result
batch.results[task_id] = result_status
Expand Down Expand Up @@ -1544,15 +1547,15 @@ def _execute_single_task(
on_event("batch_task_started", {"task_id": task_id, "position": position})

# Execute task via subprocess
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine)
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine, stall_timeout_s=batch.stall_timeout_s)

# If task is BLOCKED, try supervisor resolution
if result_status == RunStatus.BLOCKED.value:
supervisor = get_supervisor(workspace)
if supervisor.try_resolve_blocked_task(task_id):
# Supervisor resolved the blocker - retry the task
print(" [Supervisor] Retrying task after auto-resolution...")
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine)
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine, stall_timeout_s=batch.stall_timeout_s)

# Record result
batch.results[task_id] = result_status
Expand Down Expand Up @@ -1643,7 +1646,7 @@ def execute_task(task_id: str) -> tuple[str, str]:
on_event("batch_task_started", {"task_id": task_id, "parallel": True})

# Execute via subprocess
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine)
result_status = _execute_task_subprocess(workspace, task_id, batch.id, engine=batch.engine, stall_timeout_s=batch.stall_timeout_s)

# Record result (thread-safe due to GIL for simple dict operations)
batch.results[task_id] = result_status
Expand Down Expand Up @@ -1700,6 +1703,7 @@ def _execute_task_subprocess(
task_id: str,
batch_id: Optional[str] = None,
engine: str = "react",
stall_timeout_s: int = 300,
) -> str:
"""Execute a single task via subprocess.

Expand All @@ -1710,6 +1714,7 @@ def _execute_task_subprocess(
task_id: Task to execute
batch_id: Optional batch ID for process tracking (enables force stop)
engine: Agent engine to use ("plan" or "react")
stall_timeout_s: Stall detection timeout in seconds (0 = disabled)

Returns:
RunStatus value string (COMPLETED, FAILED, BLOCKED)
Expand All @@ -1719,6 +1724,7 @@ def _execute_task_subprocess(
sys.executable, "-m", "codeframe.cli.app",
"work", "start", task_id, "--execute",
"--engine", engine,
"--stall-timeout", str(stall_timeout_s),
]

process = None
Expand Down
3 changes: 3 additions & 0 deletions codeframe/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class AgentBudgetConfig:
max_iterations: int = 100
auto_fix_enabled: bool = True
early_termination_enabled: bool = True
stall_timeout_s: int = 300


@dataclass
Expand Down Expand Up @@ -153,6 +154,8 @@ def validate(self) -> list[str]:
errors.append(
"agent_budget.base_iterations must be between min_iterations and max_iterations"
)
if budget.stall_timeout_s < 0:
errors.append("agent_budget.stall_timeout_s must be >= 0 (0 = disabled)")

return errors

Expand Down
1 change: 1 addition & 0 deletions codeframe/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class EventType:
AGENT_AUTOFIX_APPLIED = "AGENT_AUTOFIX_APPLIED"
AGENT_EARLY_TERMINATION = "AGENT_EARLY_TERMINATION"
AGENT_BUDGET_CALCULATED = "AGENT_BUDGET_CALCULATED"
AGENT_STALL_DETECTED = "AGENT_STALL_DETECTED"

# Blocker events
BLOCKER_CREATED = "BLOCKER_CREATED"
Expand Down
123 changes: 90 additions & 33 deletions codeframe/core/react_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import json
import logging
import os
import threading
from pathlib import Path
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Callable, Optional
Expand All @@ -22,6 +23,7 @@
from codeframe.core.context import ContextLoader, TaskContext
from codeframe.core.events import EventType
from codeframe.core.fix_tracker import EscalationDecision, FixAttemptTracker, FixOutcome
from codeframe.core.stall_monitor import StallEvent, StallMonitor
from codeframe.core.models import AgentPhase, CompletionEvent, ErrorEvent, ProgressEvent
from codeframe.core.quick_fixes import apply_quick_fix, find_quick_fix
from codeframe.core.tools import AGENT_TOOLS, execute_tool
Expand Down Expand Up @@ -107,6 +109,7 @@ def __init__(
llm_provider: LLMProvider,
max_iterations: int = 30,
max_verification_retries: int = 5,
stall_timeout_s: float = 300,
event_publisher: Optional[EventPublisher] = None,
dry_run: bool = False,
verbose: bool = False,
Expand All @@ -119,6 +122,7 @@ def __init__(
self.llm_provider = llm_provider
self.max_iterations = max_iterations
self.max_verification_retries = max_verification_retries
self._stall_timeout_s = stall_timeout_s
self.event_publisher = event_publisher
self.dry_run = dry_run
self.verbose = verbose
Expand All @@ -129,6 +133,14 @@ def __init__(
self.fix_tracker = FixAttemptTracker()
self.blocker_id: Optional[str] = None

# Stall detection
self._stall_triggered = threading.Event()
self._stall_event: Optional[StallEvent] = None
self._stall_monitor = StallMonitor(
stall_timeout_s=stall_timeout_s,
on_stall=self._on_stall,
)

# Token budget tracking for conversation compaction
self._context_window_size: int = DEFAULT_CONTEXT_WINDOW
self._compaction_threshold: float = self._read_compaction_threshold()
Expand Down Expand Up @@ -187,45 +199,49 @@ def run(self, task_id: str) -> AgentStatus:

system_prompt = self._build_system_prompt(context)

status = self._react_loop(system_prompt)
if status == AgentStatus.FAILED:
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "max_iterations_reached",
})
self._emit_stream_error(task_id, "max_iterations_reached")
return status
self._stall_monitor.start(task_id)
try:
status = self._react_loop(system_prompt)
if status == AgentStatus.FAILED:
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "max_iterations_reached",
})
self._emit_stream_error(task_id, "max_iterations_reached")
return status

if status == AgentStatus.BLOCKED:
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "blocked",
})
self._emit_stream_error(task_id, "blocked")
return AgentStatus.BLOCKED
if status == AgentStatus.BLOCKED:
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "blocked",
})
self._emit_stream_error(task_id, "blocked")
return AgentStatus.BLOCKED

# Final verification with retry
passed, reason = self._run_final_verification(system_prompt)
if passed:
self._verbose_print(f"[ReactAgent] Task {task_id} completed: {AgentStatus.COMPLETED.name}")
self._emit(EventType.AGENT_COMPLETED, {"task_id": task_id})
self._emit_stream_completion(task_id)
return AgentStatus.COMPLETED
# Final verification with retry
passed, reason = self._run_final_verification(system_prompt)
if passed:
self._verbose_print(f"[ReactAgent] Task {task_id} completed: {AgentStatus.COMPLETED.name}")
self._emit(EventType.AGENT_COMPLETED, {"task_id": task_id})
self._emit_stream_completion(task_id)
return AgentStatus.COMPLETED

if reason == "escalated_to_blocker" or reason == "stall_detected":
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "blocked",
})
self._emit_stream_error(task_id, "blocked")
return AgentStatus.BLOCKED

if reason == "escalated_to_blocker":
self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "blocked",
"reason": "verification_failed",
})
self._emit_stream_error(task_id, "blocked")
return AgentStatus.BLOCKED

self._emit(EventType.AGENT_FAILED, {
"task_id": task_id,
"reason": "verification_failed",
})
self._emit_stream_error(task_id, "verification_failed")
return AgentStatus.FAILED
self._emit_stream_error(task_id, "verification_failed")
return AgentStatus.FAILED
finally:
self._stall_monitor.stop()
except Exception:
logger.exception("ReactAgent.run() failed for task %s", task_id)
self._emit(EventType.AGENT_FAILED, {
Expand Down Expand Up @@ -262,6 +278,20 @@ def _react_loop(self, system_prompt: str) -> AgentStatus:
prompt_summary = system_prompt[:200]

while iterations < self.max_iterations:
# Check for stall before each iteration
if self._stall_triggered.is_set():
stall_ctx = ""
if self._stall_event:
stall_ctx = (
f"Agent stalled: no tool call for {self._stall_event.elapsed_s:.0f}s "
f"(timeout: {self._stall_event.stall_timeout_s}s)"
)
self._create_text_blocker(
stall_ctx or "Agent stalled with no tool activity",
"stall_detected",
)
return AgentStatus.BLOCKED

self._verbose_print(f"[ReactAgent] Iteration {iterations + 1}/{self.max_iterations}")
self._emit(EventType.AGENT_ITERATION_STARTED, {
"task_id": self._current_task_id,
Expand Down Expand Up @@ -337,6 +367,11 @@ def _react_loop(self, system_prompt: str) -> AgentStatus:

result = self._execute_tool_with_lint(tc)

if not result.is_error:
self._stall_monitor.notify_tool_executed(
self._current_task_id, iterations,
)
Comment on lines +370 to +373

@coderabbitai coderabbitai Bot Mar 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Failing tool calls still count as activity.

The feature defines a stall as "no tool calls", but the timer is only reset for non-error results here. If the agent is actively iterating on failing run_tests or run_command calls, it will eventually be misclassified as stalled and blocked. Notify the monitor after every completed tool call, not only successful ones.

Minimal fix
                 result = self._execute_tool_with_lint(tc)
-
-                if not result.is_error:
-                    self._stall_monitor.notify_tool_executed(
-                        self._current_task_id, iterations,
-                    )
+                self._stall_monitor.notify_tool_executed(
+                    self._current_task_id,
+                    iterations,
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/core/react_agent.py` around lines 370 - 373, The stall monitor is
only notified on successful tool results (checks result.is_error) causing
failing tool calls like run_tests/run_command to not reset the stall timer;
change the logic in react_agent.py so that
self._stall_monitor.notify_tool_executed(self._current_task_id, iterations) is
called for every completed tool invocation (regardless of result.is_error)—i.e.,
move the notify_tool_executed call out of the result.is_error conditional so all
tool completions trigger the stall monitor update.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional design. A stall means the agent is not doing anything (e.g., LLM API hangs or agent loops without acting). When the agent is actively executing tools — even failing ones — it IS making progress (discovering errors, trying fixes). The existing fix_tracker and escalation system handles repeated failures. Stall detection is specifically for the case where nothing happens at all.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankbria, understood — that's a clear and well-reasoned separation of concerns. Stall detection (no tool calls at all, e.g., LLM hang) vs. repeated failure recovery (fix_tracker + escalation) are orthogonal problems handled by distinct systems. I'll withdraw the concern.


✏️ Learnings added
Learnt from: frankbria
Repo: frankbria/codeframe PR: 423
File: codeframe/core/react_agent.py:370-373
Timestamp: 2026-03-08T05:05:00.413Z
Learning: In `codeframe/core/react_agent.py`, `StallMonitor.notify_tool_executed()` is intentionally called only on *successful* tool results (`if not result.is_error`). A "stall" is defined as the agent executing zero tool calls (e.g., LLM API hang or agent looping without acting). Failing tool calls still constitute activity and are handled by the separate `fix_tracker` / escalation system. Do not flag this conditional as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/core/react_agent.py : ReAct agent must implement the observe → think → act loop using the 7 defined tools: read_file, edit_file, create_file, run_command, run_tests, search_codebase, list_files

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{react_agent,tools}.py : ReactAgent implementation must use 7 structured tools: `read_file`, `edit_file`, `create_file`, `run_command`, `run_tests`, `search_codebase`, `list_files`. These tools are defined in `core/tools.py` and must be called via tool-use protocol.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/runtime.py : Default execution engine for agent tasks must be ReAct (Reasoning + Acting) with iterative tool-use loop (observe → think → act). Legacy plan-based engine available via `--engine plan` flag only.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/core/{agent,react_agent,runtime}.py : Agent state transitions (IDLE, PLANNING, EXECUTING, BLOCKED, COMPLETED, FAILED) must be managed by the Agent class; runtime handles TaskStatus transitions (BACKLOG, READY, IN_PROGRESS, DONE, BLOCKED, FAILED). Agent must NOT call `tasks.update_status()` directly.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/core/agent.py : Do NOT update task status from agent.py; runtime handles all TaskStatus transitions based on agent state

Learnt from: frankbria
Repo: frankbria/codeframe PR: 360
File: codeframe/core/tools.py:651-735
Timestamp: 2026-02-09T02:52:03.968Z
Learning: In codeframe/core/tools.py, agent tool functions (read_file, list_files, search_codebase, edit_file, create_file, run_tests, run_command) must be stateless with signature (input_data: dict, workspace_path: Path, tool_call_id: str) -> ToolResult and must not emit events. They are separate from verification gates in codeframe/core/gates.py. Gate diagnostic events (GATES_STARTED, GATES_COMPLETED) belong in the verification pipeline (gates.run()), not inside individual tools. If tool observability is required, implement it at the execute_tool dispatcher level, not within the individual tool implementations.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{gates,agent,react_agent}.py : Verification gates must run incrementally after file changes (ruff) and finally (pytest, ruff, BUILD). Self-correction loop retries failed gates up to 5 times for ReAct and 3 times for plan engine with pattern-based quick fixes before LLM intervention.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{diagnostics,diagnostic_agent}.py : Failed task diagnosis via `cf work diagnose <task-id>` must use AI-powered analysis via `core/diagnostic_agent.py`. Analyze error logs, failed verification gates, and previous attempts to suggest root causes and recovery actions.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{fix_tracker,quick_fixes}.py : Self-correction loop must track fix attempts via `core/fix_tracker.py` to prevent repeating failed fixes. Normalize errors for comparison and detect escalation patterns (same error/file 3+ times). Use pattern-based quick fixes via `core/quick_fixes.py` before LLM intervention.

Learnt from: frankbria
Repo: frankbria/codeframe PR: 128
File: tests/agents/test_bottleneck_detection.py:486-500
Timestamp: 2025-12-17T19:21:40.014Z
Learning: In tests/agents/test_bottleneck_detection.py, test_skip_agents_below_threshold should be async and mock _get_agent_workload to return workload below AGENT_OVERLOAD_THRESHOLD (5) while providing non-empty tasks list to prevent early return in detect_bottlenecks().


self._emit(EventType.AGENT_TOOL_RESULT, {
"task_id": self._current_task_id,
"tool_call_id": result.tool_call_id,
Expand Down Expand Up @@ -437,6 +472,9 @@ def _run_final_verification(
max_fix_turns = 5 # LLM turns per retry attempt

for attempt in range(1 + self.max_verification_retries):
if self._stall_triggered.is_set():
return (False, "stall_detected")

self._verbose_print("[ReactAgent] Running final verification...")
self._emit_progress(
AgentPhase.VERIFYING,
Expand Down Expand Up @@ -912,6 +950,25 @@ def _emit_stream_error(self, task_id: str, reason: str) -> None:
except Exception:
logger.debug("Failed to close task stream", exc_info=True)

# ------------------------------------------------------------------
# Stall detection
# ------------------------------------------------------------------

def _on_stall(self, event: StallEvent) -> None:
"""Callback invoked by StallMonitor when inactivity is detected."""
self._stall_event = event
self._stall_triggered.set()
self._verbose_print(
f"[ReactAgent] Stall detected: no tool call for {event.elapsed_s:.0f}s "
f"(timeout={event.stall_timeout_s}s, iterations={event.iterations_completed})"
)
self._emit(EventType.AGENT_STALL_DETECTED, {
"task_id": event.task_id,
"elapsed_s": event.elapsed_s,
"stall_timeout_s": event.stall_timeout_s,
"iterations_completed": event.iterations_completed,
})

# ------------------------------------------------------------------
# Blocker creation helpers
# ------------------------------------------------------------------
Expand Down
Loading
Loading