Skip to content

Architecture Overview

little-loops is a Claude Code plugin providing development workflow automation with issue management, code quality commands, and parallel processing capabilities.

Related Documentation: - Command Reference - All slash commands with usage - API Reference - Detailed class and method documentation - Troubleshooting - Common issues and solutions - README - Installation and quick start

System Components

The system consists of three main layers:

  1. Command Layer - Slash commands, skills, and agents for Claude Code
  2. Automation Layer - Python CLI tools for batch processing
  3. Configuration Layer - JSON-based project configuration

High-Level Architecture

flowchart TB
    subgraph "Claude Code Plugin"
        CMD[Commands<br/>28 slash commands]
        AGT[Agents<br/>9 specialized agents]
        SKL[Skills<br/>39 composable skills]
    end

    subgraph "Configuration"
        CFG[ll-config.json]
        SCHEMA[config-schema.json]
        TPL[little_loops/templates/*.json]
    end

    subgraph "Python Automation"
        CLI[cli/<br/>Entry points]
        AUTO[issue_manager.py<br/>Sequential processing]
        PARALLEL[parallel/<br/>Parallel processing]
    end

    subgraph "Issue Storage"
        ISSUES[.issues/<br/>bugs/, features/, enhancements/, epics/]
    end

    CFG --> CMD
    TPL --> CFG
    SCHEMA -.->|validates| CFG
    CFG --> CLI
    CMD --> AGT
    CMD --> SKL
    CLI --> AUTO
    CLI --> PARALLEL
    AUTO --> ISSUES
    PARALLEL --> ISSUES

Directory Structure

little-loops/
├── .claude-plugin/
│   └── plugin.json          # Plugin manifest
├── config-schema.json       # JSON Schema for validation (canonical source lives at scripts/little_loops/config-schema.json inside the Python package)
├── commands/                # 28 slash command templates
│   ├── help.md
│   ├── check-code.md
│   ├── run-tests.md
│   ├── scan-codebase.md
│   ├── normalize-issues.md
│   └── ...
├── agents/                  # 9 specialized agents
│   ├── codebase-analyzer.md
│   ├── codebase-locator.md
│   ├── codebase-pattern-finder.md
│   ├── consistency-checker.md
│   ├── loop-specialist.md
│   ├── plugin-config-auditor.md
│   ├── prompt-optimizer.md
│   ├── web-search-researcher.md
│   └── workflow-pattern-analyzer.md
├── hooks/                   # Lifecycle hooks and validation scripts
│   ├── hooks.json           # Hook configuration
│   ├── prompts/
│   │   └── continuation-prompt-template.md  # Handoff prompt template
│   ├── adapters/            # Host-specific adapters → little_loops.hooks dispatcher
│   │   ├── claude-code/
│   │   │   ├── post-tool-use.sh
│   │   │   ├── pre-tool-use.sh
│   │   │   ├── precompact-handoff.sh
│   │   │   ├── precompact.sh
│   │   │   ├── session-end.sh
│   │   │   └── session-start.sh
│   │   ├── opencode/        # OpenCode TS plugin adapter (Bun runtime)
│   │   │   ├── index.ts     # Plugin: session.created → session_start, session.compacted → pre_compact
│   │   │   ├── package.json
│   │   │   ├── tsconfig.json
│   │   │   └── README.md
│   │   └── codex/           # Codex CLI bash adapter — scripts and hooks.json moved in-package (FEAT-2274/BUG-2275)
│   │       └── README.md
│   └── scripts/             # Hook scripts
│       ├── check-duplicate-issue-id.sh
│       ├── check-duplicate-issue-id-post.sh
│       ├── context-monitor.sh
│       ├── precompact-state.sh  # Legacy shell handler; replaced by adapters/claude-code/precompact.sh
│       ├── scratch-pad-redirect.sh
│       ├── scratch-cleanup.sh
│       ├── session-cleanup.sh
│       ├── session-start.sh  # Legacy shell handler; replaced by adapters/claude-code/session-start.sh
│       ├── user-prompt-check.sh
│       └── lib/
│           └── common.sh    # Shared shell functions
├── skills/                  # 39 skill definitions
│   ├── analyze-history/     # Proactive
│   │   └── SKILL.md
│   ├── debug-loop-run/      # User-invoked
│   │   ├── SKILL.md
│   │   └── reference.md
│   ├── audit-loop-run/      # User-invoked
│   │   └── SKILL.md
│   ├── audit-claude-config/ # User-invoked
│   │   ├── SKILL.md
│   │   ├── report-template.md
│   │   └── wave1-prompts.md
│   ├── audit-docs/          # User-invoked
│   │   ├── SKILL.md
│   │   └── templates.md
│   ├── audit-issue-conflicts/ # User-invoked
│   │   └── SKILL.md
│   ├── capture-issue/       # Proactive
│   │   ├── SKILL.md
│   │   └── templates.md
│   ├── cleanup-loops/       # User-invoked
│   │   └── SKILL.md
│   ├── confidence-check/    # Proactive
│   │   ├── SKILL.md
│   │   └── rubric.md
│   ├── configure/           # User-invoked
│   │   ├── SKILL.md
│   │   ├── areas.md
│   │   └── show-output.md
│   ├── create-epics-from-unparented/ # User-invoked
│   │   ├── SKILL.md
│   │   └── agents/openai.yaml
│   ├── create-eval-from-issues/ # User-invoked
│   │   └── SKILL.md
│   ├── create-loop/         # User-invoked
│   │   ├── SKILL.md
│   │   ├── loop-types.md
│   │   ├── reference.md
│   │   └── templates.md
│   ├── format-issue/        # User-invoked
│   │   ├── SKILL.md
│   │   └── templates.md
│   ├── go-no-go/            # User-invoked
│   │   └── SKILL.md
│   ├── improve-claude-md/   # User-invoked
│   │   ├── SKILL.md
│   │   └── algorithm.md
│   ├── init/                # User-invoked
│   │   ├── SKILL.md
│   │   ├── interactive.md
│   │   └── templates.md
│   ├── issue-size-review/   # Proactive
│   │   └── SKILL.md
│   ├── issue-workflow/      # User-invoked
│   │   └── SKILL.md
│   ├── manage-issue/        # User-invoked
│   │   ├── SKILL.md
│   │   └── templates.md
│   ├── map-dependencies/    # Proactive
│   │   └── SKILL.md
│   ├── product-analyzer/    # User-invoked
│   │   └── SKILL.md
│   ├── review-loop/         # User-invoked
│   │   ├── SKILL.md
│   │   └── reference.md
│   ├── update/              # User-invoked
│   │   └── SKILL.md
│   ├── update-docs/         # User-invoked
│   │   └── SKILL.md
│   ├── workflow-automation-proposer/  # User-invoked
│   │   └── SKILL.md
│   └── distill-traces/      # User-invoked
│       └── SKILL.md
└── scripts/                 # Python package
    └── little_loops/
        ├── __init__.py
        ├── cli/                 # CLI entrypoints (package)
        │   ├── __init__.py
        │   ├── harness.py           # ll-harness one-shot runner evaluation CLI
        │   ├── auto.py
        │   ├── create_extension.py  # ll-create-extension scaffold CLI
        │   ├── parallel.py
        │   ├── messages.py
        │   ├── session.py           # ll-session: search/recent/backfill/path the unified session store
│   ├── history_context.py   # ll-history-context: render Historical Context block for an issue
        │   ├── sync.py
        │   ├── docs.py
        │   ├── history.py
        │   ├── deps.py              # ll-deps entry point
        │   ├── output.py            # Shared CLI output utilities (colors, terminal width)
        │   ├── sprint/
        │   │   ├── __init__.py      # Entry point (main_sprint) + argparse
        │   │   ├── _helpers.py      # Shared utilities
        │   │   ├── create.py        # create subcommand
        │   │   ├── edit.py          # edit subcommand
        │   │   ├── manage.py        # delete, analyze subcommands
        │   │   ├── run.py           # run subcommand
        │   │   └── show.py          # list, show subcommands
        │   ├── issues/
        │   │   ├── __init__.py      # Entry point (main_issues) + argparse
        │   │   ├── list_cmd.py      # list subcommand
        │   │   ├── next_id.py       # next-id subcommand
        │   │   ├── count_cmd.py     # count subcommand
        │   │   ├── search.py        # search subcommand
        │   │   ├── sequence.py      # sequence subcommand
        │   │   ├── impact_effort.py # impact-effort subcommand
        │   │   ├── show.py          # show subcommand
        │   │   ├── refine_status.py # refine-status subcommand
        │   │   ├── append_log.py    # append-log subcommand
        │   │   ├── anchor_sweep.py  # anchor-sweep subcommand (CLI wrapper)
        │   │   ├── fingerprint.py   # fingerprint subcommand (CLI wrapper)
        │   │   └── epic_progress.py # epic-progress subcommand
        │   ├── loop/
        │   │   ├── __init__.py      # Entry point (main_loop) + argparse
        │   │   ├── _helpers.py      # Shared utilities
        │   │   ├── run.py           # run subcommand
        │   │   ├── config_cmds.py   # validate, install
        │   │   ├── lifecycle.py     # status, stop, resume
        │   │   ├── info.py          # list, history, show
        │   │   └── testing.py       # ll-loop test/simulate subcommand utilities
        │   └── logs.py              # ll-logs: discover/extract/sequences/stats/tail/dead-skills/scan-failures subcommands + index generation
        ├── cli_args.py          # Argument parsing
        ├── config.py            # Configuration loading
        ├── state.py             # State persistence
        ├── logger.py            # Logging utilities
        ├── logo.py              # CLI logo display
        ├── frontmatter.py       # YAML frontmatter parsing
        ├── decisions.py         # Decisions and rules log data layer (FEAT-1891)
        ├── decisions_sync.py    # Decisions sync and session start integration (FEAT-1895)
        ├── learning_tests.py    # Learning test registry (CRUD for .ll/learning-tests/)
        ├── doc_counts.py        # Documentation count utilities
        ├── link_checker.py      # Link validation
        ├── issue_manager.py     # Sequential automation
        ├── issue_parser.py      # Issue file parsing
        ├── issue_discovery/     # Issue discovery and deduplication (package)
        │   ├── __init__.py      # Re-exports public API
        │   ├── matching.py      # Types and text similarity helpers
        │   ├── extraction.py    # Git history analysis and regression detection
        │   └── search.py        # Issue file search and discovery functions
        ├── issue_lifecycle.py   # Issue lifecycle operations
        ├── issue_progress.py    # Epic progress aggregation
        ├── issue_history/       # Issue history and statistics (package)
        ├── git_operations.py    # Git utilities
        ├── work_verification.py # Verification helpers
        ├── text_utils.py        # Text processing utilities
        ├── pii.py               # PII detection and redaction utilities
        ├── subprocess_utils.py  # Subprocess handling
        ├── host_runner.py       # Host CLI abstraction (HostRunner Protocol + ClaudeCodeRunner + CodexRunner + GeminiRunner + OmpRunner + OpenCodeRunner + PiRunner)
        ├── sprint.py            # Sprint definition and management
        ├── sync.py              # GitHub Issues sync
        ├── goals_parser.py      # Goals file parsing
        ├── dependency_graph.py  # Dependency graph construction
        ├── dependency_mapper/   # Cross-issue dependency discovery (sub-package)
        │   ├── __init__.py      #   Re-exports for backwards compatibility
        │   ├── models.py        #   Data models (DependencyProposal, FixResult, etc.)
        │   ├── analysis.py      #   Conflict scoring and dependency analysis
        │   ├── formatting.py    #   Report and graph formatting
        │   └── operations.py    #   File mutation operations (apply/fix)
        ├── issues/              # Issue utility sub-package (ENH-1300)
        │   ├── __init__.py      #   Package init
        │   ├── anchors.py       #   resolve_anchor(): language-agnostic backwards scan
        │   └── anchor_sweep.py  #   sweep_issues(): two-phase scan-and-rewrite
        ├── session_log.py       # Session log linking for issues
        ├── file_utils.py        # Shared file I/O utilities (atomic writes)
        ├── user_messages.py     # User message extraction
        ├── workflow_sequence/   # Workflow analysis (ll-workflows, sub-package)
        │   ├── __init__.py      #   Re-exports: analyze_workflows, models
        │   ├── analysis.py      #   Core analysis: boundaries, entity clustering
        │   ├── models.py        #   Data models (Workflow, SessionLink, etc.)
        │   └── io.py            #   YAML/JSON input-output helpers
        ├── fsm/                  # FSM loop execution engine
        │   ├── __init__.py
        │   ├── schema.py            # Loop schema definitions
        │   ├── fsm-loop-schema.json # JSON Schema for loop files
        │   ├── compilers.py         # YAML to FSM compilation
        │   ├── concurrency.py       # Concurrent loop execution
        │   ├── evaluators.py        # Condition evaluation
        │   ├── executor.py          # Loop execution
        │   ├── interpolation.py     # Variable interpolation
        │   ├── validation.py        # Schema validation
        │   ├── persistence.py       # State persistence
        │   ├── signal_detector.py   # Output signal detection
        │   ├── handoff_handler.py   # Session handoff handling
        │   └── rate_limit_circuit.py # Shared cross-worktree 429 circuit breaker
        ├── extension.py             # Extension protocol, loader, and reference implementation
        ├── testing.py               # Offline LLTestBus test harness for extension development
        ├── output_parsing.py        # Shared output parsing (ll-auto, ll-parallel)
        ├── output_cleaner.py        # Anti-event + duplicate-window tool/log pre-filter (FEAT-2470)
        ├── output/                  # Stop-sequence / prefill JSON output helpers (FEAT-2470)
        │   ├── __init__.py
        │   └── parse.py             # extract_between_tags, parse_prefilled_json
        ├── parallel/
        │   ├── __init__.py
        │   ├── orchestrator.py
        │   ├── worker_pool.py
        │   ├── merge_coordinator.py
        │   ├── priority_queue.py
        │   ├── git_lock.py
        │   ├── file_hints.py       # File hint extraction
        │   ├── overlap_detector.py  # File overlap detection
        │   ├── types.py
        │   └── tasks/
        │       ├── README.md
        │       ├── lint-all.yaml
        │       ├── test-suite.yaml
        │       ├── build-assets.yaml
        │       └── health-check.yaml
        ├── assets/                  # Package data: CLI assets
        │   └── ll-cli-logo.txt
        ├── templates/               # Package data: project-type configs and section templates
        │   ├── python-generic.json
        │   ├── javascript.json
        │   ├── typescript.json
        │   ├── go.json
        │   ├── rust.json
        │   ├── java-maven.json
        │   ├── java-gradle.json
        │   ├── dotnet.json
        │   ├── bug-sections.json
        │   ├── feat-sections.json
        │   ├── enh-sections.json
        │   ├── ll-goals-template.md
        │   ├── design-tokens/       # Built-in accessible default palette
        │   ├── extension/           # Extension scaffold templates (.tmpl)
        │   └── generic.json
        └── hooks/                   # (package hook modules + in-package prompt/adapter data)
            ├── prompts/
            │   └── optimize-prompt-hook.md  # Package data: prompt optimization template
            └── adapters/
                └── codex/
                    ├── hooks.json        # Package data: Codex adapter hooks template
                    ├── session-start.sh  # SessionStart → session_start (sets LL_HOOK_HOST=codex)
                    ├── pre-compact.sh    # PreCompact → pre_compact (sets LL_HOOK_HOST=codex)
                    ├── prompt-submit.sh  # UserPromptSubmit → user_prompt_submit (sets LL_HOOK_HOST=codex)
                    └── post-tool-use.sh  # PostToolUse → post_tool_use (sets LL_HOOK_HOST=codex)

Orchestration Layers

The three orchestration CLIs (ll-auto, ll-sprint, ll-parallel) are organized as a layered architecture (EPIC-1867). The full decomposition rationale lives in docs/research/ll-orchestrator-decomposition-plan-v0.2.md; this section summarizes the layers and their status.

flowchart TB
    subgraph L3["Layer 3 — ll-parallel (canonical parallel substrate, kept as Python)"]
        PAR[ParallelOrchestrator + WorkerPool + MergeCoordinator]
    end

    subgraph L2["Layer 2 — ll-sprint (wave driver + shim; FSM planned: FEAT-1899)"]
        SPRINT[DependencyGraph waves + contention refinement]
    end

    subgraph L1["Layer 1 — ll-auto (FSM + shim; planned: FEAT-2000/2001/2002)"]
        AUTO[Per-issue lifecycle: ready → manage → verify]
    end

    subgraph L0["Layer 0 — shared core (library + CLI subcommands; FEAT-1901)"]
        CORE[issue scan/parse, state, config, host_runner, worktree_utils]
    end

    SPRINT -- "multi-issue waves" --> PAR
    AUTO --> CORE
    SPRINT --> CORE
    PAR --> CORE
Layer Tool Role Status
0 (library) Shared orchestration core exposed as internal library + CLI subcommands In progress (FEAT-1901)
1 ll-auto Per-issue sequential lifecycle; target: FSM loop (loops/ll-auto.yaml) + thin CLI shim Python today; FSM migration planned (FEAT-2000/2001/2002)
2 ll-sprint Dependency-aware wave planning + execution; target: FSM wave driver + shim Python today; FSM wave driver planned (FEAT-1899)
3 ll-parallel Canonical parallel substrate — worker pool, git worktrees, merge coordination Kept as Python permanently — no FSM equivalent

Layer 3 is normative: ll-parallel is the canonical parallel substrate for the entire toolkit. The FSM engine has no concurrency primitive, so there is no FSM replacement for the worker pool / worktree / MergeCoordinator machinery — it stays Python. ll-sprint multi-issue waves already delegate to ParallelOrchestrator today, and the planned Layer-2 FSM wave driver will continue to shell out to it for multi-issue waves. Anything that needs concurrent issue processing builds on ll-parallel rather than reimplementing parallelism.


Sequential Mode (ll-auto)

The sequential mode processes issues one at a time in priority order.

sequenceDiagram
    participant User
    participant CLI as ll-auto
    participant Manager as AutoManager
    participant Claude as Claude CLI
    participant Git

    User->>CLI: ll-auto --max-issues 5
    CLI->>Manager: Initialize with config

    loop For each issue (priority order)
        Manager->>Manager: Find highest priority issue

        Note over Manager,Claude: Phase 1: Validation
        Manager->>Manager: expand_skill("ready-issue") → prompt string
        Manager->>Claude: expanded prompt (or /ll:ready-issue fallback)
        Claude-->>Manager: READY / NOT_READY / CLOSE

        alt READY
            Note over Manager,Claude: Phase 2: Implementation
            Manager->>Claude: /ll:manage-issue type action id
            Claude->>Git: Make changes
            Claude->>Git: Create commit
            Claude-->>Manager: Success

            Note over Manager,Git: Phase 3: Verification
            Manager->>Git: Update issue status: done
            Manager->>Manager: Verify completion
        else NOT_READY
            Manager->>Manager: Mark failed, skip
        else CLOSE
            Manager->>Git: Update issue status: done (closed)
        end

        Manager->>Manager: Save state
    end

    Manager-->>User: Summary report

Sequential Mode Components

Component File Purpose
AutoManager issue_manager.py Main orchestration loop
IssueParser issue_parser.py Parse issue files
StateManager state.py Persist state for resume
Logger logger.py Colorized console output

Parallel Mode (ll-parallel)

The parallel mode uses git worktrees to process multiple issues concurrently.

Per-EPIC integration branches (FEAT-2339). By default each worker forks from and merges back to parallel.base_branch. When parallel.epic_branches.enabled is true (config, or --epic-branches for a single run), the WorkerPool resolves each issue's nearest ancestor EPIC and routes all children of that EPIC onto one shared epic/<EPIC-ID>-<slug> integration branch — both as fork point and merge target — via WorkerResult.epic_branch. That integration branch forks from parallel.base_branch by default, but an EPIC may declare a base_branch: (alias target_branch:) frontmatter field to fork from a different ref; if a declared base does not resolve locally or on remote, ll-sprint dispatch hard-stops rather than degrading dependent children to a false partial (FEAT-2652). The MergeCoordinator merges the EPIC branch back to the base branch once the EPIC's last child completes (epic_branches.merge_to_base_on_complete), optionally opening a PR (epic_branches.open_pr). Standalone (parentless) issues keep the per-worker branch behavior unchanged. When epic_branches.verify_before_merge is true, that merge/PR-open is gated on a scratch-worktree run of test_cmd/lint_cmd against the EPIC branch tip; a failure blocks it, leaves the branch open for retry, and is surfaced in the run summary (ENH-2603).

epic_branches also has an FSM-loop-side consumer outside this WorkerPool path (ENH-2601): auto-refine-and-implement/sprint-refine-and-implement read parallel.epic_branches.enabled/.prefix to create (not check out) the epic/<EPIC-ID>-<slug> branch when scope resolves to an EPIC-NNN id, then run a post-implementation test_cmd/lint_cmd verify pass folded into summary.json. After each delegate pass, recheck_set re-resolves the EPIC's descendant set (transitive parent:-chain walk, ENH-2615) and cycles newly-decomposed children back through delegate — whose per-entry worktree attach re-attaches the same epic branch — so mid-run decomposition work also lands on the integration branch instead of bypassing it. Once all the EPIC's children are done, a merge_epic_branch state merges (or, per epic_branches.open_pr, opens a PR for) the branch back to base_branch, honoring merge_to_base_on_complete/verify_before_merge the same way the WorkerPool path above does (BUG-2614) — both paths share the same stateless free functions in little_loops.worktree_utils (verify_epic_branch_before_merge/merge_epic_branch_to_base/open_pr_for_epic_branch). Both call sites forward project.src_dir to verify_epic_branch_before_merge, which prepends the scratch worktree's source dir onto PYTHONPATH before running test_cmd/lint_cmd — so branch-only modules resolve to the worktree rather than the editable-install .pth's main checkout, which would otherwise false-fail collection for any EPIC branch that adds a new module (BUG-2629). See LOOPS_REFERENCE.md § auto-refine-and-implement.

flowchart TB
    subgraph Orchestrator["ParallelOrchestrator"]
        ORCH[Main Controller]
        QUEUE[IssuePriorityQueue]
        STATE[OrchestratorState]
    end

    subgraph Workers["Worker Pool"]
        POOL[WorkerPool]
        W1[Worker 1]
        W2[Worker 2]
        WN[Worker N]
    end

    subgraph Merge["Merge Coordinator"]
        MCOORD[MergeCoordinator]
        MQUEUE[Merge Queue]
    end

    subgraph Worktrees["Git Worktrees"]
        WT1[".worktrees/worker-1/"]
        WT2[".worktrees/worker-2/"]
        WTN[".worktrees/worker-N/"]
    end

    ORCH --> QUEUE
    ORCH --> STATE
    ORCH --> POOL

    POOL --> W1
    POOL --> W2
    POOL --> WN

    W1 --> WT1
    W2 --> WT2
    WN --> WTN

    W1 --> MCOORD
    W2 --> MCOORD
    WN --> MCOORD

    MCOORD --> MQUEUE

Parallel Processing Flow

sequenceDiagram
    participant Orch as Orchestrator
    participant Queue as PriorityQueue
    participant Pool as WorkerPool
    participant W1 as Worker 1
    participant W2 as Worker 2
    participant Merge as MergeCoordinator
    participant Git

    Note over Orch,Queue: Setup Phase
    Orch->>Queue: Scan and queue issues

    Note over Orch,Pool: Processing Phase
    Orch->>Pool: Start workers

    par Worker 1
        Pool->>W1: Process BUG-001
        W1->>Git: Create worktree + branch
        W1->>W1: Run ready-issue
        W1->>W1: Run manage-issue
        W1->>Git: Commit in worktree
        W1-->>Pool: WorkerResult
    and Worker 2
        Pool->>W2: Process BUG-002
        W2->>Git: Create worktree + branch
        W2->>W2: Run ready-issue
        W2->>W2: Run manage-issue
        W2->>Git: Commit in worktree
        W2-->>Pool: WorkerResult
    end

    Note over Pool,Merge: Merge Phase (Sequential)
    Pool->>Merge: Queue BUG-001 result
    Merge->>Git: Merge branch to main
    Merge-->>Orch: Merge complete

    Pool->>Merge: Queue BUG-002 result
    Merge->>Git: Merge branch to main
    Merge-->>Orch: Merge complete

    Note over Orch,Git: Cleanup Phase
    Orch->>Git: Remove worktrees
    Orch->>Git: Delete branches

Parallel Mode Components

Component File Purpose
ParallelOrchestrator orchestrator.py Coordinate all components
IssuePriorityQueue priority_queue.py Priority-based issue ordering
WorkerPool worker_pool.py Thread pool with worktrees
MergeCoordinator merge_coordinator.py Sequential merge queue

Extension Architecture & Event Flow

little-loops includes an extension architecture built on a structured event bus. Extensions implement the LLExtension protocol and receive LLEvent notifications from core subsystems. Topic-based filtering lets extensions subscribe only to the event namespaces they care about.

Components

Component File Purpose
LLEvent events.py Structured event dataclass (type, timestamp, payload)
EventBus events.py Multi-observer dispatcher with pluggable Transport sinks (defined in transport.py; SQLiteTransport in session_store.py): JsonlTransport, UnixSocketTransport, OTelTransport, WebhookTransport, SQLiteTransport
LLExtension extension.py Runtime-checkable protocol for event consumers
ExtensionLoader extension.py Discovers extensions from config paths and entry points
InterceptorExtension extension.py Protocol for plugins providing before_route/after_route hooks; stored in FSMExecutor._interceptors
ActionProviderExtension extension.py Protocol for plugins providing custom ActionRunner instances; populated into FSMExecutor._contributed_actions
EvaluatorProviderExtension extension.py Protocol for plugins providing custom evaluator callables; populated into FSMExecutor._contributed_evaluators
LLHookIntentExtension extension.py Protocol for plugins contributing hook intent handlers (provided_hook_intents()); detected via hasattr() in wire_extensions, merged into _HOOK_INTENT_REGISTRY in hooks/__init__.py
ReferenceInterceptorExtension extensions/reference_interceptor.py Passthrough reference implementation of InterceptorExtension; copy-paste starting point for custom interceptors

Event Emitters

The EventBus is wired into the following subsystems, which emit events at key lifecycle points:

Subsystem File Events Emitted
FSM Executor fsm/executor.py loop_start, state_enter, action_start, action_complete, loop_complete
StateManager state.py State persistence events (save, load, mark completed/failed)
Issue Lifecycle issue_lifecycle.py Issue status transitions (move, close, defer, skip, undefer) — emits issue.completed, issue.closed, issue.deferred, issue.skipped (from skip_issue()), issue.started (from undefer_issue()), issue.failure_captured
Parallel Orchestrator parallel/orchestrator.py Worker start/complete, merge events

Extensions are wired to the EventBus at CLI entry points via wire_extensions(), so they receive events from all subsystems during a run:

CLI Entry Point File Extensions Wired Transports Wired
ll-loop run cli/loop/run.py Yes — EventBus + FSMExecutor registry wired (interceptors, contributed actions/evaluators populated) Yes — wire_transports() after extensions; executor.close_transports() runs in finally before lock release
ll-loop resume cli/loop/lifecycle.py Yes — EventBus + FSMExecutor registry wired Yes — wire_transports() after extensions; executor.close_transports() runs in finally so transports flush on exit/exception
ll-loop monitor cli/loop/lifecycle.py No — read-only attach: does not instantiate PersistentExecutor or subscribe to EventBus; reads <instance-id>.events.jsonl from disk and forwards events to StateFeedRenderer. Ctrl-C detaches without signaling the loop process. No
ll-parallel cli/parallel.py Yes — EventBus only (no FSMExecutor wiring) Yes — wire_transports() after extensions; teardown runs in ParallelOrchestrator._cleanup() via event_bus.close_transports()
ll-sprint cli/sprint/run.py Yes — EventBus only (no FSMExecutor wiring for parallel branch) Yes — per-wave wire_transports() after extensions; teardown delegated to per-wave ParallelOrchestrator._cleanup()
ll-auto cli/auto.py No — EventBus is internal to AutoManager Yes — AutoManager.__init__() wires SQLiteTransport(db_path) directly; does not call wire_transports()

The transport layer fans events out additively: every event emitted on the EventBus is delivered to every registered observer and every registered transport. Built-in transports: JsonlTransport (durable file log; selected via events.transports: ["jsonl"]), UnixSocketTransport (real-time AF_UNIX streaming for local TUIs and dashboards; selected via events.transports: ["socket"], requires POSIX), OTelTransport (OpenTelemetry OTLP exporter; selected via events.transports: ["otel"], requires pip install 'little-loops[otel]'), WebhookTransport (batched HTTP POST to a remote endpoint; selected via events.transports: ["webhook"], requires pip install 'little-loops[webhooks]'), and SQLiteTransport (writes events to the per-project .ll/history.db unified session store; selected via events.transports: ["sqlite"], queryable via ll-session). Note: AutoManager.__init__() wires SQLiteTransport directly (not via the config-driven events.transports path), so ll-auto records issue lifecycle events without requiring "sqlite" in the project config.

UnixSocketTransport — initial state seeding: When a new client connects to events.sock, the transport immediately sends state_change events for all currently running loops (read from .loops/.running/*.state.json) before the client enters the regular event stream. This means a dashboard or TUI that connects mid-run receives the current FSM state of every active loop without waiting for the next state transition. Clients that connect before any loop is running receive no seed events (the event stream is empty until a loop starts).

OTel mapping: Each loop run becomes a trace. loop_start opens the root span; state_enter opens a child span (closing the prior state span); action_start/action_complete bracket a grandchild span; loop_complete closes all open spans and sets the trace status. Span events are recorded for evaluate, route, retry_exhausted, handoff_detected, handoff_spawned, and action_output on the innermost open span. loop_resume starts a new root span (new trace). Sub-loop events (depth > 0) are no-ops with a single per-session warning.

Webhook batching: WebhookTransport.send() enqueues non-blocking; a daemon thread POSTs accumulated events as a JSON array on each batch_ms tick. Failed POSTs retry with exponential backoff (up to 3 times, 0.5s–8s); after exhaustion the batch is dropped with a warning. close() does one final flush before joining the thread. New transports plug in through the same Transport protocol without changes to EventBus or the CLI wiring.

history.db schema versions: SQLiteTransport applies incremental PRAGMA user_version migrations on open. Each version adds tables or views without dropping prior ones.

Version Object Purpose
v1 tool_events, file_events, issue_events, correction_events Core event tables — tool calls, file reads/writes, issue lifecycle, user corrections
v2 message_events User and assistant message text for FTS search
v3 FTS5 index on message_events BM25 full-text search (ll-session search --fts)
v4 sessions One row per Claude Code session; indexed by session_id for ll-session path resolution (ENH-1710)
v5 issue_sessions VIEW Joins issue_events to message_events via overlapping timestamps; enables ll-history sessions <ID> and ll-session recent --issue <ID> (ENH-1711)
v6 last_backfill_ts meta key Enables incremental JSONL backfill at session start; session_start hook records the last-run timestamp so only newly-modified JSONL files are processed on subsequent starts (ENH-1830)
v7 skill_events Records /ll: skill invocations at dispatch time via the user_prompt_submit hook; enables ll-session recent --kind skill and FTS search with kind='skill' (ENH-1833)
v8 cli_events Records ll- CLI invocations via cli_event_context() in session_store.py; enables ll-session recent --kind cli (ENH-1848)
v9 idx_corrections_dedup Unique index on user_corrections(session_id, content) enabling idempotent INSERT OR IGNORE during correction mining; backfill() and backfill_incremental() call mine_corrections_from_messages() to retroactively populate corrections from message_events (ENH-1904)
v10 summary_nodes, summary_spans LCM-style hierarchical summary DAG (FEAT-1712): summary_nodes stores three-level LCM Algorithm 3 summaries (normal LLM → aggressive bullet-point LLM → deterministic truncation) as leaf and condensed nodes over message_events blocks; summary_spans links each node back to its source messages for lossless drill-down. Enables ll-session grep, ll-session expand, and ll-session describe. Compaction is opt-in via history.compaction.enabled in ll-config.json.
v11 assistant_messages Stores concatenated text blocks from assistant responses so the SFT pipeline can read conversation turn-pairs from the database instead of re-parsing JSONL (ENH-1942). Includes tool_use_count for filter predicates and idx_assistant_messages_dedup for idempotent backfill.
v12 summary_nodes.level, idx_summary_nodes_cross_dedup Adds level INTEGER DEFAULT 0 column to summary_nodes for N-level DAG traversal (0 = leaf/per-session condensed, 1+ = cross-session condensed, max = root) and a cross-session dedup index idx_summary_nodes_cross_dedup on (level, ts_start, ts_end) WHERE kind='condensed' AND session_id IS NULL (ENH-1953).
v13 correction_retirements Records addressed correction clusters (topic fingerprint + optional rule ID) so detect_recurring_feedback() excludes already-ruled topics from future runs; unique index on topic_fingerprint for idempotent inserts (ENH-2046).
v14 issue_snapshots Stores full issue content (title, priority, body, frontmatter) at key lifecycle transitions (captured, done, cancelled) so completed issue context is queryable from the DB even after the source .md file is moved or deleted. FTS via search_index with kind="snapshot" (ENH-2151).
v15 skill_events.exit_code/success/duration_ms Completion-side columns on skill_events, written by skill_event_context() (the skill-host analogue of cli_event_context()); dispatch-only rows keep NULL. Enables ll-session skill-stats per-skill success-rate rollups (ENH-2460).
v16 issue_events.session_id, idx_issue_events_session_id, rebuilt issue_sessions VIEW Authoritative session linkage captured at transition time by SQLiteTransport from the issue.* event payload; the timestamp-overlap heuristic is preserved as the deprecated legacy_issue_sessions_ts_overlap VIEW and the issue_sessions VIEW now prefers exact session_id joins, falling back to the legacy inference only for issues with no authoritative rows (ENH-2462).
v17 commit_events Ground-truth record of what shipped: (ts, commit_sha UNIQUE, parent_sha, message, author, branch, issue_id, files_json). Written live by record_commit_event() (post-commit hook hooks/scripts/record-commit-post-commitlittle_loops.hooks.post_commit) and retroactively by ll-session backfill walking git log --all; issue_id inferred from Closes/Fixes/Issue: references and branch naming. Enables ll-session recent --kind commit and FTS with kind="commit" (ENH-2458).
v18 test_run_events Persisted pytest run results (pass/fail/error/skip counts, duration, failing node IDs, env label, HEAD sha, branch, command) written best-effort by the little_loops.pytest_history_plugin pytest11 plugin via record_test_run_event(); opt out with PYTEST_DISABLE_PLUGIN_LL_HISTORY=1. Enables ll-session recent --kind test_run and FTS with kind="test_run" (ENH-2459).
v19 raw_events Verbatim-JSONL-line source of truth for the JSONL-derived cache tables (tool_events, message_events, assistant_messages, skill_events, sessions): (ts, session_id, host, source_path, line_no, event_type, raw_line, parsed_json, compacted, summary_node_id), unique on (source_path, line_no). ll-session backfill now ingests JSONL lines here only; ll-session rebuild wipes and re-derives the cache tables (plus user_corrections, summary_nodes/summary_spans, and the corresponding search_index rows) by replaying raw_events. ll-session compact [--and-prune] sweeps rows past analytics.retention.raw_event_max_age_days into per-session kind='retention' summary nodes and marks them compacted=1; ll-session prune now deletes only raw_events rows already marked compacted=1 (previously it deleted tool_events/cli_events/file_events/message_events directly and never touched search_index, leaving stale FTS rows — fixed by rebuild() always re-populating search_index from current state). The three legacy watermarks (last_backfill_ts, last_backfill_ts_assistant_messages, last_backfill_ts_skill_events) collapse to a single last_raw_event_ts meta key; a new last_rebuild_version key gates the SessionStart hook's opt-in-on-migration --rebuild pass. Issue/loop/commit/cli/file/test_run tables are outside this table's scope and keep their existing direct-write paths (ENH-2581).
v20 usage_events Real LLM token counts the API returned (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens) plus a derived cost_usd (via pricing.estimate_cost_usd, NULL for unpriced models) and a forward-compat nullable state column: (ts, session_id, model, state, input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, cost_usd). Derived from raw_events by _backfill_usage_events() — one row per assistant turn, parsing message.usage on type == "assistant" transcript records. state is always NULL on parser-written rows (the transcript carries no FSM-state boundary); reserved for a future live per-state writer. Enables ll-session recent --kind usage, FTS with kind="usage", and history_reader.recent_usage_events()/aggregate_usage() (ENH-2461).

Schema migration runs automatically; no manual ll-session backfill is needed for new tables. The issue_sessions VIEW requires captured_at populated on issue_events rows, which ll-session backfill seeds from on-disk sources for pre-v4 databases. As of ENH-1830, session_start automatically triggers an incremental backfill in a background thread, so new interactive session data is indexed without manual intervention.

Extension Loading

Extensions are loaded via two mechanisms: 1. Config paths: "extensions": ["my_package:MyExtension"] in ll-config.json 2. Entry points: importlib.metadata discovery under the little_loops.extensions group

Topic-Based Event Filtering

Extensions can declare an event_filter class attribute to subscribe only to specific event namespaces, using fnmatch glob patterns matched against the event's "event" key:

class MyExtension:
    event_filter = "fsm.*"          # only FSM lifecycle events
    # event_filter = ["fsm.*", "issue.*"]  # multiple namespaces
    # event_filter = None           # all events (default)

    def on_event(self, event: LLEvent) -> None:
        ...

wire_extensions() forwards event_filter to bus.register(). If the attribute is absent or None, the extension receives all events.

See API Reference — Extension API for full protocol, loader, and wire_extensions() documentation.


History DB: Producer→Consumer Flow

.ll/history.db is the per-project event history store — a SQLite database populated by hook writers and queryable in milliseconds without re-parsing JSONL or markdown. It provides agent context (user corrections, related file edits, prior issue work) to skills like refine-issue, ready-issue, and confidence-check without the overhead of full-log scanning.

Write Path

sequenceDiagram
    participant SS as session_start
    participant PTU as post_tool_use
    participant UPS as user_prompt_submit
    participant EB as EventBus
    participant ST as SQLiteTransport
    participant DB as history.db

    SS->>DB: ensure_db() — bootstrap schema (v1–v20)
    SS-->>DB: backfill_incremental() ingests JSONL into raw_events (background thread; --rebuild only when SCHEMA_VERSION > last_rebuild_version)
    PTU->>DB: tool_events / file_events (direct write, analytics.enabled)
    UPS->>DB: user_corrections / skill_events via record_correction() / record_skill_event()
    EB->>ST: emit(IssueEvent | LoopEvent)
    ST->>DB: INSERT INTO issue_events / loop_events

Read Path

flowchart TB
    DB[history.db]
    HR[history_reader.py]
    DB --> HR
    HR --> HC["ll-history-context CLI<br/>find_user_corrections + recent_file_events<br/>→ ## Historical Context block"]
    HR --> LS["ll-session CLI<br/>search + related_issue_events<br/>+ sessions_for_issue"]
    HR --> SK["Skills<br/>refine-issue / ready-issue / confidence-check<br/>/ create-sprint / scope-epic / manage-issue / review-epic"]
    HR --> SS2["session_start hook<br/>project_digest → render_project_context<br/>→ &lt;project_context&gt; block (ENH-1907)"]

Components

Component File Role
ensure_db() session_store.py Bootstrap schema (v1–v20 migrations) at session start
backfill_incremental() session_store.py Background JSONL → DB seed thread
compact_session() session_store.py LCM-style compaction: groups message_events into blocks and creates summary_nodes/summary_spans; opt-in via history.compaction.enabled (FEAT-1712). After per-session passes, cross-session recursive condensation (ENH-1954) groups condensed nodes level-by-level into a multi-level DAG terminating at a single project-root summary node (session_id=NULL, level=max); gated by history.compaction.cross_session_enabled.
SQLiteTransport.send() session_store.py Routes issue.* / loop.* events to DB
EventBus.emit() events.py Dispatches events to registered transports
post_tool_use hook hooks/post_tool_use.py Writes tool_events / file_events per call
user_prompt_submit hook hooks/user_prompt_submit.py Writes user_corrections / skill_events via is_correction() heuristic
cli_event_context() session_store.py Context manager that records ll- CLI entry-point invocations to cli_events (ENH-1849). Honors LL_HISTORY_DB env var for path override.
history_reader.py history_reader.py Public read API: 10 query functions, 7 dataclasses, ll_grep / ll_expand / ll_describe (FEAT-1712), project_digest / render_project_context (ENH-1907)
ll-history-context CLI cli/history_context.py Primary consumer: ## Historical Context block (issue mode) + project digest dry-run (--project)
ll-session CLI cli/session.py Secondary consumer: search, issue events, sessions, grep/expand/describe (FEAT-1712)
Skills commands/refine-issue.md etc. Call ll-history-context for agent context injection
session_start hook hooks/session_start.py Ambient consumer: injects <project_context> block at session start (opt-in, ENH-1907)

Decisions Log: .ll/decisions.yaml + .ll/decisions.d/

The decisions log is the per-project decisions and rules persistence layer, managed by ll-issues decisions subcommands and the decisions.py data layer. Storage is hybrid (BUG-2642): new entries are written as append-only per-entry fragments under .ll/decisions.d/<uuid4>.json (UUID4 ids, so concurrent EPIC-branch appends never collide), and a legacy .ll/decisions.yaml flat file may also exist. load_decisions() reads the union of both tiers; save_decisions() (compaction) folds every fragment into the flat file and deletes the fragment directory. A fresh install has only .ll/decisions.d/. It stores three entry types:

Entry Type Purpose
rule Enforced policies (advisory or required); required rules surface in /ll:ready-issue validation
decision Recorded architectural or process decisions; auto-generated from completed issues via ll-issues decisions generate
exception One-time exceptions to existing rules; suppress false-positive violations in /ll:ready-issue and /ll:verify-issues
coupling Wire-issue static layer: maps if_changed glob patterns to then_check audit targets; tier (hard/soft/fyi) controls how matches are injected into agent prompts; optional archetype groups rules into named bundles (e.g., add-cli-command)

Opt-in: An absent decisions log (neither .ll/decisions.yaml nor .ll/decisions.d/) is never an error — all integrations gracefully skip when it is missing. Presence gates must accept either tier ([ -f .ll/decisions.yaml ] || [ -d .ll/decisions.d ]); gating on the flat file alone silently skips governance on never-compacted installs. Enable the feature by adding a decisions: block to .ll/ll-config.json.

Integrity transports: the schema and load semantics of both tiers (flat file and .ll/decisions.d/*.json fragments) are gated by ll-verify-decisions (ENH-2589) at three transport-layer integrations, listed in firing order:

  1. Claude Code PreToolUse hook (ENH-2592, hooks/scripts/check-decisions-yaml.sh) — innermost belt; stages Write/Edit candidate content in a temp config root and runs the validator against it. Block (exit 2) fires before the host writes the file.
  2. Git pre-commit hook (ENH-2590, .pre-commit-config.yaml repo: local block) — runs the validator on staged changes; fails the git commit on any caught exception.
  3. Pytest CI belt (ENH-2591, scripts/tests/test_decisions_yaml_gate.py) — wide-net belt covering git commit --no-verify and non-hook edit paths; runs as part of python -m pytest scripts/tests/.

Key consumers: /ll:ready-issue (Decisions Gate), /ll:verify-issues (rule violation detection), /ll:format-issue (quality analysis), decisions_sync.py (active rules → .ll/ll.local.md sync), /ll:wire-issue Phase 3.5 (coupling entries → MUST_AUDIT injection into agent prompts).

Correction Detection Heuristic

is_correction() in session_store.py decides whether a user message should be recorded as a user_corrections row. It applies three independent pattern sets in order:

  1. Prefix patterns (_CORRECTION_RE) — Opening phrases like "no,", "wrong,", "actually,", "that's not", "you're wrong".
  2. Phrase-internal patterns (_PHRASE_RE) — Mid-sentence signals: "instead", "you missed", "should be" (guarded against false-positive affirmatives like "should be fine"), "wrong approach", "remember that", "always use", "never use", "from now on", "I meant … not". (ENH-1887)
  3. Explicit escape hatch (_REMEMBER_RE) — A message beginning with !remember is always classified as a correction regardless of phrasing. (ENH-1887)

Any match across the three sets records the message as a correction. A fourth mechanism is available via the optional extra_patterns argument to is_correction(): user-configured raw regex phrases from analytics.capture.correction_patterns are compiled and evaluated as an additional search() pass. The three module-level pattern sets remain the built-in base and are never replaced. Consumers (refine-issue, ready-issue, confidence-check, go-no-go) retrieve these rows via ll-history-context to surface prior corrections as context before generating a response.

Graceful-Degradation Contract

  • _connect_readonly() returns None on schema-version mismatch, file-not-found, or any open failure
  • All query functions (find_user_corrections, recent_file_events, search, related_issue_events, sessions_for_issue) return [] when the connection is None
  • All hook writers wrap DB calls in contextlib.suppress(Exception) so a write failure never aborts a tool call
  • SQLiteTransport.send() is a no-op when self._conn is None

See also: Extension Architecture & Event Flow for the full schema-version table (v1–v20) and CLI transport-wiring table.


Host Runner Layer

Sitting alongside the hook-intent layer is the host_runner abstraction (scripts/little_loops/host_runner.py). Where hook intents normalize incoming host events into the LLHookEvent envelope, the host runner normalizes outgoing CLI invocations: every shell-out to a host CLI (claude, codex, opencode, pi, gemini, omp) is built through a HostRunner implementation rather than hard-coded argv. This makes the orchestration layer host-agnostic and keeps host-specific argv shape out of call sites like ll-auto, ll-parallel, ll-action, ll-loop, FSM evaluators, and FSM handoff.

Component Purpose
HostRunner (Protocol) Contract every runner satisfies — detect(), build_streaming(), build_blocking_json(), build_version_check(), build_detached() factories returning HostInvocation; describe_capabilities() returning CapabilityReport
HostInvocation (frozen dataclass) Value object holding binary, args, env, capabilities, and cleanup_paths — passed to subprocess.Popen/run; callers must unlink cleanup_paths after the subprocess completes
HostCapabilities (frozen dataclass) Capability flags (streaming, permission_skip, agent_select, tool_allowlist) describing what a host supports
ClaudeCodeRunner Production runner for the claude CLI
CodexRunner Production runner for the codex CLI; auto-detected when codex is on PATH
GeminiRunner Production runner for the gemini CLI (Gemini CLI); auto-detected when gemini is on PATH (ENH-2185)
OmpRunner Production runner for the oh-my-pi omp CLI; auto-detected when omp is on PATH (FEAT-1850)
OpenCodeRunner Stub for the opencode CLI (FEAT-1472 stub state)
PiRunner Frozen stub for the vanilla pi-mono pi CLI (cancelled — ARCHITECTURE-050; superseded by OmpRunner)
resolve_host() Discovery entry point — honors LL_HOST_CLI / orchestration.host_cli overrides, then probes PATH for known host binaries
HostNotConfigured Raised when no runner can be resolved — error includes LL_HOST_CLI remediation hint
CapabilityNotSupported UserWarning subclass emitted when a caller requests a capability the active host lacks
CapabilityReport (frozen dataclass) Structured preflight report returned by describe_capabilities() — holds host, binary, version, capabilities, and hooks; consumed by ll-doctor and ll-action
CapabilityEntry (frozen dataclass) One capability's name and "full" / "partial" / "unsupported" status
HookEntry (frozen dataclass) One hook's name and "installed" / "registered" / "deferred" / "absent" status
apply_host_cli_from_config() Reads orchestration.host_cli from BRConfig and exports it as LL_HOST_CLI before resolve_host() runs

New host-CLI call sites MUST go through resolve_host() rather than adding new "claude" literals. See HOST_COMPATIBILITY.md for the per-host orchestration matrix and API Reference — little_loops.host_runner for the full public surface.


Class Relationships

classDiagram
    class BRConfig {
        +project: ProjectConfig
        +issues: IssuesConfig
        +automation: AutomationConfig
        +parallel: ParallelAutomationConfig
        +get_issue_dir(category) Path
        +create_parallel_config() ParallelConfig
        +to_dict() dict
    }

    class IssueParser {
        +config: BRConfig
        +parse_file(path) IssueInfo
    }

    class IssueInfo {
        +path: Path
        +issue_type: str
        +priority: str
        +issue_id: str
        +title: str
        +status: str
        +priority_int: int
    }

    class AutoManager {
        +config: BRConfig
        +state_manager: StateManager
        +event_bus: EventBus
        +db_path: Path | None
        +run() int
    }

    class StateManager {
        +state_file: Path
        +load() ProcessingState
        +save()
        +mark_completed(issue_id)
        +mark_failed(issue_id, reason)
    }

    class ParallelOrchestrator {
        +parallel_config: ParallelConfig
        +br_config: BRConfig
        +queue: IssuePriorityQueue
        +worker_pool: WorkerPool
        +merge_coordinator: MergeCoordinator
        +run() int
    }

    class WorkerPool {
        +parallel_config: ParallelConfig
        +start()
        +submit(issue) Future
        +shutdown()
        +cleanup_all_worktrees()
    }

    class MergeCoordinator {
        +config: ParallelConfig
        +start()
        +queue_merge(result)
        +shutdown()
    }

    BRConfig --> IssueParser
    IssueParser --> IssueInfo
    BRConfig --> AutoManager
    AutoManager --> StateManager
    BRConfig --> ParallelOrchestrator
    ParallelOrchestrator --> WorkerPool
    ParallelOrchestrator --> MergeCoordinator
    ParallelOrchestrator --> IssuePriorityQueue

Configuration Flow

flowchart LR
    subgraph Load["Load Phase"]
        JSON[".ll/ll-config.json"]
        INIT["BRConfig.__init__()"]
        PARSE["_parse_config()"]
    end

    subgraph Objects["Config Objects"]
        PC[ProjectConfig]
        IC[IssuesConfig]
        AC[AutomationConfig]
        PAC[ParallelAutomationConfig]
    end

    subgraph Usage["Usage"]
        CMD["Command Templates<br/>{{config.project.*}}"]
        AUTO_CLI["ll-auto"]
        PAR_CLI["ll-parallel"]
    end

    JSON --> INIT
    INIT --> PARSE
    PARSE --> PC
    PARSE --> IC
    PARSE --> AC
    PARSE --> PAC
    PC --> CMD
    IC --> CMD
    AC --> AUTO_CLI
    PAC --> PAR_CLI

Design tokens (DesignTokensConfig) serve as a cross-cutting input to artifact-generating loops: ll-loop run and ll-loop resume pre-inject the resolved token set into the FSM initial context before the first state is entered.

Project-enriched artifacts. This is one instance of a broader principle: little-loops generators stamp resolved project context into their output at generation time rather than fetching it at runtime. Design tokens are the shipped example (render_as_css_vars injected into the loop context above). The pattern extends to other generators — e.g. the policy-router HTML builder (ll-artifact policy-builder, FEAT-2390) stamps the canonical predicate grammar (policy_rules.grammar_spec()) and the project's invokable skill/command catalog (cli/action.py:_load_skills()) into a self-contained .html as JSON <script> islands. This is deliberate: the enrichment is what makes a generated artifact useful in this project, so outputs are point-in-time snapshots tied to the project, not portable-generic templates. "Self-contained" describes a runtime property (no fetch, works over file://), not cross-project portability — regenerate to pick up project changes.


Issue Processing Lifecycle

stateDiagram-v2
    [*] --> Discovered: /ll:scan-codebase

    Discovered --> Prioritized: /ll:prioritize-issues
    Prioritized --> Validating: /ll:ready-issue

    Validating --> Ready: READY verdict
    Validating --> NotReady: NOT_READY verdict
    Validating --> ShouldClose: CLOSE verdict

    Ready --> Deciding: decision_needed: true
    Deciding --> Ready: /ll:decide-issue
    Ready --> InProgress: /ll:manage-issue
    InProgress --> Verifying: Implementation done
    Verifying --> Completed: Tests pass
    Verifying --> Failed: Tests fail

    NotReady --> Discovered: Fix issue file
    ShouldClose --> Completed: Move to completed/
    Failed --> Discovered: Create follow-up issue
    Discovered --> Deferred: Defer issue
    Deferred --> Discovered: Undefer issue

    Completed --> [*]: Move to .issues/completed/
    Deferred --> [*]: Move to .issues/deferred/

Priority Queue Design

The priority queue separates P0 (critical) issues for sequential processing while allowing P1-P5 to be processed in parallel.

flowchart TB
    subgraph Input["Issue Scanning"]
        SCAN[Scan .issues/ directories]
    end

    subgraph Queue["IssuePriorityQueue"]
        P0Q[P0 Queue<br/>Sequential]
        PARQ[P1-P5 Queue<br/>Parallel]
    end

    subgraph Processing["Processing"]
        SEQ[Sequential<br/>One at a time]
        PAR[Parallel<br/>Up to max_workers]
    end

    SCAN --> P0Q
    SCAN --> PARQ

    P0Q --> SEQ
    PARQ --> PAR

    SEQ --> |Complete before| PAR

Rationale: P0 issues are critical and may have dependencies. Processing them sequentially ensures stability before parallel work begins.


Sprint Mode (ll-sprint)

Sprint execution uses dependency-aware wave-based scheduling. Issues are grouped into waves where each wave contains issues whose blockers have all completed.

flowchart TB
    subgraph Build["Build Phase"]
        LOAD[Load sprint issues]
        INFO[Load IssueInfo objects]
        GRAPH[Build DependencyGraph]
        WAVES[Calculate execution waves]
    end

    subgraph Waves["Wave Execution"]
        W1[Wave 1<br/>No blockers]
        W2[Wave 2<br/>Blocked by Wave 1]
        W3[Wave N<br/>Blocked by Wave N-1]
    end

    subgraph Parallel["ParallelOrchestrator"]
        ORCH[Execute wave in parallel]
        WORKERS[Workers process issues]
        MERGE[Merge results]
    end

    LOAD --> INFO
    INFO --> GRAPH
    GRAPH --> WAVES
    WAVES --> W1
    W1 --> ORCH
    ORCH --> WORKERS
    WORKERS --> MERGE
    MERGE --> W2
    W2 --> ORCH
    MERGE --> W3

Sprint Execution Flow

sequenceDiagram
    participant User
    participant CLI as ll-sprint
    participant Manager as SprintManager
    participant Graph as DependencyGraph
    participant Orch as ParallelOrchestrator

    User->>CLI: ll-sprint run sprint-1
    CLI->>Manager: Load sprint
    Manager-->>CLI: Sprint with issues

    CLI->>Manager: load_issue_infos(issues)
    Manager-->>CLI: List[IssueInfo]

    CLI->>Graph: from_issues(issue_infos)
    Graph-->>CLI: DependencyGraph

    CLI->>Graph: get_execution_waves()
    Graph-->>CLI: [[Wave1], [Wave2], ...]

    loop For each wave
        CLI->>CLI: Log wave issues
        CLI->>Orch: Execute wave issues
        Orch-->>CLI: Wave complete
    end

    CLI-->>User: Sprint complete

Wave Calculation Example

Given issues with dependencies: - FEAT-001: No blockers - BUG-001: No blockers - FEAT-002: Blocked by FEAT-001 - FEAT-003: Blocked by FEAT-001 - FEAT-004: Blocked by FEAT-002, FEAT-003

The DependencyGraph.get_execution_waves() returns:

Wave Issues Reason
1 FEAT-001, BUG-001 No blockers
2 FEAT-002, FEAT-003 FEAT-001 completed in Wave 1
3 FEAT-004 FEAT-002, FEAT-003 completed in Wave 2

Issues within each wave execute in parallel. Waves execute sequentially.

Dependency Discovery

The dependency_mapper module complements dependency_graph by discovering new dependency relationships:

  • dependency_graph.py: Execution ordering from existing Blocked By data
  • dependency_mapper/: Discovery of new relationships via file overlap + semantic conflict analysis (split into models, analysis, formatting, operations sub-modules)

The /ll:map-dependencies skill uses dependency_mapper to analyze active issues, propose dependencies based on shared file references, validate existing dependency integrity (broken refs, missing backlinks, cycles), and write approved relationships to issue files.

Semantic Conflict Analysis

When two issues reference the same file, the mapper goes beyond simple file overlap to determine whether they actually conflict. It computes a conflict score (0.0–1.0) from three signals:

  1. Semantic target overlap (weight 0.5) — Extracts PascalCase component names, function references, and explicit scope mentions from issue content, then computes Jaccard similarity
  2. Section mention overlap (weight 0.3) — Detects UI region keywords (header, body, sidebar, footer, card, modal, form) and checks if both issues target the same region
  3. Modification type match (weight 0.2) — Classifies each issue as structural, infrastructure, or enhancement based on keyword matching

Score thresholds: - < 0.4: Parallel-safe — issues touch different sections of the same file and can run concurrently - >= 0.4: Dependency proposed — issues likely conflict and should be sequenced

Same-priority ordering: When two conflicting issues share the same priority, the mapper uses modification type to determine direction (structural → infrastructure → enhancement) rather than arbitrary ID ordering.


Key Design Decisions

Git Worktree Isolation

Each parallel worker operates in a separate git worktree:

.worktrees/
├── worker-1/                    # ll-parallel worker (full repo copy)
│   ├── src/
│   ├── tests/
│   └── .claude/
├── worker-2/
├── worker-N/
└── <timestamp>-<loop-name>/     # ll-loop --worktree isolated run

Benefits: - No file conflicts between workers - Each worker has isolated branch - Clean rollback on failure

Trade-offs: - Disk space usage (full copy per worker) - Initial setup time for worktrees

Sequential Merging

Despite parallel issue processing, merges happen one at a time:

flowchart LR
    W1[Worker 1<br/>Complete] --> MQ[Merge Queue]
    W2[Worker 2<br/>Complete] --> MQ
    W3[Worker 3<br/>Complete] --> MQ

    MQ --> M1[Merge 1]
    M1 --> M2[Merge 2]
    M2 --> M3[Merge 3]

Rationale: Parallel merges would cause conflicts. Sequential merging with rebase-on-conflict ensures clean integration.

State Persistence

Both modes save state for resume capability:

Mode State File Contents
Sequential .auto-manage-state.json Current issue, completed list, failed list, timing
Parallel .parallel-manage-state.json In-progress, completed, failed, pending merges

Format:

{
  "completed_issues": ["BUG-001", "BUG-002"],
  "failed_issues": {"BUG-003": "Test failure"},
  "attempted_issues": ["BUG-001", "BUG-002", "BUG-003"],
  "timing": {
    "BUG-001": {"ready": 30.5, "implement": 120.2, "verify": 5.1}
  }
}

Merge Strategy

The merge coordinator is a sophisticated git operations state machine that handles: 1. Sequential merge queue (one at a time to avoid conflicts) 2. Automatic stash/unstash of local changes with smart exclusions 3. Adaptive pull strategy (tracks problematic commits, switches to merge on repeat) 4. Index recovery (detects and repairs corrupted git state) 5. Lifecycle file coordination (auto-commits pending moves) 6. Conflict retry with rebase (up to max_merge_retries times) 7. Circuit breaker (pauses after consecutive failures) 8. Untracked file backup and retry

See MERGE-COORDINATOR.md for comprehensive documentation.

Context Monitor and Session Continuation

When context window limits approach, the system can automatically preserve work and spawn fresh sessions.

flowchart TB
    subgraph Hook["PostToolUse Hook"]
        ESTIMATE[Estimate context usage]
        CHECK[Check threshold]
    end

    subgraph Handoff["Active Handoff Path"]
        TRIGGER[Trigger /ll:handoff]
        WRITE[Write continuation prompt]
        SIGNAL[Output CONTEXT_HANDOFF signal]
    end

    subgraph CLI["CLI Detection"]
        DETECT[Detect handoff signal]
        READ[Read continuation prompt]
        SPAWN[Spawn fresh session]
    end

    subgraph PassivePath["Passive Handoff Path (PreCompact)"]
        PC_WRITE[precompact-handoff.sh writes continuation prompt]
        COMPACT[Claude Code compacts context]
        RESUME[/ll:resume re-injects context in current session]
    end

    ESTIMATE --> CHECK
    CHECK -->|>= 80%| TRIGGER
    TRIGGER --> WRITE
    WRITE --> SIGNAL
    SIGNAL --> DETECT
    DETECT --> READ
    READ --> SPAWN
    SPAWN -->|Resume work| ESTIMATE
    PC_WRITE --> COMPACT
    COMPACT --> RESUME
    RESUME -->|Work continues| ESTIMATE

Context Estimation: The hook uses a three-tier priority for token counts:

Priority Source When Active
1 (highest) result_token_count in state file Non-zero; written by on_usage callback from stream-json result events — zero lag, authoritative
2 transcript_baseline_tokens use_transcript_baseline: true and transcript available — one-turn lag, API-exact
3 (fallback) Heuristic estimates When both above are absent

When result_token_count > 0 in .ll/ll-context-state.json, the context monitor uses it directly and skips heuristics entirely.

Heuristic estimates (fallback only):

Tool Estimation
Read lines × 10 tokens
Grep output_lines × 5 tokens
Bash chars × 0.3 tokens
Task 2000 tokens (summarized)
WebFetch 1500 tokens
Other 100 tokens base

Continuation Flow:

  1. Hook triggers at 80% estimated context usage (configurable)
  2. Handoff command generates .ll/ll-continue-prompt.md with session state
  3. CLI tools (ll-auto, ll-parallel) detect CONTEXT_HANDOFF signal in output
  4. Fresh session spawned with continuation prompt
  5. Work continues seamlessly from saved state

Configuration (enabled by default):

{
  "context_monitor": {
    "enabled": true,
    "auto_handoff_threshold": 80
  }
}

Files: - hooks/prompts/continuation-prompt-template.md - Template for handoff prompts - .ll/ll-context-state.json - Running context usage state - .ll/ll-continue-prompt.md - Generated continuation prompt - subprocess_utils.py - Handoff detection and continuation reading

Session Log Auto-Linking

When an issue file is written with status: done in its frontmatter, a PostToolUse hook automatically appends a Session Log entry. This ensures session logs are linked regardless of which path completed the issue.

Trigger: Any Write tool call whose file path is in .issues/ and whose frontmatter contains status: done.

Covered completion paths: - manage-issue skill (Phase 5) - ll-auto (sequential batch) - ll-parallel (concurrent worktree) - ll-sprint (dependency-ordered) - Manual git mv during a Claude session

Implementation: - Hook script: hooks/scripts/issue-completion-log.sh - Uses little_loops.session_log.append_session_log_entry() with source hook:posttooluse-git-mv - Session JSONL path is read directly from the transcript_path field in the PostToolUse stdin payload


Issue Auto-Commit

When issues.auto_commit: true is set in .ll/ll-config.json, a PostToolUse hook automatically commits issue file changes after every Write or Edit operation on a file in .issues/. The hook skips gracefully if any other changes are staged or unstaged in the working tree.

Trigger: Any Write or Edit tool call whose file path is in .issues/.

Implementation: - Hook script: hooks/scripts/issue-auto-commit.sh - Config flags: issues.auto_commit (bool, default false), issues.auto_commit_prefix (string, default "chore(issues)") - Commit message format: <prefix>: <verb> <filename> where verb is add (new file) or update (existing file) - Python handler: _maybe_auto_commit() in scripts/little_loops/hooks/post_tool_use.py


Session Event Capture

When session_capture.enabled: true is set in .ll/ll-config.json, a PostToolUse hook fires on every tool invocation and appends one structured JSON event record to .ll/ll-session-events.jsonl. This event log is the data source for FEAT-1264's PreCompact snapshot builder, which uses it to reconstruct a structured handoff context (pending tasks, net-modified files, unresolved errors) that is more accurate than the current git-diff-based approach.

Trigger: Any tool invocation (matcher: *).

Event types captured: - file — Read, Write, Edit, Glob, Grep tool calls (subject = file path) - task — TodoWrite, TaskCreate, TaskUpdate tool calls (subject = content/id, status = task status) - git — Bash invocations containing git with exit 0 (op = git subcommand, subject = args) - error — Bash invocations with non-zero exit code (op = "bash_error", subject = command, status = exit code)

Implementation: - Hook script: hooks/scripts/session-capture.sh - Output file: .ll/ll-session-events.jsonl (one compact JSON object per line) - Config flag: session_capture.enabled (bool, default false) - Failure-safe: all error paths exit 0; capture failures never block tool execution

Consumer: FEAT-1264 (precompact-handoff.sh) reads this log to build the structured handoff snapshot.


Context Efficiency

Efficiency metric: tokens-per-task, not tokens-per-request.

For ll-auto, ll-parallel, and ll-sprint, the correct optimization target is minimizing total tokens consumed per completed issue, not per individual turn. Over-aggressive compression that causes retries, re-reads, or error recovery is less efficient than a longer conversation that completes the task cleanly.

This principle is validated by published research on long-context LLM architectures (see docs/research/LCM-Lossless-Context-Management.md, Section 4.3): systems that aggressively chunk context introduce variance and error cascades, while systems that preserve working context through task completion achieve better reliability per token.

Implications for compression decisions: - Compress at 80% context utilization (see auto_handoff_threshold in ### Context Monitor and Session Continuation, above), not earlier - Prefer keeping relevant tool outputs in context over re-fetching when needed again - A failed task that restarts from scratch costs more tokens than a task that completes in a longer conversation

Relationship to ENH-499: The inter-issue context checkpoint (implemented in ENH-499) applies this principle at issue boundaries — it triggers a structured summarization reset rather than re-running tool calls to reconstruct state.

  • Skill pre-expansion (skill_expander.expand_skill) eliminates the ToolSearch → Skill deferred-tool round-trip when ll-auto spawns Claude subprocesses: the full skill/command Markdown is read, config placeholders substituted, and the resulting self-contained prompt string is passed directly. This removes one tool call from every Phase 1 and Phase 2 invocation.

Learning Test Registry

The Learning Test Registry is a persistent store of proven facts about external systems (APIs, SDKs, libraries) that the codebase or its agents depend on. It exists so that expensive exploration work — "how does the Anthropic streaming API actually shape its events?" — is captured once and reused indefinitely.

Lifecycle

The registry is populated by the /ll:explore-api skill, which walks the four-phase Feathers Learning Test loop:

flowchart LR
    INGEST[Phase 1: Ingest<br/>check existing record<br/>read docs/source]
    HYPOTHESIZE[Phase 2: Hypothesize<br/>3–7 falsifiable claims]
    EXECUTE[Phase 3: Execute<br/>run proof script<br/>capture stdout/stderr]
    REFINE[Phase 4: Refine<br/>classify pass/fail/untested<br/>write LearnTestRecord]
    INGEST --> HYPOTHESIZE
    HYPOTHESIZE --> EXECUTE
    EXECUTE --> REFINE
    REFINE -.-> INGEST

Phase 1 short-circuits if ll-learning-tests check "<target>" already returns a record — future agents skip rediscovery for free, which is the whole point.

Schema

Records are YAML-frontmatter Markdown files stored under .ll/learning-tests/<slug>.md. The LearnTestRecord dataclass (scripts/little_loops/learning_tests.py) has five fields:

Field Type Notes
target str Free-text human-readable name
date str ISO date the record was written
status Literal["proven", "refuted", "stale"] proven if any assertion passed; stale is set via mark-stale
assertions list[Assertion] Each {claim: str, result: "pass"|"fail"|"untested"}
raw_output_path str \| None Pointer to .ll/learning-tests/raw/<slug>.txt

Slug derivation uses little_loops.issue_parser.slugify() (lowercase, strip non-word chars, collapse whitespace and hyphens), so "Anthropic SDK streaming" becomes anthropic-sdk-streaming.md.

Storage Layout

.ll/learning-tests/
├── <slug>.md              # one LearnTestRecord per target
├── ...
└── raw/                   # raw stdout/stderr captures from proof scripts
    ├── <slug>.txt
    └── ...

The raw/ subdirectory is created on demand by /ll:explore-apiwrite_record() does not auto-create it. Files in raw/ are the unedited output of the proof script; they are evidence, not summaries.

Spike Plan Docs — Storage Layout

.ll/spikes/
├── spike-<ISSUE-ID>.md    # one plan doc per interactive /ll:spike run
└── ...

The /ll:spike skill (skills/spike/SKILL.md Phase 3) writes its plan doc to a resolved artifact directory: ${context.run_dir} when running inside an FSM loop (injected by scripts/little_loops/cli/loop/run.py, propagated to child contexts by scripts/little_loops/fsm/executor.py), and the standardized .ll/spikes/ directory when invoked interactively (no run_dir exists). The skill body mkdir -p .ll/spikes/ on demand — nothing pre-creates it. Plan docs are curated evidence paired with the issue's committed ## Spike Results section, so .ll/spikes/ is git-tracked via the !/.ll/ un-ignore (no .gitignore entry), mirroring .ll/learning-tests/ and .ll/decisions.d/ (ENH-2655).

CLI Surface

ll-learning-tests (scripts/little_loops/cli/learning_tests.py) is intentionally narrow: it owns reads and stale-marking, but not writes.

Subcommand Purpose Exit codes
check "<target>" Print JSON record by target name 0 if found, 1 if missing
list Print JSON array of all records always 0
mark-stale "<target>" Set status: stale on an existing record 0

There is no write/add subcommand. Record creation is owned by /ll:explore-api (and any future skill variants) so the prompt context — claims, reasoning, proof script — is captured alongside the result, not just the result alone. Skills emit the on-disk YAML directly via the Write tool to match the format that write_record() produces.

For automated bulk staleness detection across all records, use ll-loop run learning-tests-audit — a built-in FSM loop that compares record dates against PyPI/npm registry release timelines and batch-marks stale records. Once records are marked stale, run ll-loop run migrate-sdk-version to re-prove them: it iterates the stale queue, re-runs /ll:explore-api for each target, classifies each result as still-valid, needs-upgrade, or refuted, and produces a triage report. Together these two loops form the two-step registry maintenance workflow. See docs/guides/LOOPS_REFERENCE.md → API Adoption.

LearningTestsConfig Consumers

The LearningTestsConfig dataclass (scripts/little_loops/config/features.py) is consumed by three call sites within EPIC-2207's scope:

Call Site Issue Config Field Read
learning_tests_gate.py ENH-2208 stale_after_days
/ll:refine-issue / /ll:wire-issue skills ENH-2209 learning_tests.enabled
Sprint pre-flight (fsm/executor.py) ENH-2210 learning_tests.enabled

A future refactor of this config schema must update all three. See config-schema.json for the full LearningTestsConfig schema definition.


Data Flow Summary

flowchart TB
    subgraph User["User Input"]
        CMD_INPUT["ll-auto / ll-parallel"]
        FLAGS["--max-issues, --workers, etc."]
    end

    subgraph Config["Configuration"]
        LOAD["Load .ll/ll-config.json"]
        MERGE_CFG["Merge with defaults"]
    end

    subgraph Discovery["Issue Discovery"]
        SCAN["Scan .issues/*/"]
        PARSE["Parse markdown files"]
        SORT["Sort by priority"]
    end

    subgraph Processing["Processing"]
        VALIDATE["Validate (ready-issue)"]
        IMPLEMENT["Implement (manage-issue)"]
        VERIFY["Verify (tests pass)"]
    end

    subgraph Completion["Completion"]
        MOVE["Move to completed/"]
        COMMIT["Git commit"]
        REPORT["Summary report"]
    end

    CMD_INPUT --> LOAD
    FLAGS --> LOAD
    LOAD --> MERGE_CFG
    MERGE_CFG --> SCAN
    SCAN --> PARSE
    PARSE --> SORT
    SORT --> VALIDATE
    VALIDATE --> IMPLEMENT
    IMPLEMENT --> VERIFY
    VERIFY --> MOVE
    MOVE --> COMMIT
    COMMIT --> REPORT