Skip to content
Closed
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
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The system consists of main bash scripts and a modular library system designed f
### Main Scripts

1. **smithers_loop.sh** - The main autonomous research loop that executes Claude Code repeatedly
- Checks graceful exit conditions BEFORE circuit breaker to ensure completion is detected first
- Exit priority: evidence-based completion → permission denials → minimum loops → exit_signal → safety limits
2. **smithers_monitor.sh** - Live monitoring dashboard for tracking research progress
3. **setup.sh** - Project initialization script for new Smithers research projects
4. **smithers_import.sh** - Research brief import tool that converts documents to Smithers format
Expand Down Expand Up @@ -59,6 +61,8 @@ The system uses a modular architecture with research-focused components:

5. **lib/circuit_breaker.sh** - Research-Specific Stagnation Detection
- Three states: CLOSED (normal), HALF_OPEN (monitoring), OPEN (halted)
- Recognizes SYNTHESIS.md creation/updates as evidence of research progress
- Won't open when research is complete (calls `is_research_complete()` as safety net)
- Research thresholds:
- `CB_SEARCH_REPETITION_THRESHOLD=3` - Repeated identical searches
- `CB_NO_NEW_SOURCES_THRESHOLD=5` - Loops without new sources
Expand All @@ -69,6 +73,7 @@ The system uses a modular architecture with research-focused components:
6. **lib/response_analyzer.sh** - Intelligent Research Response Analysis
- Analyzes Claude Code output for completion signals
- Detects SMITHERS_STATUS blocks with research-specific fields
- `exit_signal` from `.response_analysis` is read directly by `should_exit_gracefully()`
- Research patterns: `PHASE_COMPLETE_PATTERNS`, `RESEARCH_COMPLETE_PATTERNS`
- Session management with `.smithers/.smithers_session`

Expand Down Expand Up @@ -123,6 +128,22 @@ Smithers enforces a 5-phase research methodology:
- Provide actionable recommendations
- Requirements: All prior phases complete

### Research Completion Detection
Research is considered complete when any of these conditions are met (checked in order):
1. **SYNTHESIS.md marker**: File containing `RESEARCH_COMPLETE` (case-insensitive) found in any of: `.smithers/evidence/SYNTHESIS.md`, `.smithers/SYNTHESIS.md`, or `SYNTHESIS.md` at project root
2. **Phase status**: `current_phase == "COMPLETE"` in `phases/status.json`
3. **Threshold-based**: In SYNTHESIZE phase with all evidence thresholds met (12+ sources, 15+ claims, 6+ validations)

### Exit Priority Order
The loop checks completion conditions in this priority:
1. **Evidence-Based Completion** (PRIORITY 0): `is_research_complete()` — bypasses minimum loops
2. **Permission Denials** (PRIORITY 1): Blocked tools requiring `.smithersrc` updates
3. **Minimum Loops** (PRIORITY 2): Must reach `MIN_RESEARCH_LOOPS` (default: 3)
4. **Exit Signal** (PRIORITY 3): Claude set `EXIT_SIGNAL: true` in SMITHERS_STATUS
5. **Safety Limits** (PRIORITY 4): Test-only loop stagnation detection

Graceful exit is checked BEFORE the circuit breaker to prevent false stagnation halts.

## Key Commands

### Installation
Expand Down
27 changes: 27 additions & 0 deletions lib/circuit_breaker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,21 @@ record_loop_result() {
[[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Research progress - phase: $last_phase -> $current_phase" >&2
fi
fi

# Check if SYNTHESIS.md was created or updated (research completion artifact)
if [[ -f "$SMITHERS_DIR/evidence/SYNTHESIS.md" ]]; then
local synthesis_mtime
synthesis_mtime=$(stat -c %Y "$SMITHERS_DIR/evidence/SYNTHESIS.md" 2>/dev/null || \
stat -f %m "$SMITHERS_DIR/evidence/SYNTHESIS.md" 2>/dev/null || echo 0)
local last_synthesis_mtime=$(cat "$SMITHERS_DIR/.last_synthesis_mtime" 2>/dev/null || echo 0)
synthesis_mtime=$((synthesis_mtime + 0))
last_synthesis_mtime=$((last_synthesis_mtime + 0))
if [[ $synthesis_mtime -gt $last_synthesis_mtime ]]; then
research_progress=true
echo "$synthesis_mtime" > "$SMITHERS_DIR/.last_synthesis_mtime"
[[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Research progress - SYNTHESIS.md updated" >&2
fi
fi
# =========================================================================

# Determine if progress was made
Expand Down Expand Up @@ -229,6 +244,18 @@ record_loop_result() {
consecutive_no_progress=$((consecutive_no_progress + 1))
fi

# SAFETY: If research is complete, force progress=true to prevent CB from opening
# This handles the case where research is done but no new files are being created
if [[ "$has_progress" != "true" ]] && type is_research_complete &>/dev/null 2>&1; then
local _phases_file="$SMITHERS_DIR/phases/status.json"
local _evidence_dir="$SMITHERS_DIR/evidence"
if [[ -f "$_phases_file" ]] && [[ $(is_research_complete "$_phases_file" "$_evidence_dir") == "true" ]]; then
has_progress=true
consecutive_no_progress=0
[[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Research complete - overriding no-progress" >&2
fi
fi

# Detect same error repetition
if [[ "$has_errors" == "true" ]]; then
consecutive_same_error=$((consecutive_same_error + 1))
Expand Down
20 changes: 14 additions & 6 deletions lib/phase_state_machine.sh
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,21 @@ is_research_complete() {

# PRIMARY CHECK: SYNTHESIS.md with RESEARCH_COMPLETE marker = done
# This is the ground truth - if Claude wrote this, research is complete
local synthesis_file="$evidence_dir/SYNTHESIS.md"
if [[ -f "$synthesis_file" ]]; then
if grep -q "RESEARCH_COMPLETE" "$synthesis_file" 2>/dev/null; then
echo "true"
return 0
# Check multiple locations where Claude might write the synthesis
local synthesis_locations=(
"$evidence_dir/SYNTHESIS.md"
"${evidence_dir%/*}/SYNTHESIS.md"
"SYNTHESIS.md"
)

for synthesis_file in "${synthesis_locations[@]}"; do
if [[ -f "$synthesis_file" ]]; then
if grep -qi "RESEARCH_COMPLETE" "$synthesis_file" 2>/dev/null; then
echo "true"
return 0
fi
fi
fi
done

# FALLBACK: Phase-based completion (legacy)
if [[ "$current_phase" == "COMPLETE" ]]; then
Expand Down
88 changes: 69 additions & 19 deletions smithers_loop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ setup_tmux_session() {
# Split right pane horizontally (top: Claude output, bottom: status)
tmux split-window -v -t "$session_name:0.1" -c "$project_dir"

# Right-top pane (pane 1): Live Claude Code output
# Right-top pane (pane 1): Live research output
tmux send-keys -t "$session_name:0.1" "tail -f '$project_dir/$LIVE_LOG_FILE'" Enter

# Right-bottom pane (pane 2): Smithers status monitor
Expand All @@ -322,8 +322,9 @@ setup_tmux_session() {
smithers_cmd="'$smithers_home/smithers_loop.sh'"
fi

# Always use --live mode in tmux for real-time streaming
smithers_cmd="$smithers_cmd --live"
# Note: --live streaming mode (stdbuf | tee | jq pipeline) is fragile and can
# cause Claude to receive SIGSTOP. Background mode works reliably with the
# tail -f pane already providing real-time output via the log file.

# Forward --calls if non-default
if [[ "$MAX_CALLS_PER_HOUR" != "100" ]]; then
Expand Down Expand Up @@ -570,7 +571,23 @@ should_exit_gracefully() {
fi

# =================================================================
# PRIORITY 2: Safety limits (prevent infinite loops)
# PRIORITY 3: Direct exit_signal from response analysis
# If Claude explicitly signaled EXIT_SIGNAL: true in its output,
# respect it as a completion signal.
# =================================================================
if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
local exit_signal=$(jq -r '.analysis.exit_signal // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
if [[ "$exit_signal" == "true" ]]; then
local evidence_counts=$(get_evidence_counts)
log_status "SUCCESS" "Exit condition: Claude exit signal (exit_signal=true)" >&2
log_status "INFO" "Evidence: $evidence_counts" >&2
echo "exit_signal"
return 0
fi
fi

# =================================================================
# PRIORITY 4: Safety limits (prevent infinite loops)
# =================================================================
if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
echo ""
Expand Down Expand Up @@ -779,15 +796,15 @@ init_claude_session() {
# Handle stat failure (-1) - treat as needing new session
# Don't expire sessions when we can't determine age
if [[ $age_hours -eq -1 ]]; then
log_status "WARN" "Could not determine session age, starting new session"
log_status "WARN" "Could not determine session age, starting new session" >&2
rm -f "$CLAUDE_SESSION_FILE"
echo ""
return 0
fi

# Check if session has expired
if [[ $age_hours -ge $CLAUDE_SESSION_EXPIRY_HOURS ]]; then
log_status "INFO" "Session expired (${age_hours}h old, max ${CLAUDE_SESSION_EXPIRY_HOURS}h), starting new session"
log_status "INFO" "Session expired (${age_hours}h old, max ${CLAUDE_SESSION_EXPIRY_HOURS}h), starting new session" >&2
rm -f "$CLAUDE_SESSION_FILE"
echo ""
return 0
Expand All @@ -796,13 +813,13 @@ init_claude_session() {
# Session is valid, try to read it
local session_id=$(cat "$CLAUDE_SESSION_FILE" 2>/dev/null)
if [[ -n "$session_id" ]]; then
log_status "INFO" "Resuming Claude session: ${session_id:0:20}... (${age_hours}h old)"
log_status "INFO" "Resuming Claude session: ${session_id:0:20}... (${age_hours}h old)" >&2
echo "$session_id"
return 0
fi
fi

log_status "INFO" "Starting new Claude session"
log_status "INFO" "Starting new Claude session" >&2
echo ""
}

Expand Down Expand Up @@ -1049,6 +1066,15 @@ build_claude_command() {
CLAUDE_CMD_ARGS+=("--output-format" "json")
fi

# Auto-approve tool use for allowed tools (prevents interactive permission prompts)
local perm_mode="${CLAUDE_PERMISSION_MODE:-acceptEdits}"
case "$perm_mode" in
acceptEdits|default|plan) ;;
*) log_status "WARN" "Invalid CLAUDE_PERMISSION_MODE '$perm_mode', using 'acceptEdits'" >&2
perm_mode="acceptEdits" ;;
esac
CLAUDE_CMD_ARGS+=("--permission-mode" "$perm_mode")

# Add allowed tools (each tool as separate array element)
if [[ -n "$CLAUDE_ALLOWED_TOOLS" ]]; then
CLAUDE_CMD_ARGS+=("--allowedTools")
Expand All @@ -1066,7 +1092,11 @@ build_claude_command() {

# Add session continuity flag
if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
CLAUDE_CMD_ARGS+=("--continue")
if [[ -n "$session_id" ]]; then
CLAUDE_CMD_ARGS+=("--resume" "$session_id")
else
CLAUDE_CMD_ARGS+=("--continue")
fi
fi

# Add loop context as system prompt (no escaping needed - array handles it)
Expand Down Expand Up @@ -1376,6 +1406,9 @@ execute_claude_code() {
last_line=$(tail -1 "$output_file" 2>/dev/null | head -c 80)
# Copy to live.log for tmux monitoring
cp "$output_file" "$LIVE_LOG_FILE" 2>/dev/null
else
# Output file empty (JSON mode buffers until done) — show waiting status
echo -e "$(date '+%H:%M:%S') $progress_indicator Phase: $(get_current_phase) | Claude working... (${progress_counter}0s)" >> "$LIVE_LOG_FILE"
fi

# Update progress file for monitor
Expand Down Expand Up @@ -1415,6 +1448,23 @@ EOF

log_status "SUCCESS" "✅ Claude Code execution completed successfully"

# Write research summary to live.log for tmux monitoring
{
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Loop #$loop_count completed — $(date '+%Y-%m-%d %H:%M:%S')"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Extract the result text from JSON output
local result_text
result_text=$(jq -r '.result // empty' "$output_file" 2>/dev/null)
if [[ -n "$result_text" ]]; then
echo "$result_text"
else
echo "(No result text in output)"
fi
echo ""
} > "$LIVE_LOG_FILE"

# Save session ID from JSON output
if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
save_claude_session "$output_file"
Expand Down Expand Up @@ -1572,16 +1622,8 @@ main() {
break
fi

# Check circuit breaker before attempting execution
if should_halt_execution; then
reset_session "circuit_breaker_open"
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
log_status "ERROR" "🛑 Circuit breaker has opened - execution halted"
break
fi

# Check for graceful exit conditions BEFORE rate limiting
# This ensures we exit even if rate limited with research complete
# Check for graceful exit conditions BEFORE circuit breaker
# This ensures research completion is detected before the CB can halt the loop
local exit_reason=$(should_exit_gracefully)
if [[ "$exit_reason" != "" ]]; then
# Handle permission_denied specially
Expand Down Expand Up @@ -1642,6 +1684,14 @@ main() {
break
fi

# Check circuit breaker after graceful exit conditions
if should_halt_execution; then
reset_session "circuit_breaker_open"
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
log_status "ERROR" "🛑 Circuit breaker has opened - execution halted"
break
fi

# Check rate limits (AFTER exit conditions so we can exit even when rate limited)
if ! can_make_call; then
wait_for_reset
Expand Down
2 changes: 1 addition & 1 deletion smithers_status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ if [[ -f "$SYNTHESIS_FILE" ]]; then
echo " Modified: $MODIFIED"

# Check for completion markers
if grep -q "RESEARCH_COMPLETE" "$SYNTHESIS_FILE" 2>/dev/null; then
if grep -qi "RESEARCH_COMPLETE" "$SYNTHESIS_FILE" 2>/dev/null; then
echo -e "${GREEN}✅ Contains RESEARCH_COMPLETE marker${NC}"
COMPLETE=true
else
Expand Down
Loading
Loading