Skip to content

Troubleshooting Guide

Common issues and solutions for little-loops.

Quick Reference


Configuration Issues

Config file not found

Symptom: Commands use default values instead of project config

Cause: Missing or mislocated .ll/ll-config.json

Solution: 1. Run /ll:init to create config 2. Verify path is .ll/ll-config.json (not project root) 3. Check file permissions

# Verify config exists
ls -la .ll/ll-config.json

Invalid JSON in config

Symptom: JSONDecodeError on startup

Cause: Malformed JSON syntax

Solution: 1. Validate JSON syntax:

python -m json.tool .ll/ll-config.json
2. Common fixes: - Remove trailing commas after last items - Ensure all strings use double quotes - Check for missing closing braces

Type commands not running

Symptom: type_cmd ignored or errors

Cause: Tool not installed or set to wrong value

Solution: 1. Install the type checker:

pip install mypy  # For Python
npm install -g typescript  # For TypeScript
2. Set "type_cmd": null to disable type checking 3. Verify command works standalone:
mypy src/

Config values not substituting

Symptom: {{config.project.src_dir}} appears literally in output

Cause: Variable path doesn't match config structure

Solution: 1. Check available paths with:

from little_loops.config import BRConfig
from pathlib import Path
c = BRConfig(Path.cwd())
print(c.to_dict())
2. Use correct path (e.g., config.project.src_dir not config.src_dir)


Git Worktree Problems

Ghost git worktree refs after SIGKILL

Symptom: fatal: '<path>' already exists when ll-parallel starts a new run, even though .worktrees/worker-* directories look clean.

Cause: A SIGKILL mid-teardown can delete the on-disk worktree directory before git worktree prune commits. This leaves a ghost metadata entry at .git/worktrees/<name>/. The next git worktree add for the same path fails because git still believes the worktree exists.

How it's handled: The ll-parallel orchestrator now scans .git/worktrees/ at startup and prunes any entry whose on-disk path no longer exists before creating new worktrees. This is automatic and requires no manual action. (ENH-1246)

If you still see the error (e.g., older plugin version):

git worktree prune
git worktree list  # verify the ghost is gone

Worktree cleanup fails on locked worktree

Symptom: git worktree remove --force errors with "unable to remove" even with --force, leaving stale worktrees after a SIGKILL.

Cause: Older git versions do not honor --force on a locked worktree. The lock file must be removed explicitly first.

How it's handled: The worktree cleanup code in worktree_utils, merge_coordinator, and the orchestrator now runs git worktree unlock <path> before git worktree remove --force. This is automatic. (ENH-1251, ENH-1252, ENH-1253)

Manual fix if cleanup still fails:

git worktree unlock .worktrees/worker-<name>
git worktree remove --force .worktrees/worker-<name>
git worktree prune

Worktree creation fails

Symptom: "Failed to create worktree" or "fatal: ... already exists" in ll-parallel

Cause: Branch already exists, or worktree directory locked

Solution: 1. Clean up stale worktrees:

ll-parallel --cleanup
2. Delete orphaned branches:
git branch -D $(git branch | grep 'parallel/')
3. Remove leftover directories:
rm -rf .worktrees/
git worktree prune

Worktree permission denied

Symptom: Cannot write to worktree directory

Cause: Previous process left locks or wrong permissions

Solution:

# Remove worktrees directory
rm -rf .worktrees/

# Prune git's worktree tracking
git worktree prune

# Verify cleanup
git worktree list

Worktree not inheriting settings

Symptom: Claude uses wrong model or settings in worktree

Cause: .claude/settings.local.json not present or not copied

Solution: 1. Ensure main repo has .claude/settings.local.json 2. The worktree should inherit this automatically 3. Check worktree has access:

cat .worktrees/worker-1/.claude/settings.local.json

Git commands fail inside worktree sessions

Symptom: git status / git log errors or wrong repo inside an ll-parallel / ll-loop subprocess spawned in a worktree

Cause: In a git worktree, .git is a file (not a directory) pointing to the real gitdir. Some tools and subprocesses fail to resolve this automatically.

Solution: ClaudeCodeRunner.build_streaming() in host_runner.py automatically detects worktrees and sets GIT_DIR / GIT_WORK_TREE environment variables before spawning Claude (the legacy run_claude_command helper in subprocess_utils.py is retained as a public alias that now dispatches through host_runner). If you are invoking shell commands manually inside a worktree, set them yourself:

export GIT_DIR=$(cat .git | sed 's/gitdir: //')
export GIT_WORK_TREE=$(pwd)
git status   # now works correctly

Too many worktrees

Symptom: Disk space issues or "too many open files"

Cause: Worktrees not cleaned up after runs

Solution:

# List all worktrees
git worktree list

# Clean up all worktrees
ll-parallel --cleanup

# Or manually
git worktree remove .worktrees/worker-1 --force
git worktree prune


Claude CLI Issues

Command not found: claude

Symptom: FileNotFoundError or "claude: command not found"

Cause: Claude CLI not installed or not in PATH

Solution: 1. Install Claude CLI:

npm install -g @anthropic-ai/claude-code
2. Verify installation:
which claude
claude --version
3. Check PATH includes npm global bin directory

Claude exits with returncode 1

Symptom: Commands fail but no clear error message

Cause: Various - permission, timeout, API issues

Solution: 1. Run command manually to see full output:

claude -p "/ll:ready-issue BUG-001"
2. Check if --dangerously-skip-permissions is working 3. Verify API key/authentication status:
claude --auth-status

Timeout during issue processing

Symptom: Issue processing stops after timeout_seconds

Cause: Complex issues or slow API responses

Solution: 1. Increase timeout in config:

{
  "automation": {
    "timeout_seconds": 7200
  }
}
2. For parallel mode:
{
  "parallel": {
    "timeout_per_issue": 7200
  }
}
3. For loop queue waiting (ll-loop run --queue):
{
  "loops": {
    "queue_wait_timeout_seconds": 7200
  }
}
4. Use CLI flag: ll-auto --timeout 7200

Permission denied errors

Symptom: Claude refuses to execute commands

Cause: Missing --dangerously-skip-permissions or permission not granted

Solution: 1. The CLI tools add this flag automatically 2. For manual runs, add the flag explicitly 3. Check Claude Code permissions in your IDE settings

HostNotConfigured

Symptom: HostNotConfigured: <host> orchestration not yet wired (or No host CLI detected on PATH) when running ll-auto, ll-parallel, ll-sprint, ll-action, ll-loop, or an FSM evaluator.

Cause: resolve_host() in scripts/little_loops/host_runner.py could not find a runnable host. Either no supported host binary is on PATH, or LL_HOST_CLI / LL_HOOK_HOST explicitly selected a host (e.g., opencode, pi) whose runner is still a stub.

Solution: 1. Force Claude Code as the orchestration host:

export LL_HOST_CLI=claude-code
This is the safest fallback while non-Claude runners are still stubs. 2. Confirm a supported host binary is installed and on PATH:
which claude   # Claude Code
which codex    # Codex
3. Alternatively, set orchestration.host_cli in .ll/ll-config.json (see CONFIGURATION.md). 4. Stub runners (OpenCodeRunner, PiRunner) raise HostNotConfigured on every build_* call by design — track FEAT-1472 / FEAT-992 for completion.

See HOST_COMPATIBILITY.md — Orchestration CLI for the per-host matrix.


State Management

State file corruption

Symptom: JSONDecodeError when resuming

Cause: Interrupted during state save (power loss, kill -9, etc.)

Solution: 1. Delete the corrupted state file:

# For sequential mode
rm .auto-manage-state.json

# For parallel mode
rm .parallel-manage-state.json
2. Restart without --resume

Resume not working

Symptom: Reprocesses already-completed issues

Cause: State file deleted, wrong path, or different mode

Solution: 1. Check automation.state_file in config 2. Verify file exists before using --resume:

ls -la .auto-manage-state.json
3. Ensure you're using the same mode (sequential vs parallel)

Issues stuck in "attempted" state

Symptom: Issues skipped on future runs but not in completed

Cause: Issue failed but not recorded in failed_issues

Solution: 1. View current state:

cat .auto-manage-state.json | python -m json.tool
2. Edit state file to remove from attempted_issues:
# Or just delete and restart
rm .auto-manage-state.json

State file location confusion

Symptom: Can't find state file or using wrong one

Cause: Different state files for different modes

Solution: | Mode | Default State File | |------|-------------------| | Sequential (ll-auto) | .auto-manage-state.json | | Parallel (ll-parallel) | .parallel-manage-state.json |

Check your config for custom paths:

grep state_file .ll/ll-config.json


Session Handoff

Context monitor not triggering

Symptom: No warnings appear when context fills up

Cause: Plugin not active, jq missing, or context_monitor disabled in config

Solution: 1. Verify the ll@little-loops plugin is globally enabled in Claude Code — hooks fire automatically when the plugin is registered (no manual install step needed). Check with /ll:configure hooks show. 2. Verify jq is installed (required for the hook):

jq --version
3. Check context_monitor is not explicitly disabled in .ll/ll-config.json:
{
  "context_monitor": {
    "enabled": true
  }
}
4. Check state file is being updated:
cat .ll/ll-context-state.json

Reminders keep appearing after handoff

Symptom: "[ll] Context ~X% used" keeps showing after running /ll:handoff

Cause: Handoff file modification time not detected correctly

Solution: 1. Verify the file was created/modified:

ls -la .ll/ll-continue-prompt.md
2. Check handoff_complete in state file:
cat .ll/ll-context-state.json | jq '.handoff_complete'
3. Manually mark complete if needed:
# Edit .ll/ll-context-state.json and set "handoff_complete": true

Resume shows stale prompt

Symptom: Warning about prompt being N hours old

Cause: Continuation prompt older than prompt_expiry_hours (default: 24)

Solution: 1. Generate fresh prompt: /ll:handoff 2. Increase expiry in config:

{
  "continuation": {
    "prompt_expiry_hours": 72
  }
}
3. Stale prompts are still usable - the warning is informational

No continuation prompt found

Symptom: /ll:resume says "No continuation state found"

Cause: Handoff was never run or file deleted

Solution: 1. Run /ll:handoff to create the prompt 2. Check file location:

ls -la .ll/ll-continue-prompt.md
3. Check session state file:
cat .ll/ll-session-state.json 2>/dev/null || echo "No session state"

Automation not detecting handoff signal

Symptom: ll-auto or ll-parallel not spawning continuation sessions

Cause: Signal not in expected format or detection pattern mismatch

Solution: 1. Verify /ll:handoff outputs the signal:

CONTEXT_HANDOFF: Ready for fresh session
2. Check logs for detection:
# Look for "Detected CONTEXT_HANDOFF signal" in output
3. Verify continuation prompt exists in worktree:
cat .worktrees/worker-1/.ll/ll-continue-prompt.md

Max continuations reached

Symptom: "Reached max continuations (3), stopping"

Cause: Issue required more than 3 session restarts

Solution: 1. Increase limit in config:

{
  "continuation": {
    "max_continuations": 5
  }
}
2. Consider splitting the issue into smaller tasks 3. Check if issue is stuck in a loop (repeated handoffs without progress)

Output appears delayed or missing in external projects

Symptom: Progress output from ll-auto, ll-parallel, or ll-sprint appears delayed, batched, or not shown at all when running little-loops as a plugin from another project's directory.

Cause: Python's print() buffers output when stdout is not a TTY (e.g., when output is piped or redirected). The Logger class and ParallelOrchestrator use flush=True on all print calls to force immediate output, but earlier versions did not.

Solution: Upgrade to the version containing the fix (fix(logger): add flush=True to all print() calls for non-TTY output).

Verification: If you still see delayed output, check whether any direct print() calls in custom hooks or scripts are missing flush=True:

grep -rn "print(" hooks/ scripts/ | grep -v "flush=True"


Claude not seeing context warnings in non-interactive mode

Symptom: ll-auto or ll-parallel exhausts context without triggering handoff

Cause: The context-monitor.sh hook must use exit code 2 with stderr to send feedback to Claude in non-interactive mode

Verification: 1. Check the hook uses exit 2 and >&2:

grep -A2 "exit 2" hooks/scripts/context-monitor.sh
2. Look for hook output in Claude logs showing the warning was received

Reference: GitHub issue #11224 documents PostToolUse hook behavior - exit code 2 with stderr is required for feedback to reach Claude

Token estimation seems wrong

Symptom: Threshold triggers too early or too late

Cause: Default weights may not match your usage patterns

Solution: 1. Adjust estimation weights:

{
  "context_monitor": {
    "estimate_weights": {
      "read_per_line": 10,
      "tool_call_base": 100,
      "bash_output_per_char": 0.3
    },
    "context_limit_estimate": 1000000
  }
}

  1. Tool-specific costs (hardcoded in hooks/scripts/context-monitor.sh):
Tool Cost Formula
Read 10 per line lines × read_per_line
Grep 5 per line output_lines × 5
Glob 20 per file file_count × 20
Bash 0.3 per char chars × bash_output_per_char
Write/Edit 300 tool_call_base × 3
Task 2000 Fixed overhead
WebFetch 1500 Fixed overhead
WebSearch 1000 Fixed overhead
  1. Increase threshold for later warnings: "auto_handoff_threshold": 85
  2. Check token breakdown in state file:
    cat .ll/ll-context-state.json | jq '.breakdown'
    
  3. For custom tuning, edit hooks/scripts/context-monitor.sh lines 56-118

For comprehensive documentation, see Session Handoff Guide.


Priority and Filtering

Priority filter not working

Symptom: Wrong priority issues processed or issues skipped

Cause: Filename doesn't match pattern P[0-5]-*

Solution: 1. Verify filename format: P1-BUG-001-description.md 2. Priority must be at the start of filename 3. Check issues.priorities in config matches your prefixes:

{
  "issues": {
    "priorities": ["P0", "P1", "P2", "P3", "P4", "P5"]
  }
}

Category filtering wrong

Symptom: --category flag processes wrong issue type

Cause: Category name mismatch with config

Solution: 1. Use category keys from config: "bugs", "features", "enhancements" 2. Not the directory name or prefix 3. Check available categories:

from little_loops.config import BRConfig
from pathlib import Path
print(BRConfig(Path.cwd()).issue_categories)

No issues found

Symptom: "No issues to process" but issues exist

Cause: Wrong base_dir, category config, or file extension

Solution: 1. Check issue directory exists:

ls -la .issues/bugs/
2. Verify files have .md extension 3. Check config issues.base_dir matches actual location 4. Run diagnostic:
from little_loops.issue_parser import find_issues
from little_loops.config import BRConfig
from pathlib import Path
issues = find_issues(BRConfig(Path.cwd()))
print(f"Found {len(issues)} issues")
for i in issues[:5]:
    print(f"  {i.issue_id}: {i.title}")


Merge Conflicts

Merge fails repeatedly

Symptom: "Merge conflict after N retries"

Cause: Parallel workers modified same files

Solution: 1. Reduce workers temporarily:

ll-parallel --workers 1
2. Prioritize issues to avoid overlapping changes 3. Increase retry limit:
{
  "parallel": {
    "max_merge_retries": 5
  }
}

Local changes blocking merge

Symptom: "Your local changes would be overwritten"

Cause: Uncommitted changes in main repo

Solution: 1. Commit or stash changes before running:

git stash
ll-parallel
git stash pop
2. Recent versions auto-stash, but verify:
git stash list

Branch not found after merge

Symptom: Cleanup errors for already-deleted branch

Cause: Branch already deleted by previous cleanup

Solution: - Safe to ignore - cleanup is idempotent - Verify branches:

git branch -a | grep parallel/

Rebase conflicts during merge

Symptom: Merge coordinator reports rebase failure

Cause: Conflicting changes between workers

Solution: 1. The coordinator will retry automatically 2. If persists, check which files conflict:

git diff --name-only HEAD...parallel/BUG-001-branch
3. Consider processing conflicting issues sequentially


Loop Issues

Scope conflict blocks ll-loop run after an interrupted loop

Symptom: ll-loop run <name> fails with Scope conflict with running loop: <name>, but ll-loop status <name> reports status: interrupted (not running).

Cause: A foreground run exited (or was killed) without releasing its .lock file. The wrapper process that held the scope lock is gone, but the .lock file persists and blocks the next run. This can happen on hard kill, crash, or session disconnect.

Note: As of ENH-1605, "interrupted" loops (stopped via ll-loop stop or Ctrl-C) are resumable_reconcile_stale_runs() no longer sweeps them at startup. If you want to pick up where you left off, try ll-loop resume <name> first. If you actually want to discard the interrupted run and start fresh, use ll-loop stop to clear the lock and then re-run.

Solution for scope conflict: Run ll-loop stop <name>. The command checks the .lock file for a live PID regardless of status, terminates the orphaned process if alive, and removes the lock. If the PID is already dead it cleans up the stale file:

ll-loop resume <name>        # preferred: pick up where the loop left off
# — OR, to discard and restart —
ll-loop stop <name>          # terminate orphaned lock-holder or clean stale lock
ll-loop run <name> ...       # now unblocked
If ll-loop stop still reports "not running" (e.g. lock file is missing but scope conflict persists), check ll-loop status <name> --json for pid_source details and inspect .loops/.running/ directly.


Built-in loops not found after pip install

Symptom: ll-loop list --builtin returns an empty list, or ll-loop run <name> reports the loop cannot be found even though it's a known built-in (e.g., issue-refinement, dead-code-cleanup).

Cause: Versions of little-loops prior to v1.64.1 stored the built-in loop YAML files in a top-level loops/ directory that was not included in the Python wheel. Installing via pip install little-loops omitted those files, leaving get_builtin_loops_dir() pointing at a non-existent path. (BUG-885)

Solution: 1. Upgrade to v1.64.1 or later:

pip install --upgrade little-loops
2. Verify built-in loops are now available:
ll-loop list --builtin
3. If running from a development install (pip install -e .), re-run the install after pulling the fix:
pip install -e "./scripts[dev]"
ll-loop list --builtin


Slash Command Issues

Command not found

Symptom: /ll:command_name not recognized

Cause: Plugin not installed or wrong prefix

Solution: 1. Verify plugin is installed:

cat .claude/settings.local.json | grep enabledPlugins
2. Check prefix is ll: not br: (old prefix) 3. Run /ll:help to see available commands

Command uses wrong config values

Symptom: Command runs with default instead of project values

Cause: Config not loaded or variable path wrong

Solution: 1. Check config file exists and is valid JSON 2. Verify variable paths in command templates 3. Test config loading:

from little_loops.config import BRConfig
from pathlib import Path
c = BRConfig(Path.cwd())
print(c.project.test_cmd)  # Should show your config value

ready-issue always returns NOT_READY

Symptom: All issues fail validation

Cause: Issue file format doesn't match expected structure

Solution: 1. Check issue file has required sections 2. Verify title format: # ISSUE-ID: Title 3. Ensure issue describes a concrete, actionable task 4. Run manually to see full output:

claude -p "/ll:ready-issue BUG-001"

manage-issue completes but no changes

Symptom: Issue marked complete but code unchanged

Cause: Issue was deemed already fixed or invalid

Solution: 1. Check issue moved to completed/ directory 2. Look for closure notes in the file 3. Review git log for any commits 4. Run with verbose output to see reasoning


Hook Debugging

Hook not executing

Symptom: Expected hook behavior not occurring (context warnings, duplicate checks, etc.)

Cause: Hook disabled, missing dependencies, or execution errors

Solution: 1. Check hooks are registered in hooks/hooks.json 2. Verify hook script is executable:

chmod +x hooks/scripts/context-monitor.sh
chmod +x hooks/scripts/check-duplicate-issue-id.sh
chmod +x hooks/scripts/check-duplicate-issue-id-post.sh
chmod +x hooks/scripts/user-prompt-check.sh
chmod +x hooks/adapters/claude-code/precompact.sh
chmod +x hooks/adapters/claude-code/session-start.sh
chmod +x hooks/adapters/codex/session-start.sh
chmod +x hooks/adapters/codex/pre-compact.sh
chmod +x hooks/scripts/scratch-pad-redirect.sh

The OpenCode adapter at hooks/adapters/opencode/index.ts is a Bun-runtime TypeScript plugin and does not need chmod +x — it is loaded by the OpenCode plugin host rather than invoked as an executable file. The Codex CLI adapter scripts are bash shims (like the Claude Code ones) and do need chmod +x.

Manually exercise scratch-pad-redirect.sh with scratch_pad.enabled: true in .ll/ll-config.json:

# Bash rewrite (automation context): first token on allowlist triggers tee+tail
echo '{"tool_name":"Bash","tool_input":{"command":"pytest scripts/tests/"},"permission_mode":"bypassPermissions"}' \
  | bash hooks/scripts/scratch-pad-redirect.sh
# → allow with updatedInput.command = "pytest ... > .loops/tmp/scratch/pytest-$$.txt 2>&1; tail -20 ..."

# Read deny: oversized file with filtered extension returns a deny + Bash suggestion
yes line | head -500 > /tmp/big.txt
echo '{"tool_name":"Read","tool_input":{"file_path":"/tmp/big.txt"},"permission_mode":"bypassPermissions"}' \
  | bash hooks/scripts/scratch-pad-redirect.sh
# → deny with permissionDecisionReason suggesting cat > .loops/tmp/scratch/... && tail -20 ...
3. Check jq is installed (required by most hooks):
which jq || brew install jq  # macOS
which jq || apt install jq   # Linux

Hook timeout errors

Symptom: "Hook exceeded timeout" or hook execution aborted

Cause: Lock acquisition timeout or slow file operations

Solution: 1. Check for stale lock files:

find .claude -name "*.lock" -o -name "*.lock.lock"
rm -rf .claude/*.lock .claude/*.lock.lock
2. Verify hooks complete quickly (should be <1s normally):
time bash hooks/scripts/context-monitor.sh < test-input.json
3. Check hook timeout settings in hooks/hooks.json

State file corruption

Symptom: Hook crashes with JSON parse errors

Cause: Interrupted write operation or concurrent access bug

Solution: 1. Check state file is valid JSON:

jq empty .ll/ll-context-state.json 2>&1
jq empty .ll/ll-precompact-state.json 2>&1
2. Delete corrupted state files (they'll be recreated):
rm .ll/ll-context-state.json
rm .ll/ll-precompact-state.json
3. Verify atomic_write_json is being used: - Bash hook scripts source it from hooks/scripts/lib/common.sh - Python handlers import it from little_loops.file_utils (FEAT-1454 port)

Lock files not cleaned up

Symptom: Stale .lock files preventing hook execution

Cause: Hook process killed before cleanup or trap failure

Solution: 1. Remove all lock files:

# Clean up hook lock files
rm -rf .claude/.*.lock
rm -rf .claude/.*.lock.lock
rm -rf .issues/.*.lock
rm -rf .issues/.*.lock.lock
2. Check for hung processes:
ps aux | grep -E '(context-monitor|check-duplicate|user-prompt-check|precompact)'
3. Lock files should auto-expire (3s timeout for all hooks)

Duplicate issue ID not detected

Symptom: Multiple issues created with same ID despite hook, or two issues with different type prefixes share the same integer (e.g., BUG-007 and FEAT-007)

Cause: Race condition, hook not running on Write tool, or (before ENH-986) same-type-only collision detection

Scope: The hook enforces integer uniqueness across all types (BUG, FEAT, ENH, EPIC) in all subdirectories (bugs, features, enhancements, epics, completed, deferred). Writing FEAT-007 when BUG-007 already exists is denied.

Solution: 1. Verify hook is registered for PreToolUse:

grep -A5 "check-duplicate-issue-id" hooks/hooks.json
2. Test hook manually — same-type collision:
echo '{"tool_name":"Write","tool_input":{"file_path":".issues/bugs/P2-BUG-001-test.md"}}' | \
  bash hooks/scripts/check-duplicate-issue-id.sh
3. Test cross-type collision (integer 007 used by both BUG and FEAT):
# With P2-BUG-007-existing.md already present in .issues/bugs/:
echo '{"tool_name":"Write","tool_input":{"file_path":".issues/features/P2-FEAT-007-new.md"}}' | \
  bash hooks/scripts/check-duplicate-issue-id.sh
# Expected: "deny" with message about cross-type integer collision
4. Check if lock acquisition is timing out (reduce concurrent writes) 5. For TOCTOU race (parallel writes both pass PreToolUse check): verify check-duplicate-issue-id-post.sh is registered as PostToolUse Write hook and deletes the later-written duplicate reactively.

Context monitor not updating

Symptom: Token counts not increasing or warnings not appearing

Cause: Hook not running, state file issues, or disabled

Solution: 1. Enable context monitoring in config:

{
  "context_monitor": {
    "enabled": true
  }
}
2. Check state file is updating (watch both heuristic and authoritative counts):
watch -n 1 'cat .ll/ll-context-state.json | jq "{estimated_tokens, result_token_count, transcript_baseline_tokens}"'
When result_token_count > 0, the context monitor uses it directly (zero lag, most accurate). When result_token_count == 0, it falls back to transcript_baseline_tokens or pure heuristics. 3. Verify PostToolUse hook is running:
# Add this to context-monitor.sh temporarily for debugging
echo "Hook triggered at $(date)" >> /tmp/hook-debug.log
4. Test hook manually:
echo '{"tool_name":"Read","tool_response":{"content":"test\n"}}' | \
  bash hooks/scripts/context-monitor.sh

User prompt optimization not working

Symptom: Prompts not being optimized despite enabled setting

Cause: Bypass patterns matching or template file missing

Solution: 1. Check prompt doesn't start with bypass prefix (default: *):

{
  "prompt_optimization": {
    "bypass_prefix": "*"
  }
}
2. Verify template file exists:
ls -la hooks/prompts/optimize-prompt-hook.md
3. Test hook with a sample prompt:
echo '{"prompt":"implement user authentication system"}' | \
  bash hooks/scripts/user-prompt-check.sh

Testing individual hooks

Manual hook testing:

# Test context-monitor.sh
echo '{
  "tool_name": "Read",
  "tool_response": {"content": "line1\nline2\nline3\n"}
}' | bash hooks/scripts/context-monitor.sh

# Test check-duplicate-issue-id.sh (PreToolUse)
echo '{
  "tool_name": "Write",
  "tool_input": {"file_path": ".issues/bugs/P2-BUG-999-test.md"}
}' | bash hooks/scripts/check-duplicate-issue-id.sh

# Test check-duplicate-issue-id-post.sh (PostToolUse — reactive deletion)
# With an existing P2-BUG-999-original.md already present in .issues/bugs/:
echo '{
  "tool_name": "Write",
  "tool_input": {"file_path": ".issues/bugs/P2-BUG-999-duplicate.md"}
}' | bash hooks/scripts/check-duplicate-issue-id-post.sh
# Expected: exit 2, stderr message, duplicate file deleted

# Test user-prompt-check.sh
echo '{
  "prompt": "add authentication to the app"
}' | bash hooks/scripts/user-prompt-check.sh

# Test precompact handler (Python dispatcher; adapter wraps this)
echo '{
  "transcript_path": "/tmp/test.jsonl"
}' | python -m little_loops.hooks pre_compact

# Same handler from the OpenCode adapter's perspective: set LL_HOOK_HOST to
# reproduce the host identifier the OpenCode plugin injects.
echo '{
  "transcript_path": "/tmp/test.jsonl"
}' | LL_HOOK_HOST=opencode python -m little_loops.hooks pre_compact

# Same handler from the Codex CLI adapter's perspective: set LL_HOOK_HOST=codex.
# This also flips resolve_config_path() to probe .codex/ll-config.json first.
echo '{
  "transcript_path": "/tmp/test.jsonl"
}' | LL_HOOK_HOST=codex python -m little_loops.hooks pre_compact

Hook integration tests

Run the full integration test suite:

# Run all hook integration tests
python -m pytest scripts/tests/test_hooks_integration.py -v

# Run specific test class
python -m pytest scripts/tests/test_hooks_integration.py::TestContextMonitor -v

# Run with detailed output
python -m pytest scripts/tests/test_hooks_integration.py -v -s

Common lock issues

Symptom: "Failed to acquire lock" or timeout errors

Debugging: 1. Check if flock is available:

which flock || echo "Using mkdir-based fallback locks"
2. Verify lock timeout settings (in hook scripts): - context-monitor.sh: 3s timeout - check-duplicate-issue-id.sh: 3s timeout (PreToolUse lock) - check-duplicate-issue-id-post.sh: no lock (PostToolUse reactive deletion; overall hook timeout 5s) - little_loops.hooks.pre_compact: 3s lock timeout (Python handler invoked via hooks/adapters/claude-code/precompact.sh, hooks/adapters/opencode/index.ts on the session.compacted event, or hooks/adapters/codex/pre-compact.sh on Codex's PreCompact event) 3. Monitor lock files during operation:
watch -n 0.1 'find .claude .issues -name "*.lock*" 2>/dev/null'

Special character handling

Symptom: Prompts with special characters cause errors or injection

Verification: 1. Test with special characters:

echo '{"prompt":"test with $VAR and $(cmd) and & chars"}' | \
  bash hooks/scripts/user-prompt-check.sh
2. Check for sed usage (should use bash parameter expansion instead):
grep -n "sed" hooks/scripts/*.sh
# Should only appear in comments, not active code
3. Run integration tests:
python -m pytest scripts/tests/test_hooks_integration.py::TestUserPromptCheck -v


Diagnostic Commands

These commands use the Python API directly. See API Reference for full documentation.

Check configuration

Uses BRConfig to load and display resolved configuration:

# View resolved config
python -c "
from little_loops.config import BRConfig
from pathlib import Path
import json
c = BRConfig(Path.cwd())
print(json.dumps(c.to_dict(), indent=2))
"

Check issue discovery

Uses find_issues() to list all discovered issues:

# List all discovered issues
python -c "
from little_loops.issue_parser import find_issues
from little_loops.config import BRConfig
from pathlib import Path
issues = find_issues(BRConfig(Path.cwd()))
for i in issues:
    print(f'{i.priority} {i.issue_id}: {i.title}')
"

Check worktree status

# List all worktrees
git worktree list

# List parallel branches
git branch -a | grep parallel/

# Check worktree directory
ls -la .worktrees/

Check state files

# Sequential state
cat .auto-manage-state.json 2>/dev/null | python -m json.tool || echo "No sequential state"

# Parallel state
cat .parallel-manage-state.json 2>/dev/null | python -m json.tool || echo "No parallel state"

Verify Python package

# Check installation
pip show little-loops 2>/dev/null || pip show -e . 2>/dev/null

# Check CLI tools
which ll-auto
which ll-parallel

# Check version
python -c "import little_loops; print(little_loops.__file__)"

Getting Help

If you're still stuck:

  1. Check the README for basic setup
  2. Review ARCHITECTURE.md for system understanding
  3. Check API.md for module details

For persistent issues, create a bug report with: - Config file (sanitized - remove any secrets) - Command run and full output - Git status output - Python and Claude CLI versions