Configuration Reference¶
little-loops uses .ll/ll-config.json for project-specific settings. All settings have sensible defaults. Run ll-init to auto-detect your project type and generate a config file.
For interactive editing, use /ll:configure.
Full Configuration Example¶
{
"$schema": "https://raw.githubusercontent.com/BrennonTWilliams/little-loops/main/scripts/little_loops/config-schema.json",
"project": {
"name": "my-project",
"src_dir": "src/",
"test_dir": "tests",
"test_cmd": "pytest tests/",
"lint_cmd": "ruff check src/",
"type_cmd": "mypy src/",
"format_cmd": "ruff format src/"
},
"issues": {
"base_dir": ".issues",
"categories": {
"bugs": { "prefix": "BUG", "dir": "bugs", "action": "fix" },
"features": { "prefix": "FEAT", "dir": "features", "action": "implement" },
"enhancements": { "prefix": "ENH", "dir": "enhancements", "action": "improve" }
},
"completed_dir": "completed",
"deferred_dir": "deferred",
"priorities": ["P0", "P1", "P2", "P3", "P4", "P5"],
"templates_dir": null,
"capture_template": "full",
"duplicate_detection": {
"exact_threshold": 0.8,
"similar_threshold": 0.5
},
"next_issue": { "strategy": "confidence_first" },
"auto_commit": false,
"auto_commit_prefix": "chore(issues)"
},
"automation": {
"timeout_seconds": 7200,
"idle_timeout_seconds": 0,
"post_stream_close_grace_seconds": 300,
"timeout_kill_grace_seconds": 30,
"state_file": ".auto-manage-state.json",
"worktree_base": ".worktrees",
"max_workers": 2,
"stream_output": true
},
"parallel": {
"max_workers": 2,
"p0_sequential": true,
"worktree_base": ".worktrees",
"state_file": ".parallel-manage-state.json",
"timeout_per_issue": 3600,
"max_merge_retries": 2,
"stream_subprocess_output": false,
"command_prefix": "/ll:",
"ready_command": "ready-issue {{issue_id}}",
"manage_command": "manage-issue {{issue_type}} {{action}} {{issue_id}}",
"decide_command": "decide-issue {{issue_id}}",
"worktree_copy_files": [".claude/settings.local.json", ".env", ".ll/ll.local.md"],
"require_code_changes": true,
"use_feature_branches": false,
"epic_branches": { "enabled": false },
"remote_name": "origin"
},
"commands": {
"pre_implement": null,
"post_implement": null,
"custom_verification": [],
"confidence_gate": {
"enabled": false,
"readiness_threshold": 85,
"outcome_threshold": 65
},
"tdd_mode": false,
"max_refine_count": 5,
"recursive_refine": {
"max_depth": 3
},
"rate_limits": {
"max_wait_seconds": 21600,
"long_wait_ladder": [300, 900, 1800, 3600],
"circuit_breaker_enabled": true,
"circuit_breaker_path": ".loops/tmp/rate-limit-circuit.json"
}
},
"scan": {
"focus_dirs": ["src/", "tests/"],
"exclude_patterns": ["**/node_modules/**", "**/__pycache__/**", "**/.git/**"],
"custom_agents": []
},
"product": {
"enabled": false,
"goals_file": ".ll/ll-goals.md",
"analyze_user_impact": true,
"analyze_business_value": true,
"goals_discovery": {
"max_files": 5,
"required_files": ["README.md"]
}
},
"prompt_optimization": {
"enabled": false,
"mode": "quick",
"confirm": true,
"bypass_prefix": "*",
"clarity_threshold": 6
},
"compression": {
"heuristic_underperforms": false,
"trigger_pct": 0.4,
"trigger_tokens": null,
"max_tool_result_age_turns": 5,
"max_assistant_tail_turns": 8
},
"continuation": {
"enabled": true,
"include_todos": true,
"include_git_status": true,
"include_recent_files": true,
"max_continuations": 3,
"prompt_expiry_hours": 24
},
"context_monitor": {
"enabled": true,
"auto_handoff_threshold": 80,
"use_transcript_baseline": true
},
"sprints": {
"sprints_dir": ".sprints",
"default_timeout": 3600,
"default_max_workers": 2
},
"sync": {
"enabled": false,
"provider": "github",
"github": {
"repo": null,
"label_mapping": {
"BUG": "bug",
"FEAT": "enhancement",
"ENH": "enhancement",
"EPIC": "epic"
},
"priority_labels": true,
"sync_completed": false,
"state_file": ".ll/ll-sync-state.json"
}
},
"documents": {
"enabled": false,
"categories": {}
},
"design_tokens": {
"enabled": true,
"path": ".ll/design-tokens",
"primitives_file": "primitives.json",
"semantic_file": "semantic.json",
"themes_dir": "themes",
"active_theme": "dark"
},
"artifacts": {
"default_output_dir": ".",
"templates_dir": "artifacts/templates",
"promotion_dir": ".loops/artifacts"
},
"loops": {
"loops_dir": ".loops"
},
"scratch_pad": {
"enabled": false,
"threshold_lines": 200,
"automation_contexts_only": true,
"tail_lines": 20,
"command_allowlist": ["cat", "pytest", "mypy", "ruff", "ls", "grep", "find"],
"file_extension_filters": [".log", ".txt", ".json", ".md", ".py", ".ts", ".tsx", ".js"]
},
"dependency_mapping": {
"overlap_min_files": 2,
"overlap_min_ratio": 0.25,
"min_directory_depth": 2,
"conflict_threshold": 0.4,
"high_conflict_threshold": 0.7,
"confidence_modifier": 0.5,
"scoring_weights": {
"semantic": 0.5,
"section": 0.3,
"type": 0.2
},
"exclude_common_files": [
"__init__.py", "pyproject.toml", "setup.py",
"setup.cfg", "CHANGELOG.md", "README.md", "conftest.py"
]
},
"code_query": {
"provider": "auto",
"codegraph": {
"db_path": ".codegraph/codegraph.db"
},
"staleness": "warn"
},
"refine_status": {
"columns": [],
"elide_order": []
},
"cli": {
"color": true,
"colors": {
"logger": {
"info": "36",
"success": "32",
"warning": "33",
"error": "38;5;208"
},
"priority": {
"P0": "38;5;208;1",
"P1": "38;5;208",
"P2": "33",
"P3": "0",
"P4": "90",
"P5": "90"
},
"type": {
"BUG": "38;5;208",
"FEAT": "32",
"ENH": "34",
"EPIC": "35"
},
"fsm_active_state": "32",
"fsm_edge_labels": {}
}
},
"decisions": {
"enabled": false,
"log_path": ".ll/decisions.yaml",
"auto_generate": []
},
"extensions": [
"my_package.ext:MyExtension"
]
}
Top-Level Fields¶
install_source¶
A string recorded by ll-init that identifies how little-loops was installed. This field is written automatically and is not intended to be edited by hand.
| Value | Meaning |
|---|---|
"local-editable" |
Installed via pip install -e (development / editable install) |
"pypi" |
Installed from PyPI via pip install little-loops |
"global-claude-code" |
Installed as a global Claude Code plugin |
"project-claude-code" |
Installed as a project-level Claude Code plugin |
"global-codex" |
Installed as a global Codex plugin |
"global-pi" |
Installed as a global Pi plugin |
null |
Source could not be determined |
ll-init re-writes this field on every run. If you change your install method (e.g., switch from a local editable install to a PyPI release), run ll-init again to refresh it.
Configuration Sections¶
project¶
Project-level settings for commands:
| Key | Default | Description |
|---|---|---|
name |
Directory name | Project name |
src_dir |
src/ |
Source code directory |
test_dir |
tests |
Test directory path |
test_cmd |
pytest |
Command to run tests. Key absent → the default; explicit null → opt out of the test gate entirely rather than guessing (BUG-3269) |
lint_cmd |
ruff check . |
Command to run linter. Same absent-vs-null distinction as test_cmd (BUG-3269) |
type_cmd |
mypy |
Command for type checking |
format_cmd |
ruff format . |
Command to format code |
build_cmd |
null |
Optional build command |
run_cmd |
null |
Optional run/start command (smoke test) |
health_url |
null |
Optional service health probe URL (FEAT-2551, used by oracles/code-run-gate service_health state) |
test_patterns |
See per-project-type template | Gitignore-style glob patterns identifying test files, matched via little_loops.test_file_patterns (ENH-2973, consumed by ENH-2853/ENH-2854) |
On a fresh (non---force) ll-init, src_dir/test_cmd/lint_cmd/
format_cmd/type_cmd/scan.focus_dirs are no longer pure template
literals — init/introspect.py first inspects the repo's own manifests
(pyproject.toml tool tables, package.json scripts, package-layout
markers) and adopts a value only when unambiguously declared, tagging it
declared/inferred in the printed summary and the --plan JSON's
provenance key; anything not found stays the table default above, tagged
default (FEAT-2703). Re-init with an existing .ll/ll-config.json is
unaffected — existing values always win. test_patterns is deliberately
not introspected: it's a glob list with no manifest source to infer
from, so the per-project-type template default is the whole story.
issues¶
Issue management settings:
| Key | Default | Description |
|---|---|---|
base_dir |
.issues |
Base directory for issues |
categories |
See above | Issue category definitions |
completed_dir |
completed |
Deprecated — use IssueInfo.status instead; kept for backward compatibility |
deferred_dir |
deferred |
Deprecated — use IssueInfo.status instead; kept for backward compatibility |
priorities |
[P0-P5] |
Valid priority prefixes |
templates_dir |
null |
Directory for issue templates |
deploy_templates |
false |
When true, ll-init copies the bundled per-type section templates into <project_root>/.ll/templates/ so projects can customise them. Deployed copies take precedence over the bundled wheel templates via resolve_templates_dir(). |
capture_template |
"full" |
Default template style for captured issues ("full", "minimal", or "legacy") |
duplicate_detection.exact_threshold |
0.8 |
Jaccard similarity threshold for exact duplicates (0.0-1.0) |
duplicate_detection.similar_threshold |
0.5 |
Jaccard similarity threshold for similar issues (0.0-1.0) |
link_epics.min_score |
0.0 |
Minimum score for an ll-issues link-epics proposal or cluster edge (0.0-1.0). Separate from duplicate_detection.similar_threshold — different semantics (proposal-inclusion floor, not a dedup gate). |
next_issue.strategy |
"confidence_first" |
Selection order for ll-issues next-issue / next-issues. Named preset: confidence_first or priority_first. See issues.next_issue. |
next_issue.sort_keys |
null |
Optional list of {key, direction} entries that overrides strategy with a custom sort order. |
auto_commit |
false |
When true, the issue-auto-commit.sh PostToolUse hook automatically commits issue file changes (Write/Edit) with no other staged files present. |
auto_commit_prefix |
"chore(issues)" |
Commit message prefix used by the auto-commit hook. Final message format is <prefix>: <verb> <ISSUE_ID> <slug> where verb is capture (Write) or update (Edit/Update) and <ISSUE_ID> + <slug> are parsed from the issue filename (P[0-5]-TYPE-NNN-slug.md). |
untracked_by_design |
See config-schema.json |
Path prefixes classified untracked_by_design instead of stale when a file reference in issue prose cannot resolve against the tracked-file index (ENH-3000) — e.g. "thoughts/", "postmortems/", ".loops/runs/", ".ll/ll.local.md". Directory entries end in /; file entries are exact repo-relative paths matched by prefix. Ships a non-empty default covering this repo's own gitignored-by-design directories and every .ll/ path ll-init writes into a consumer's .gitignore; overriding replaces the whole array (no merge). |
Custom Categories: The four core categories (bugs, features, enhancements, epics) are always included automatically. You can add custom categories and they will be merged with the required ones:
{
"issues": {
"categories": {
"documentation": {"prefix": "DOC", "dir": "documentation", "action": "document"},
"tech-debt": {"prefix": "TECH-DEBT", "dir": "tech-debt", "action": "address"}
}
}
}
Each category requires a prefix (issue ID prefix), and optionally dir (subdirectory name, defaults to category key) and action (verb for commit messages, defaults to "address").
automation¶
Sequential automation settings (ll-auto):
| Key | Default | Description |
|---|---|---|
timeout_seconds |
7200 |
Per-issue timeout. A breach fails only that issue (mark_failed + record_orchestration_run(status="failed")) — ll-auto/ll-sprint continue to the next eligible issue rather than aborting the whole run (BUG-2976) |
idle_timeout_seconds |
0 |
Seconds of idle inactivity before automation considers the session idle (0 to disable) |
post_stream_close_grace_seconds |
300 |
Grace period after stdout/stderr streams close before force-killing the process group; must accommodate synchronous parallel Agent tool calls still in flight (BUG-2718) |
timeout_kill_grace_seconds |
30 |
Grace period given to the process group after a wall-clock or idle timeout fires: SIGTERM is sent first, and SIGKILL only follows if the group is still alive after this many seconds — lets an in-flight commit or lifecycle write finish instead of being cut off (ENH-3130) |
state_file |
.auto-manage-state.json |
State persistence |
worktree_base |
.worktrees |
Git worktree directory |
max_workers |
2 |
Parallel workers |
stream_output |
true |
Stream subprocess output |
max_continuations |
3 |
Maximum continuation prompts before automation stops (minimum 1) |
ready_issue_unknown_retries |
1 |
Retries when ready-issue returns no parseable verdict (UNKNOWN). Distinct from a real NOT_READY — UNKNOWN means the model did not answer at all, a probabilistic misread that would otherwise discard the whole run. 0 disables (minimum 0) |
parallel¶
Parallel automation settings with git worktree isolation (ll-parallel):
| Key | Default | Description |
|---|---|---|
max_workers |
2 |
Number of parallel workers |
p0_sequential |
true |
Process P0 issues sequentially |
worktree_base |
.worktrees |
Git worktree directory |
state_file |
.parallel-manage-state.json |
State persistence |
timeout_per_issue |
3600 |
Per-issue timeout in seconds |
max_merge_retries |
2 |
Rebase attempts before failing |
stream_subprocess_output |
false |
Stream Claude CLI output |
command_prefix |
/ll: |
Prefix for slash commands |
ready_command |
ready-issue {{issue_id}} |
Ready command template |
manage_command |
manage-issue {{issue_type}} {{action}} {{issue_id}} |
Manage command template |
decide_command |
decide-issue {{issue_id}} |
Command template for the decide-issue step when decision_needed: true. {{issue_id}} is substituted at runtime. |
worktree_copy_files |
[".claude/settings.local.json", ".env", ".ll/ll.local.md"] |
Files or directories to copy to worktrees. See WORKTREES.md for the full copy contract. |
require_code_changes |
true |
Require worktree to produce code changes before merging. Skips no-op runs. |
use_feature_branches |
false |
Create a feature/<id>-<slug> branch per issue instead of parallel/<id>-<timestamp>. When true, auto-merge is skipped and branches survive as PR-ready. Use for PR-based CI/CD workflows. |
push_feature_branches |
false |
Push the feature branch to remote_name after worker success using git push --force-with-lease. Requires use_feature_branches: true. |
open_pr_for_feature_branches |
false |
Open a draft PR via gh pr create after push and record pr_url: on the issue. Requires push_feature_branches: true and gh auth status. |
base_branch |
auto-detected | Base branch targeted by PR creation when open_pr_for_feature_branches is true. Also used as the rebase target for worktree updates. When unset, auto-detected at startup (origin/HEAD → current branch → main); an explicit value overrides auto-detection. An individual EPIC may override this for its own integration branch via a per-EPIC base_branch: (alias target_branch:) frontmatter field (FEAT-2652); ll-sprint dispatch hard-stops if a declared per-EPIC base does not exist (local or remote). |
remote_name |
"origin" |
Git remote name for fetch/pull operations. Set if your remote is not named origin (e.g., "upstream"). |
epic_branches.enabled |
false |
FEAT-2447, child 1/4 of FEAT-2339. When true, all children of a single EPIC share one integration branch (epic/<EPIC-ID>-<slug>) for both fork point AND merge target (per Decision ARCHITECTURE-096). Standalone (parentless) issues keep today's per-worker behavior unchanged. Worker-pool wiring landed in FEAT-2448; the _maybe_complete_epic / _inspect_worktree orchestrator paths landed in FEAT-2449 / FEAT-2562. Remaining CLI/TUI/docs polish is tracked in FEAT-2450. See .ll/decisions.yaml#ARCHITECTURE-096. |
epic_branches.prefix |
"epic/" |
Prefix for the per-EPIC integration branch name; the branch composes as f"{prefix}{epic_id.lower()}-{slug}" (e.g. epic/epic-2339-foo). {slug} is the kebab-cased EPIC title. |
epic_branches.merge_to_base_on_complete |
true |
When true, the EPIC integration branch is itself merged back to base_branch after the EPIC's last child completes. Set false to leave the integration branch un-merged (e.g. for manual PR review). |
epic_branches.open_pr |
false |
When true, open a PR for the EPIC integration branch via the gh CLI on completion. Requires gh installed and authenticated. |
epic_branches.verify_before_merge |
false |
When true, before merging an EPIC integration branch to base_branch (or opening its PR), check it out in a scratch worktree and run test_cmd/lint_cmd against it. On failure the merge/PR-open is blocked, the branch is left as-is (retried on the next completion event), and the failure is surfaced in the run summary rather than silently logged (ENH-2603). On the auto-refine-and-implement FSM loop path this check is skipped as redundant when the loop's verify state already produced a passed verdict for the current epic tip — the merge_epic_branch state reuses that verdict instead of re-running the suite (ENH-2630). |
epic_branches.refresh_on_reuse |
"merge" |
Staleness guard on a reused (already-existing, local-hit) EPIC integration branch relative to its resolved fork base (ENH-3302). "merge" (default): merge the base into the branch via a scratch worktree before dispatch/worktree-attach; a merge conflict aborts and degrades to warn without failing the run. "warn": measure and log only, no git state change. "off": disable the check entirely. Applies only on a local-hit reuse (the branch already exists locally) — a remote-only branch is left unchanged. |
product¶
Product analysis configuration for /ll:scan-product:
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable product-focused issue analysis |
goals_file |
.ll/ll-goals.md |
Path to product goals/vision document |
analyze_user_impact |
true |
Include user impact assessment in issues |
analyze_business_value |
true |
Include business value scoring in issues |
goals_discovery.max_files |
5 |
Maximum markdown files to analyze for goal discovery (1-20) |
goals_discovery.required_files |
["README.md"] |
Files that must exist for discovery (warning if missing) |
product.enabled defaults to false; opt in with ll-init --yes --enable product (or set product.enabled: true in .ll/ll-config.json for an existing project). When enabled, ll-init deploys .ll/ll-goals.md automatically. ll-goals.md is optional — if absent, goals are auto-discovered from existing project documentation (README, CHANGELOG, architecture docs). Create a hand-authored goals file only when you want precise control over product vision, personas, and strategic priorities.
commands¶
Command customization for /ll:manage-issue:
| Key | Default | Description |
|---|---|---|
pre_implement |
null |
Command to run before implementation |
post_implement |
null |
Command to run after implementation |
custom_verification |
[] |
Additional verification commands |
confidence_gate.enabled |
false |
Enable confidence score gate before implementation |
confidence_gate.readiness_threshold |
85 |
Minimum readiness score (1-100) required to proceed |
confidence_gate.outcome_threshold |
65 |
Minimum outcome confidence score (1-100) required to proceed |
tdd_mode |
false |
Enable TDD mode: write failing tests before implementation |
max_refine_count |
5 |
Maximum lifetime /ll:refine-issue full-rewrite calls per issue (1–20). Gap-analysis runs (--gap-analysis) are exempt: they log a discriminated Session Log entry (/ll:refine-issue:gap-analysis) that is not counted toward the cap. Enforced by refine-to-ready-issue and directly by check_attempt_budget in recursive-refine before each sub-loop entry |
recursive_refine.max_depth |
3 |
Maximum decomposition depth per subtree for the recursive-refine loop (1–∞, integer); issues at or beyond this depth are skipped with reason depth-cap and recorded in .loops/tmp/recursive-refine-skipped-depth.txt instead of being passed to size-review |
rate_limits.max_wait_seconds |
21600 |
Total wall-clock budget (seconds) spent retrying 429s before routing to on_rate_limit_exhausted (default 6h) |
rate_limits.long_wait_ladder |
[300, 900, 1800, 3600] |
Long-wait tier backoff ladder (seconds): 5 min → 15 min → 30 min → 1 h. Each 429 after the short-burst tier advances the index, capped at the last entry |
rate_limits.circuit_breaker_enabled |
true |
Enable cross-worktree circuit breaker: prompt-mode actions pre-sleep until estimated_recovery_at when a peer worker has observed a 429 |
rate_limits.circuit_breaker_path |
".loops/tmp/rate-limit-circuit.json" |
Path to the shared circuit-breaker sidecar file read/written by all ll-parallel workers |
commands.review_epic¶
Configuration for the /ll:review-epic skill:
| Key | Default | Description |
|---|---|---|
review_epic.stale_days |
14 |
Days without activity before a child issue is considered stalled. |
review_epic.enable_scope_drift_check |
true |
Enable LLM-based scope-drift and missing-coverage passes. |
When confidence_gate.enabled is true, manage-issue checks the issue's confidence_score frontmatter before Phase 3 (Implementation). If the score is below readiness_threshold, implementation halts. Use --force-implement to bypass.
readiness_threshold / outcome_threshold also drive the confidence gates inside the
autodev, recursive-refine, eval-driven-development, refine-to-ready-issue,
rn-implement, and rn-remediate built-in loops. Those loops no
longer hardcode the values: at launch the runner seeds context.readiness_threshold and
context.outcome_threshold from commands.confidence_gate.* (BUG-2767). Precedence, highest
first:
--context readiness_threshold=95on thell-loop runcommand line- A
context:literal declared in the loop YAML (built-in loops deliberately declare none) commands.confidence_gate.readiness_threshold/.outcome_thresholdinll-config.json- Schema defaults — 85 / 65
An unconfigured project therefore gates at 85/65 in every built-in loop (BUG-2768 reconciled the last three loops, which previously pinned 85/65 or 85/75 literals that shadowed this config).
When tdd_mode is true, manage-issue splits Phase 3 into Phase 3a (Write Tests — Red) and Phase 3b (Implement — Green). In Phase 3a, tests are written based on the plan's acceptance criteria and must fail against the current codebase. In Phase 3b, implementation code is written to make those tests pass.
Per-issue override: Set testable: false in an issue's YAML frontmatter to skip Phase 3a for that issue even when tdd_mode is true. Use this for documentation-only changes, prompt-file edits, or any issue where automated testing is not applicable. See ISSUE_TEMPLATE.md for details.
scan¶
Codebase scanning configuration:
| Key | Default | Description |
|---|---|---|
focus_dirs |
["src/", "tests/"] |
Directories to scan |
exclude_patterns |
Standard patterns | Paths to exclude from scanning |
custom_agents |
[] |
Custom scanning agents to include |
prompt_optimization¶
Automatic prompt optimization settings (/ll:toggle-autoprompt). When enabled, each user message is evaluated for clarity before being sent to Claude — ambiguous or under-specified prompts are rewritten to be more actionable.
Opt-in: enabled defaults to false, and ll-init omits the section unless you ask for it (--enable prompt_optimization, or the TUI prompt). Turn it on later with /ll:toggle-autoprompt enabled.
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable automatic prompt optimization |
mode |
"quick" |
Optimization mode ("quick" or "thorough") |
confirm |
true |
Show diff and ask for confirmation before applying |
bypass_prefix |
* |
Prefix character to skip optimization for that message |
clarity_threshold |
6 |
Minimum clarity score (1–10) to pass through unchanged |
Mode differences:
- quick — Checks wording clarity and specificity only. Fast (< 1 s). Catches vague requests like "fix the bug" but won't add codebase-specific context.
- thorough — Also searches the codebase for relevant files, patterns, and conventions to enrich the prompt with concrete references. Slower (5–15 s depending on project size) but produces significantly more precise prompts.
clarity_threshold: Prompts that score at or above this value (1–10) are passed through unchanged. Score 1–5 = vague/generic; 6 = adequately specific; 7–10 = precise with concrete references. Lower the threshold to optimize more aggressively; raise it to reduce interruptions on already-clear prompts.
bypass_prefix: Prepend this character to any message to skip optimization entirely for that message. Default *, so *just do it skips optimization. Useful for one-off commands, raw prompts, or when the optimization would lose intentional ambiguity.
When to disable: Turn off (enabled: false) for codebases with domain-specific shorthand where optimization rewrites valid terminology, or when running in fully automated pipelines where prompts are pre-authored.
continuation¶
Session continuation and handoff settings (/ll:handoff, /ll:resume):
| Key | Default | Description |
|---|---|---|
enabled |
true |
Enable continuation prompt features |
include_todos |
true |
Include todo list state in continuation prompt |
include_git_status |
true |
Include git status in continuation prompt |
include_recent_files |
true |
Include recently modified files in continuation prompt |
max_continuations |
3 |
Max automatic session continuations for CLI tools |
prompt_expiry_hours |
24 |
Hours before continuation prompt is considered stale |
context_monitor¶
Context window monitoring for automatic session handoff. See Session Handoff Guide for full details.
| Key | Default | Description |
|---|---|---|
enabled |
true |
Enable context window monitoring (enabled by default; all project templates include this setting) |
auto_handoff_threshold |
80 |
Context usage percentage to trigger handoff warning |
state_file |
.ll/ll-context-state.json |
File used to track context usage state between turns |
context_limit_estimate |
0 (auto) |
Override for the context window token limit. Omit or set to 0 for auto-detection ([1m]-suffixed model ids resolve to 1M by identifier; known claude--4 base models → 200000; transcript baseline exceeding the resolved limit auto-upgrades to 1000000 as a fallback). Set to an explicit non-zero value to override, e.g. 1000000 for 1M-context models. Also overridable via LL_CONTEXT_LIMIT env var. |
use_transcript_baseline |
true |
Use JSONL transcript token counts as an API-exact baseline (one-turn lag). Part of the three-tier token priority system: result_token_count > 0 (zero-lag authoritative, written by the on_usage callback from stream-json result events) → transcript baseline (one-turn lag, ±5–15%) → pure heuristics (±30–50%). This setting enables the second tier; the first tier (result_token_count) is always active when available. |
session_capture¶
Continuous session event capture (FEAT-1262). When enabled, session-capture.sh
appends one structured event record per tool invocation to .ll/ll-session-events.jsonl,
providing the data source for FEAT-1264's PreCompact handoff snapshot builder.
Default is off; opt in alongside FEAT-1264.
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable per-tool event capture to .ll/ll-session-events.jsonl. |
analytics¶
Context-window analytics settings (FEAT-1160 family). When enabled, the
post_tool_use hook (FEAT-1623) persists per-tool byte metrics
(bytes_in / bytes_out / cache_hit) into .ll/history.db for
consumption by /ll:ctx-stats (FEAT-1624). Default is off; opt in once
the ctx-stats CLI ships.
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable per-tool byte tracking and file-event recording in the post_tool_use hook. When false, the handler is a no-op and writes nothing to SQLite (tool_events or file_events). |
analytics.capture¶
Per-category gating for analytics writes (ENH-1840). All categories default to enabled; set individual fields to restrict which data is collected. ll-doctor reports the current state of these settings.
| Key | Type | Default | Description |
|---|---|---|---|
analytics.capture.skills |
list[str] |
["*"] |
Skill names whose invocations are recorded. ["*"] captures all; use an explicit list to restrict (e.g. ["refine-issue", "scan-codebase"]). |
analytics.capture.cli_commands |
list[str] |
["*"] |
CLI command names whose invocations are recorded. ["*"] captures all. |
analytics.capture.corrections |
bool |
true |
Record user correction events into user_corrections. |
analytics.capture.file_events |
bool |
true |
Record file-read/write events into file_events. When false, ll-ctx-stats will not have per-file byte data. |
analytics.capture.usage_events |
bool |
true |
Capture real LLM token-usage events (input/output/cache tokens + derived cost) into usage_events (ENH-2461). Populated by both the raw_events backfill parser (_backfill_usage_events, historical rows, state always NULL) and the live per-invocation writer at loop-run finish (state populated, ENH-2724); disabling this flag skips the live writer's INSERTs. |
analytics.capture.hooks |
bool |
true |
Capture per-fire hook telemetry (exit code, duration, stderr preview) into hook_events (ENH-2506). Absent-key configs still capture (forward-compat: read via AnalyticsCaptureConfig, not a raw dict lookup) — only an explicit false disables it. |
analytics.capture.correction_patterns |
list[str] |
[] |
Additional regex patterns appended to the built-in correction detector. Built-ins always remain active; absent config leaves behavior unchanged. Patterns are raw regex strings. |
Example — disable file-event recording:
analytics.retention¶
Retention policy for history.db raw event tables (ENH-1906). Pruning is dual-gated: both min_project_age_days and min_db_size_mb must be exceeded before any rows are deleted. This protects fresh or small projects from accidental data loss. High-value tables (issue_events, user_corrections) are never pruned regardless of settings.
Run ll-session prune --dry-run to preview what would be deleted before committing.
| Key | Type | Default | Description |
|---|---|---|---|
analytics.retention.min_project_age_days |
integer |
365 |
Minimum project age in days (MIN(started_at) from sessions table) before pruning is allowed. Both gates must be exceeded. |
analytics.retention.min_db_size_mb |
integer |
800 |
Minimum .ll/history.db file size in MB before pruning is allowed. Both gates must be exceeded. |
analytics.retention.raw_event_max_age_days |
integer\|null |
90 |
Delete rows older than N days from tool_events, cli_events, file_events, and message_events. null disables per-table pruning. |
Example — reduce raw-event retention to 30 days once the DB reaches 200 MB:
{
"analytics": {
"retention": {
"min_project_age_days": 180,
"min_db_size_mb": 200,
"raw_event_max_age_days": 30
}
}
}
history¶
History.db read/consume configuration (ENH-1913). Single namespace owner for all .ll/history.db
consumer tunables. The producer side (hooks, SQLiteTransport) is always active when analytics is
enabled; these keys control how skills and CLI tools read that data.
| Key | Type | Default | Description |
|---|---|---|---|
history.db_path |
string\|null |
null |
Override the default .ll/history.db location; relative paths resolve against the project root. The LL_HISTORY_DB env var takes precedence over this (ENH-2623). Independent kill switch: LL_ANALYTICS_CAPTURE=0 suppresses ll-* analytics capture entirely — the db is never resolved (so neither this key nor LL_HISTORY_DB is consulted) and no file is created (ENH-3449). |
history.workspace_manifest_path |
string\|null |
null |
Path to an ll-workspace.yaml manifest declaring workspace membership for cross-repo .ll/history.db aggregation (FEAT-3409). Relative paths resolve against the project root; ~ is expanded (a deliberate divergence from history.db_path, which does not expand ~). Overrides the nearest-ancestor-walk default discover_workspace_members() otherwise uses. A declared-but-missing manifest — this key set to a nonexistent path — raises FileNotFoundError rather than degrading to an empty member list; only an undeclared (ancestor-walk) miss degrades. A member repo's own history.db_path is not consulted by workspace discovery — a member with a custom one must repeat it in the manifest. |
history.velocity_window |
integer |
10 |
Number of recent issues to use when computing velocity (ENH-1905). |
history.effort_fields |
list[str] |
["session_count", "cycle_time_days"] |
Fields extracted from history.db for effort reporting (ENH-1905). |
history.max_age_days |
integer\|null |
null |
Maximum age in days for history entries; null = no limit (ENH-1905). |
history.planning_skills |
list[str] |
["create-sprint", "scope-epic", "manage-issue", "review-epic"] |
Skill names whose sessions are included in planning context queries (ENH-1909). |
history.session_digest¶
Opt-in project-context snapshot injected at session start (ENH-1907). Queries history.db and
prepends a <project_context> block to session context so every new session gets a "what's been
happening lately" summary. Default: enabled (opt-out via history.session_digest.enabled: false). Run ll-history-context --project to preview.
| Key | Type | Default | Description |
|---|---|---|---|
history.session_digest.enabled |
boolean |
true |
Gate flag — set false to disable injection. |
history.session_digest.days |
integer |
7 |
Freshness window in days; rows older than this are excluded. |
history.session_digest.char_cap |
integer |
1200 |
Hard character ceiling on the injected block. Truncates with +N more. |
history.session_digest.sections |
list[str] |
[] |
Ordered list of section keys to render. Empty = all providers. Supported: "touched_files", "completed_issues", "recurring_corrections". |
history.evolution¶
Feedback evolution configuration (ENH-1911). Controls which correction patterns surface in evolution analysis.
| Key | Type | Default | Description |
|---|---|---|---|
history.evolution.feedback_min_recurrence |
integer |
2 |
Minimum recurrence count for a correction to surface in evolution analysis. |
history.evolution.bypass_min_count |
integer |
2 |
Minimum bypass count threshold for evolution signal suppression. |
history.go_no_go¶
Go/no-go decision scoring configuration (ENH-1914).
| Key | Type | Default | Description |
|---|---|---|---|
history.go_no_go.correction_penalty |
number |
-0.2 |
Score penalty applied per correction event in go/no-go scoring. |
history.capture_issue¶
Capture-issue deduplication configuration (ENH-1914).
| Key | Type | Default | Description |
|---|---|---|---|
history.capture_issue.dup_overlap_threshold |
number |
0.7 |
Overlap ratio threshold above which a new issue is considered a duplicate. |
history.compaction¶
LCM-style three-level compaction for summary_nodes (FEAT-1712). Controls whether ll-session backfill generates LLM summaries over message_events blocks and stores them as a summary DAG. Disabled by default to avoid background LLM calls without user opt-in.
Three-level LCM Algorithm 3 escalation: When enabled, each block of message events is summarized progressively:
- Level 1 — Normal LLM summary (default, up to
budget_tokens): A standard LLM call generates a concise summary of the message block. If the summary is within budget and converged, it stops here. - Level 2 — Aggressive bullet-point LLM: If Level 1 produces more than one summary paragraph (or exceeds half the budget), a second LLM call condenses the output into tight bullet points.
- Level 3 — Deterministic truncation: If Level 2 still produces >1 paragraph (or the LLM is unavailable), the summarizer falls back to a deterministic character-based truncation — no LLM call. This guarantees termination without runaway costs.
Each summary is stored as a node in summary_nodes. Condensed nodes receive parent_id linkage back to their source leaves, forming an N-level DAG traversal path. ll-session grep and ll-session expand use a recursive CTE to drill from any condensed node (at any depth) through descendant leaves back to source messages.
Cross-session recursive condensation (ENH-1954): When cross_session_enabled is true (default), the compaction pass recurses over existing condensed nodes level by level after per-session compaction finishes. At each level, condensed nodes are grouped by token budget (same greedy algorithm as per-session block accumulation), summarised, and inserted as higher-order condensed nodes (session_id=NULL, level=1+). Recursion continues until exactly one project-root summary node remains — providing a single, top-level summary of the entire project's session history. Set max_level to cap the recursion depth.
| Key | Type | Default | Description |
|---|---|---|---|
history.compaction.enabled |
boolean |
false |
Gate flag — set true to enable LLM summarization during backfill. |
history.compaction.budget_tokens |
integer |
4096 |
Token budget per summary node. |
history.compaction.model |
string\|null |
null |
Model override for summary generation; null uses the session default. |
history.compaction.timeout |
integer |
60 |
Timeout in seconds for each LLM summarization call. |
history.compaction.cross_session_enabled |
boolean |
true |
Enable recursive cross-session condensation (ENH-1954). Set false to preserve pre-ENH-1954 per-session-only behavior. |
history.compaction.max_level |
integer\|null |
null |
Maximum condensation depth. null means no limit — recurses until one root remains. |
{
"history": {
"compaction": {
"enabled": true,
"budget_tokens": 4096,
"model": null,
"timeout": 60,
"cross_session_enabled": true,
"max_level": null
}
}
}
Example — enable session digest and tighten velocity window:
{
"history": {
"velocity_window": 5,
"max_age_days": 90,
"session_digest": {
"enabled": true,
"days": 7,
"char_cap": 1200
}
}
}
queue¶
ll-queue persistence configuration (FEAT-2682). Owns the .ll/queue.db location for the
persisted work-item queue (add/list/status/remove).
| Key | Type | Default | Description |
|---|---|---|---|
queue.db_path |
string\|null |
null |
Override the default .ll/queue.db location; relative paths resolve against the project root. |
skill_budget¶
Skill file token-budget enforcement, consumed by ll-verify-skill-budget and ll-doctor's skill-budget check.
| Key | Type | Default | Description |
|---|---|---|---|
skill_budget.threshold_tokens |
integer |
2000 |
Token budget threshold for skill files. |
Precedence, highest to lowest:
--thresholdCLI flag onll-verify-skill-budget(overridesll-config.json).skill_budget.threshold_tokensin.ll/ll-config.json.- Built-in default (
2000).
sprints¶
Sprint management settings (ll-sprint, /ll:create-sprint):
| Key | Default | Description |
|---|---|---|
sprints_dir |
.sprints |
Directory for sprint definitions |
default_timeout |
3600 |
Default timeout per issue in seconds |
default_max_workers |
2 |
Worker count for parallel execution within waves (1-8) |
max_issue_wall_clock_time |
2700 |
Hard per-issue wall-clock timeout in seconds. If an issue (including continuations) exceeds this limit, the orchestrator kills it and proceeds to the next issue. |
sync¶
GitHub Issues synchronization for /ll:sync-issues:
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable GitHub Issues sync feature |
provider |
"github" |
Issue tracking provider (currently only GitHub) |
github.repo |
null |
GitHub repository in owner/repo format (auto-detected if null) |
github.label_mapping |
{"BUG": "bug", ..., "EPIC": "epic"} |
Map issue types (BUG/FEAT/ENH/EPIC) to GitHub labels |
github.priority_labels |
true |
Add priority as GitHub label (e.g., "P1") |
github.sync_completed |
false |
Also sync completed issues (close on GitHub) |
github.state_file |
.ll/ll-sync-state.json |
File to track sync state |
github.pull_template |
"minimal" |
Creation variant for issues pulled from GitHub ("full", "minimal", or "legacy"). Determines section structure of the generated issue file. |
github.pull_limit |
integer |
500 |
To enable sync, set sync.enabled: true. The repository is auto-detected from your git remote; set sync.github.repo to override.
documents¶
Document category tracking for /ll:align-issues:
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable document category tracking |
categories |
{} |
Document categories with file lists |
To enable document tracking, set documents.enabled: true and define categories:
{
"documents": {
"enabled": true,
"categories": {
"architecture": {
"description": "System design and technical decisions",
"files": ["docs/ARCHITECTURE.md", "docs/reference/API.md"]
}
}
}
}
Each category requires a files array of relative paths. The optional description field documents what the category covers.
design_tokens¶
Design system token settings for artifact-generating loops. When enabled, ll-loop run and ll-loop resume pre-inject the resolved token set into the FSM initial context before the first state is entered.
Defaulting rule (BUG-3274): a wholly absent design_tokens section resolves to off — no scaffolding, no warnings, matching a project that never opted in. Within a present design_tokens section, an omitted enabled key resolves to the dataclass default (true). This two-tier rule is unrelated to the per-loop use_design_tokens opt-out described next, which has its own independent absent-key-means-true convention.
Per-loop opt-out (ENH-3099): an individual loop can skip token injection even when design_tokens.enabled is true globally, by setting use_design_tokens: false in its YAML context: block (or via --context use_design_tokens=false at run time). Accepted falsy string values are case-insensitive false, no, off, 0, or an empty string; anything else (including the key being absent) is treated as true. When opted out, context.design_tokens_context and context.design_guidance_context (the DESIGN.md prose body, ENH-3267) are still set to "" so ${context.design_tokens_context} / ${context.design_guidance_context} interpolate without error in prompts that reference them unconditionally.
Multi-Profile System (ENH-1768)¶
Design tokens are organized into profiles under path/profiles/. Each profile is a self-contained directory with its own primitives.json, semantic.json, spacing.json, typography.json, and themes/ subdirectory.
| Key | Type | Default | Description |
|---|---|---|---|
active |
str |
"default" |
Name of the active profile; must match a subdirectory in path/profiles/. |
profiles_dir |
str\|null |
null |
Subdirectory under path containing profile directories. Defaults to "profiles" at runtime when null. |
Built-in profiles:
- default — WCAG AA accessible palette
- editorial-mono — monochrome editorial theme
- warm-paper — warm paper-like palette
Profile directory layout:
.ll/design-tokens/profiles/
├── default/
│ ├── primitives.json
│ ├── semantic.json
│ ├── spacing.json
│ ├── typography.json
│ └── themes/
│ ├── light.json
│ └── dark.json
├── editorial-mono/
│ └── ...
└── warm-paper/
└── ...
Run /ll:configure design-tokens to interactively set up profiles and select the active one.
DESIGN.md import source (ENH-3264)¶
As an alternative to the multi-file profile layout, the loader can read a single root
DESIGN.md file — a Google Labs draft
spec (alpha, Apache-2.0) for describing a visual identity to coding agents in one
human-readable file. This is an import edge only: profiles remain the canonical
internal model, and nothing is ever written to disk from a DESIGN.md (no generated
profiles/<name>/*.json, no scaffolded starter file).
| Key | Type | Default | Description |
|---|---|---|---|
source |
"auto"\|"profile"\|"design_md" |
"auto" |
Selects the token source format. |
"auto"(default) — prefers a materialized profile (at least one ofprimitives.json,semantic.json,typography.json,spacing.jsonpresent under the resolved token root); falls back to a rootDESIGN.mdwhen no profile is materialized. Selection is based on what's on disk, not on whetheractivehas been explicitly set (it defaults to"default"either way, so "unset" isn't observable)."profile"— always uses the multi-file profile layout, ignoring any rootDESIGN.md."design_md"— always reads the project's rootDESIGN.md; degrades to no tokens (with a stderr warning) if the file is absent.
Discovery is case-exact and not configurable. The loader looks for a file named
exactly DESIGN.md (not design.md or Design.md) at the project root only — no
recursive search, no path override. This matters because Path.exists() is
case-insensitive on APFS (macOS) and NTFS, so a naive existence check would
incorrectly match a lowercase design.md; the loader lists the directory and
compares names exactly instead.
Namespace mapping. DESIGN.md's flat frontmatter blocks are renamed onto profile
namespaces before resolution: colors → color, typography → font, spacing →
space, rounded → radius. Well-known color names (Material-Design-style —
primary, secondary, on-*, surface*, background, outline*, error, ...) are
further mapped onto the four semantic roles (surface/text/border/action) so
the generator prompt gets grouped, role-labeled output instead of a flat hex dump;
unrecognized names fall into a residual "Other" group rather than being dropped.
{path.to.token} alias references are rewritten to match, so a value like
"{colors.primary}" still resolves correctly even though the key it points at moved
namespaces (and, for role-mapped colors, an extra level deeper).
Not supported from DESIGN.md:
- Themes. The spec has no theme mechanism (one flat colors: map, no light/dark
split). A DESIGN.md source resolves to a single theme; active_theme is silently
ignored for the default (theme=None) call path used by ll-loop run/resume, and
a caller that passes an explicit theme= (only artifact_template_kit.themed_css_vars,
used by ll-artifact HTML generators) gets a one-time stderr warning instead. Projects that
need light/dark keep using profiles.
- components:. Structural guidance, not tokens — it is dropped before token
resolution and never appears in render_as_prompt_context()/render_as_css_vars()
output. (It does feed the prose-guidance channel introduced by a later change.)
- Unitless numeric dimensions. A hand-authored rounded: {sm: 4} (no unit) is
emitted verbatim as invalid CSS (--radius-sm: 4;). DESIGN.md numeric values must
carry their units (4px, 0.25rem, ...) — this is an authoring contract, not
something the loader corrects.
Malformed-but-partially-scannable YAML frontmatter is accepted (the house frontmatter
parser's permissive line-based fallback salvages what it can rather than raising), and
an unresolvable {ref} degrades to a token-empty result with a stderr warning instead
of raising — unlike the profile path, where the same conditions raise ValueError.
Either way, the file's prose body (frontmatter stripped) still parses and is available
on the result even when no usable tokens were found.
Auto-scaffolding built-in profiles¶
When you run /ll:configure and select or enable a built-in design token profile, the skill detects whether the profile directory is missing from disk and offers to materialize it automatically via shutil.copytree:
-
Case A — switching active profile to an unmaterialized built-in: If
activeis changed todefault,editorial-mono, orwarm-paperbut that profile subdirectory does not yet exist underpath/profiles/,/ll:configureprompts you to scaffold it. Accept to copy the built-in bundle from the ll package into your project's profile directory. -
Case B — enabling design tokens for the first time with no profiles directory: If
enabledis flipped fromfalsetotrue(or was absent) and noprofiles/directory exists at all, all three built-in profiles are copied in one operation without prompting.
Both cases accept automatically when DANGEROUSLY_SKIP_PERMISSIONS is set or when /ll:configure is invoked with --auto.
Custom or unknown profile names (anything not in the built-in list) are unaffected — a warning is emitted if the directory is missing, but no scaffold is offered.
Legacy flat structure (pre-ENH-1768):
When no profiles/ directory is present, the resolver falls back to the flat layout:
| Key | Type | Default | Description |
|---|---|---|---|
primitives_file |
str |
"primitives.json" |
Filename for primitive (raw) token values within path. |
semantic_file |
str |
"semantic.json" |
Filename for semantic (aliased) token values within path. |
themes_dir |
str |
"themes" |
Subdirectory of path containing per-theme override files. |
active_theme |
str |
"dark" |
Name of the active theme; must match a file in themes_dir. |
W3C DTCG $value Format (ENH-1769)¶
The design token loader supports the W3C Design Tokens Community Group $value format in addition to the legacy flat key-value layout. When a token file contains $value keys (e.g., {"color-primary": {"$value": "#A3B59A"}}), the loader normalizes them to the internal representation automatically — detection is unconditional/automatic based on file content, with no config key to toggle it.
Example DTCG token file:
See also: Design Tokens Community Group specification
artifacts¶
Output settings for ll-artifact, the generator of self-contained human-facing HTML artifacts (FEAT-2390). Backs the policy-builder subcommand (stamps design-token CSS vars, the canonical predicate grammar, and the skill/command catalog into a file://-safe policy-router / rubric loop builder page), render (FEAT-3036 Phase 1), which resolves and deterministically renders user-authored .llat/ artifact templates, and templatize (FEAT-3308), which both reads and writes templates_dir: it splices a generated artifact into a reusable .llat/ template and promotes the result there. The page shell and design-token stamping shared across these are factored into artifact_template_kit.py (ENH-3035), not owned by policy-builder alone.
| Key | Type | Default | Description |
|---|---|---|---|
default_output_dir |
str |
"." |
Directory where ll-artifact writes generated artifacts when no --output/-o override is given. Relative paths resolve against the project root. |
templates_dir |
str |
"artifacts/templates" |
Directory (relative to the project root) where named .llat/ artifact templates are looked up by ll-artifact render <name> when the given argument does not resolve as a filesystem path. Also the default write target for ll-artifact templatize's -o resolution (FEAT-3314) — when -o is omitted, templatize writes <templates_dir>/<artifact-stem>.llat. |
templatize_max_input_bytes |
int |
400000 |
Size ceiling, in bytes (not tokens), guarding ll-artifact templatize's LLM region-discovery call when --regions is omitted (FEAT-3315) and ll-artifact extract's LLM extraction call (FEAT-3310). The measured quantity differs per subcommand: templatize measures combined artifact+source-document size; extract measures the source document alone. Exceeding it exits 1 naming the measured size, with no host call issued. |
promotion_dir |
str |
".loops/artifacts" |
Destination directory for loop→artifact handoff promotion (FEAT-3309) — where a loop's declared artifact_output deliverable is copied on an authorized terminal (ll-loop run/resume). Relative to the project root, created on demand. Deliberately distinct from default_output_dir, which defaults to . — promotion never uses default_output_dir, so it never drops a file into the project root by default. Also the default output directory for ll-artifact dashboard, for the same reason. |
export |
object |
see below | history.db snapshot export policy for ll-artifact dashboard (FEAT-3304). Exactly two keys in v1; additionalProperties: false applies to this nested block too. |
artifacts.export¶
| Key | Type | Default | Description |
|---|---|---|---|
mode |
str |
"shareable" |
"shareable" | "local". In shareable mode the ENH-075 per-column allowlist is applied and --tables may select only from the types it covers — it cannot widen the allowlist. In local mode the column projection is lifted (SELECT *) and any ll-session export type may be selected, for personal use only. ll-artifact dashboard --local overrides this per invocation. Both modes stamp the mode and the allowlist version visibly into the generated page, so a recipient can always tell which produced the file. |
max_artifact_bytes |
int |
8000000 |
Size ceiling, in bytes, for a generated dashboard, measured on the final rendered HTML — the quantity that actually bites the user. Exceeding it exits 1 naming the measured size and the limit, and writes no file; narrowing --since is the only user-side fix. A cheap pre-check applies the same ceiling to the raw snapshot before gzip/base64/render so an all-history export fails fast instead of materializing hundreds of MB first. Calibrated against a measured 30-day export of a 6.6 GB history.db: ~4.1 MB gzip+base64 snapshot plus sql.js's ~0.92 MB fixed floor ≈ 5.0 MB, leaving ~3 MB of headroom (roughly a 55–60-day window before the ceiling bites). |
The ENH-075 allowlist itself is deliberately not configurable: it is a code
constant with a version marker (_SHAREABLE_COLUMNS /
_SHAREABLE_ALLOWLIST_VERSION in session_store/queries.py), so a shared
artifact's stamp cannot be forged by editing local config. ENH-075's sketched
"additions" field is deferred — an unversioned project-local widening would make
the allowlist-version stamp ambiguous.
"artifacts": {
"default_output_dir": ".",
"templates_dir": "artifacts/templates",
"templatize_max_input_bytes": 400000,
"promotion_dir": ".loops/artifacts",
"export": {
"mode": "shareable",
"max_artifact_bytes": 8000000
}
}
Per-project config only needs an artifacts block to override these defaults; the dataclass defaults suffice otherwise.
decisions¶
Decisions and rules log configuration (FEAT-1891). When enabled, architectural decisions and project rules are persisted to a log for traceability. Storage is hybrid: new entries are append-only per-entry fragments under .ll/decisions.d/*.json, folded into the legacy .ll/decisions.yaml flat file on compaction (BUG-2642). Reads union both tiers; a fresh install has only the fragment directory. The fragment directory is derived from log_path (its .d-suffixed sibling) and is not independently configurable (BUG-2647).
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Enable the decisions log feature. |
log_path |
str |
".ll/decisions.yaml" |
Path to the legacy flat file; the derived fragment directory is its .d-suffixed sibling (.ll/decisions.d/). |
auto_generate |
list[str] |
[] |
Issue type prefixes that filter which issue types are processed when running ll-issues decisions generate. Empty list processes all types. Example: ["FEAT", "ENH"] skips BUG entries. |
Integrity gate (ENH-2591). The local test suite
(python -m pytest scripts/tests/) is this project's CI per .claude/CLAUDE.md.
A pytest belt at scripts/tests/test_decisions_yaml_gate.py
shells out to ll-verify-decisions against the live decisions log — both the
flat .ll/decisions.yaml and the .ll/decisions.d/*.json fragments, which the
validator re-globs in a strict second pass (positive case) and an OTHE-203 corrupted tmp_path fixture (negative
case), so any YAML parse error, missing required field, or unknown
entry-type discriminator fails the local suite — closing the
git commit --no-verify and non-hook edit paths that the pre-commit
hook (ENH-2590) alone cannot cover. The gate skips gracefully when
ll-verify-decisions is absent from PATH.
Claude-side host belt (ENH-2592). A sibling belt at
hooks/scripts/check-decisions-yaml.sh
runs as a Claude Code PreToolUse hook on every Write/Edit of
.ll/decisions.yaml or a .ll/decisions.d/*.json fragment, blocking
(host-level exit 2) corruption before the file is even written. It validates the candidate content
(tool_input.content for Write, old_string → new_string reconstruction
for Edit), staged in a temp config root, against the same ll-verify-decisions
binary. Only this host-layer belt fires for Claude-driven writes inside the
session — direct editor edits bypass it; the pre-commit hook + pytest gate
remain the authoritative backstops. The hook skips gracefully when
ll-verify-decisions or python3 is missing.
learning_tests¶
Master switch for the learning test registry feature. When enabled, skills and loops can query .ll/learning-tests/ via ll-learning-tests to check whether a target API or pattern is already proven before re-doing the work. Records are stored as YAML-frontmatter markdown files under .ll/learning-tests/<slug>.md.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Enable the learning test registry and ll-learning-tests CLI. When disabled, ll-learning-tests exits with a message and skills skip proof checks. |
auto_prove |
bool |
true |
When enabled, rn-implement's learning gates (pre-dequeue check_learning_ready and the remediation-path prove_rem_learning_gate) make one ll-learning-tests prove <target> attempt before parking an issue on an unproven external-API target. Set false to keep the gates check-only for budget-conscious runs. Overridable per-run via the auto_prove_learning_gate context flag (ENH-2487). |
stale_after_days |
int |
30 |
Days after which a record is considered stale and should be re-validated. Since ENH-3125 this is the age half of a two-part predicate — see version_aware_staleness. |
version_aware_staleness |
bool |
true |
Treat installed-version drift as staleness, not calendar age alone (ENH-3125). A record carrying a proven_version that differs from the currently installed version of proven_package is stale immediately (same day it was proven); one whose version still matches is stale only past the version_match_backstop_multiplier backstop. Records with no captured version — stdlib targets, free-text targets, and anything not yet run through ll-learning-tests backfill-versions — keep pure stale_after_days behavior either way. Set false to restore pre-ENH-3125 behavior for all records. |
version_match_backstop_multiplier |
int |
12 |
Multiplier applied to stale_after_days for records whose captured version still matches (ENH-3125). Default 12 means such a record ages out at 30 × 12 days (~1 year) rather than never: proof also decays when your own usage of an API changes, and a hard-pinned dependency would otherwise never be re-verified. |
discoverability.mode |
str |
"warn" |
How learning-test gaps are surfaced: "off" — silent; "warn" — emits a one-line hint and allows the tool call; "block" — injects feedback into model context and blocks the Write/Edit. Hook behavior: the PreToolUse gate (active for Claude Code and Codex; opt-in for OpenCode) fires on every Write or Edit call, detects unknown external imports, and consults the registry. |
discoverability.skip_packages |
list[str] |
["std", "typing", "os", "sys"] |
Packages whose imports are never flagged by the PreToolUse gate. Add internal packages or well-known stdlib re-exports here to suppress false positives. |
release_gate |
str |
"warn" |
Pre-release audit behavior when stale/refuted records are found for imported packages: "block" aborts with exit 1; "warn" (default) continues with a visible warning. |
scan_dirs |
list[str] |
["scripts/"] |
Source directories to scan for Python imports during the pre-release audit and orphaned record detection. |
{
"learning_tests": {
"enabled": false,
"auto_prove": true,
"stale_after_days": 30,
"version_aware_staleness": true,
"version_match_backstop_multiplier": 12,
"discoverability": {
"mode": "warn",
"skip_packages": ["std", "typing", "os", "sys"]
}
}
}
Run /ll:configure learning-tests to enable and set up the registry directory.
See LEARNING_TESTS_GUIDE.md for the full workflow.
prepatch_check¶
Pre-patch check configuration (ENH-3142). When enabled, candidate tests added or modified by a verification step's diff are run against a worktree forked at the pre-patch base — a test that passes without the change it claims to demonstrate is flagged rather than silently accepted as evidence.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Master switch. When disabled, little_loops.prepatch_check.run_prepatch_check() records the skip via skipped_reason rather than omitting the section. |
timeout_s |
int |
300 |
Per-invocation time box (seconds) for the pre-patch pytest subprocess. A candidate test that hangs pre-patch (waiting on a fixture or port that only exists post-patch) is killed at this limit; every candidate with no reported result from that invocation is categorized timeout. |
modified_hard |
bool |
false |
Escalate a modified-and-passes-pre-patch outcome from soft to hard. A newly added test that passes pre-patch is always hard-flagged; a modified test is soft by default since legitimate assertion-strengthening routinely passes pre-patch too. |
loops¶
FSM loop settings:
| Key | Default | Description |
|---|---|---|
loops_dir |
.loops |
Directory for loop definitions and runtime state |
glyphs.prompt |
✦ |
Badge glyph for prompt action states in FSM box diagrams |
glyphs.slash_command |
/━► |
Badge glyph for slash_command action states |
glyphs.shell |
❯_ |
Badge glyph for shell action states |
glyphs.mcp_tool |
⚡ |
Badge glyph for mcp_tool action states |
glyphs.sub_loop |
↳⟳ |
Badge glyph for sub_loop action states |
glyphs.route |
⑃ |
Badge glyph for route action states |
glyphs.parallel |
∥ |
Badge glyph for parallel action states |
glyphs.learning |
⚗ |
Badge glyph for learning (type: learning) states |
queue_wait_timeout_seconds |
86400 |
Seconds to wait for a conflicting scope lock to release when --queue is used |
throttle (per-state progressive throttling)¶
Controls the ThrottleConfig applied to a state to prevent runaway tool-call loops. Defined inline under a state in loop YAML.
| Field | Default | Description |
|---|---|---|
normal_max |
3 |
Tool calls 1..normal_max pass through unrestricted |
warn_max |
8 |
At warn_max calls, a throttle_warn event is emitted |
hard_max |
12 |
At hard_max calls, routes to on_throttle_hard (or hard stop if unset) |
Use on_throttle_hard: <state> on the same state to route gracefully instead of stopping. See EVENT-SCHEMA.md for the throttle_warn, throttle_hard, and throttle_stop events.
prompt_size_guard (per-loop interpolated-prompt size guard)¶
ENH-2486: a top-level loop block that WARNs (does not route) when a fully-interpolated action reaches warn_chars characters. It surfaces loops that silently re-embed monotonically growing captured outputs/artifacts so the ballooning is observable in <run>.events.jsonl rather than only showing up as recurring cost or an OOM. Default-enabled; disable per-run with --no-prompt-size-guard or override the threshold with --prompt-size-warn-chars N.
| Field | Default | Description |
|---|---|---|
enabled |
true |
Master switch for the guard. |
warn_chars |
50000 |
Interpolated-action char size at/above which a prompt_size_warn event is emitted. 0 disables the guard even when enabled is true. ~12.5K tokens at the 4-chars/token convention. |
The size is measured in characters because the codebase has no tokenizer; the emitted event also reports est_tokens = size // 4. See EVENT-SCHEMA.md for the prompt_size_warn event.
Override individual glyphs to customize how FSM box diagrams render state type badges:
loops.run_defaults¶
Persistent CLI defaults for ll-loop run. Values are backfilled when the corresponding flag is absent; explicit CLI flags always take precedence. Set once in ll-init via loops.run_defaults in the generated config.
| Key | Type | Default | Description |
|---|---|---|---|
run_defaults.clear |
boolean |
true |
If true, inject --clear into every ll-loop run invocation. |
run_defaults.show_diagrams |
string\|null |
"clean" |
Inject --show-diagrams <value> into every invocation. Valid values: layered, neighborhood, inline, detailed, summary, clean, local, slim, oneline, default. null disables. |
run_defaults.show_input |
boolean |
true |
Show the input: field/value (packed onto the loop: line) in the ll-loop run diagram header, on both pinned and non-pinned paths. Set false to hide it (e.g. when the input contains sensitive data). |
run_defaults.mode |
string\|null |
null |
Reserved for a future --mode flag on ll-loop run. No effect until that flag is added. |
run_defaults.include |
string |
"" |
Default loop allowlist injected into fsm.context["include"]; empty string = all loops visible. Accepts comma-separated selectors: loop-name, builtin:*, project:*, category:<label>. Override per-invocation with --context include=VALUE. |
run_defaults.delay |
number\|null |
null |
Inject --delay <seconds> into every ll-loop run invocation (inter-iteration pause). Must be a non-negative number. Explicit --delay overrides. null disables (no pause injected). |
scratch_pad¶
Observation masking via scratch pad files to reduce context bloat in automation sessions. When enabled: true, the scratch-pad-redirect PreToolUse hook (hooks/scripts/scratch-pad-redirect.sh) rewrites large Bash outputs to a scratch file + tail, keeping the transcript small. Read is not intercepted — denying a Read edit-locks the file for the session (BUG-2357), and Read is already self-capping via offset/limit.
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable scratch pad instructions for automation sessions |
automation_contexts_only |
true |
Only enforce redirection in automation sessions (ll-auto, ll-parallel, ll-sprint); skip in interactive sessions |
tail_lines |
20 |
Number of lines to surface via tail when redirecting large outputs to a scratch file (5-200) |
command_allowlist |
["cat", "pytest", "mypy", "ruff", "ls", "grep", "find"] |
Shell commands eligible for Bash redirection by the PreToolUse hook |
threshold_lines |
200 |
Retained for config compatibility; no longer affects behavior (previously gated the removed Read interception) |
file_extension_filters |
[".log", ".txt", ".json", ".md", ".py", ".ts", ".tsx", ".js"] |
Retained for config compatibility; no longer affects behavior (previously gated the removed Read interception) |
refine_status¶
Display settings for ll-issues refine-status / ll-issues rs:
| Key | Default | Description |
|---|---|---|
columns |
[] (all defaults) |
Ordered list of columns to display. Valid names: id, priority, size, title, source, norm, fmt, ready, confidence, score_complexity, score_test_coverage, score_ambiguity, score_change_surface, total. Empty list uses the default set. |
elide_order |
["source", "norm", "fmt", "size", "score_change_surface", "score_ambiguity", "score_test_coverage", "score_complexity", "confidence", "ready", "total"] |
Ordered list of columns to drop (first to last) when the table exceeds terminal width. id, priority, and title are always pinned and cannot be elided. Any column omitted from this list (other than pinned columns) is dropped rightmost-first after the explicit list is exhausted. Empty list ([]) restores the default drop sequence. |
Example — drop source and fmt before other columns on narrow terminals:
dependency_mapping¶
Dependency mapping threshold configuration for overlap detection and conflict scoring:
| Key | Default | Description |
|---|---|---|
overlap_min_files |
2 |
Minimum overlapping files to trigger overlap detection |
overlap_min_ratio |
0.25 |
Minimum ratio of overlapping files to smaller set (0.0-1.0) |
min_directory_depth |
2 |
Minimum path segments for directory overlap (e.g., src/components/ = 2) |
conflict_threshold |
0.4 |
Conflict score cutoff: below = parallel-safe, above = dependency proposed (0.0-1.0) |
high_conflict_threshold |
0.7 |
Conflict score above which issues are labeled HIGH conflict (0.0-1.0) |
confidence_modifier |
0.5 |
Confidence reduction applied when dependency direction is ambiguous (0.0-1.0) |
scoring_weights.semantic |
0.5 |
Weight for semantic target overlap (component/function names) |
scoring_weights.section |
0.3 |
Weight for section mention overlap (UI regions) |
scoring_weights.type |
0.2 |
Weight for modification type match |
exclude_common_files |
See below | Infrastructure files excluded from overlap detection |
Default exclude_common_files: ["__init__.py", "pyproject.toml", "setup.py", "setup.cfg", "CHANGELOG.md", "README.md", "conftest.py"]
code_query¶
Code-query provider selection, codegraph db path, and staleness policy, consumed by the
codegraph CodeQueryProvider (ENH-2613, ll-code). With no .codegraph/ index present,
ll-code's auto resolution falls through to the always-available fallback provider
unchanged. ll-init writes {"provider": "auto"} once a codegraph index exists (or after it
builds one via --code-graph), adds .codegraph/ to .gitignore, and ll-doctor reports the
index under Code Graph (ll-code); /ll:configure code-query edits the rest.
| Key | Default | Description |
|---|---|---|
provider |
"auto" |
Code-query provider to use for structural code lookups. One of auto, codegraph, fallback. |
codegraph.db_path |
".codegraph/codegraph.db" |
Path to the codegraph SQLite database. |
codegraph.auto_sync |
true |
Auto-run codegraph sync --quiet when the index is stale (ENH-2863). No-op if the codegraph binary isn't on PATH; never raises on failure/timeout. |
staleness |
"warn" |
How to treat a stale codegraph database relative to source changes. One of strict, warn, off. |
tamper_guard¶
Default tamper-guard policy for the non-FSM verification path (ll-auto/ll-parallel/
ll-sprint, via work_verification.py's verify_work_was_done(), ENH-2935). It supplies
the default that both orchestrators use to resolve run_tamper_guard's (ENH-2933) policy
when weakened/deleted test files are detected in the changed-file set. This key is separate
from the FSM tamper_guard: state key (ENH-2934, see the
Loops Guide "Tamper Guard" section) — it never overrides an
explicit state-level tamper_guard: key.
| Key | Default | Description |
|---|---|---|
policy |
"fail" |
Action taken when a test file (or pytest config file) is modified/deleted since the run began. One of revert (restore tracked files via git checkout --), fail (verification fails), allow (no-op, findings recorded but ignored). |
issues.next_issue¶
Selection behavior for ll-issues next-issue / next-issues. Picks which issue (or ranked list) the commands return. The default confidence_first preset is byte-identical to the legacy hardcoded ordering; the default dependency filter (ENH-2436) now skips issues with unresolved blockers unless --include-blocked is passed. See the CLI reference for the flag.
| Key | Default | Description |
|---|---|---|
strategy |
"confidence_first" |
Named preset. confidence_first: sort by (-outcome_confidence, -confidence_score, priority_int). priority_first: sort by (priority_int, -outcome_confidence, -confidence_score). |
sort_keys |
null |
Optional custom sort. A list of {key, direction} entries that overrides strategy. Valid keys: priority, outcome_confidence, confidence_score, effort, impact, score_complexity, score_test_coverage, score_ambiguity, score_change_surface. Valid directions: asc, desc. |
None-handling: missing values use a per-field sentinel — direction: "desc" puts None after all scored issues; direction: "asc" puts None last.
Unknown strategy or sort_keys[*].key values raise ValueError at config load time rather than falling back to defaults.
Example (prefer raw priority order for a deadline-driven sprint):
Example (custom ordering — complexity first, then priority):
{
"issues": {
"next_issue": {
"sort_keys": [
{ "key": "score_complexity", "direction": "asc" },
{ "key": "priority", "direction": "asc" }
]
}
}
}
cli¶
CLI output settings.
| Key | Default | Description |
|---|---|---|
color |
true |
Enable ANSI color output. Set to false for CI or plain-text terminals. Also suppressed by the NO_COLOR environment variable. Logger instances also respect this setting via use_color_enabled() after configure_output() is called. |
cli.colors.logger¶
Override ANSI color codes for log-level output from all ll-* tools.
| Key | Default ANSI | Appearance |
|---|---|---|
info |
36 |
Cyan |
success |
32 |
Green |
warning |
33 |
Yellow |
error |
38;5;208 |
Orange |
cli.colors.priority¶
Override ANSI color codes for issue priority labels in list and card output.
| Key | Default ANSI | Appearance |
|---|---|---|
P0 |
38;5;208;1 |
Bold orange |
P1 |
38;5;208 |
Orange |
P2 |
33 |
Yellow |
P3 |
0 |
Default |
P4 |
90 |
Gray |
P5 |
90 |
Gray |
cli.colors.type¶
Override ANSI color codes for issue type labels in list and card output.
| Key | Default ANSI | Appearance |
|---|---|---|
BUG |
38;5;208 |
Orange |
FEAT |
32 |
Green |
ENH |
34 |
Blue |
EPIC |
35 |
Purple-magenta |
cli.colors.fsm_active_state¶
ANSI foreground color code for the currently active state box in FSM diagrams (shown with --show-diagrams). This value controls both the border color and the interior background fill: the fg code is automatically converted to its bg equivalent (e.g. "32" → "42") so all interior cells are filled with the highlight color. The state name renders with a contrasting dark foreground (30) over the filled background.
Compound ANSI codes (e.g. "38;5;208") cannot be auto-converted to a bg code and fall back to border-only coloring with no interior fill.
| Key | Default ANSI | Appearance |
|---|---|---|
fsm_active_state |
32 |
Green border + green background fill |
Example — use blue for the active state:
cli.colors.fsm_edge_labels¶
Override the default ANSI color codes used for FSM diagram edge labels and connector line characters. Colors are applied to both the text label and the │, ─, ▼, ▶, and corner characters that form each edge.
| Key | Default ANSI | Appearance | When applied |
|---|---|---|---|
yes |
32 |
Green | Success / affirmative transitions |
no |
38;5;208 |
Orange | Failure / negative transitions |
error |
31 |
Red | Error transitions |
blocked |
31 |
Red | on_blocked routing |
partial |
33 |
Yellow | Partial-success transitions |
retry_exhausted |
38;5;202 |
Deep orange | on_retry_exhausted transitions |
rate_limit_exhausted |
38;5;214 |
Amber | on_rate_limit_exhausted transitions |
next |
90 |
Gray | Default/unconditional transitions |
default |
90 |
Gray | Unlabeled / catch-all transitions (_) |
Example — use cyan for success edges and magenta for error edges:
Set NO_COLOR=1 to disable all colorization regardless of config.
orchestration¶
Settings for the host CLI used by orchestration scripts (ll-auto, ll-parallel, ll-sprint).
| Key | Default | Description |
|---|---|---|
host_cli |
(auto-detected) | Override the host CLI: "claude-code", "codex", "opencode", "pi", "gemini", "omp", "kimi-code", or "qwen". Mirrors the LL_HOST_CLI environment variable; env var takes precedence if both are set. This enum is the orchestration-runner set and is deliberately wider than ll-init --hosts — gemini is valid here but is not a --hosts value (omp is valid in both sets as of FEAT-2261, though --hosts omp is an info-only recognition with no adapter writer). See HOST_COMPATIBILITY.md § Host tiers. |
request_path |
"cli" |
Dispatch mechanism for prompt-mode FSM states: "cli" (default, unchanged — CLI shell subprocess via resolve_host()), "sdk" (opt-in, calls the anthropic SDK's messages.create() directly — required for the F1 prompt-caching discount, since cache_control is unreachable over the CLI path), or "batch" (opt-in, submits via the Message Batches API for a flat 50% discount on input+output tokens, FEAT-2710). Per-state StateConfig.request_path overrides this default the same way State.model overrides run_model. |
disable_background_tasks |
false |
FEAT-3078/FEAT-3060: when true and a Claude Code child is spawned with automation.profile set (ll-auto, FSM loops; both fields live on the AutomationContext passed as build_streaming(automation=...), ENH-3095), injects CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 into its environment, hard-disabling tool-level background tasks — e.g. Bash run_in_background: true or an Agent/Task-tool spawn left to its background-by-default behavior (BUG-3209) — so completed work can never be silently discarded because the parent session ended before a background task's result was retrieved. Defaults to false, so background tasks are allowed unless you opt in with disable_background_tasks: true. Claude-Code-only and inert for the other seven host CLIs; shell-level backgrounding (a trailing &) is outside this flag's reach. Opt-in caveat: a project.run_cmd smoke-test step that itself relies on tool-level backgrounding loses that capability when the flag is enabled. |
Credentials (and any other environment variables) can live in a gitignored
<project_root>/.env file: BRConfig fallback-loads it at construction via
little_loops.env_file.load_env_fallback(), so every CLI entry point picks it
up. Real environment variables always win over .env values (a set-but-empty
env var also counts as present), and the parser supports comments, blank
lines, an optional export prefix, and quoted values — no python-dotenv
dependency.
A configured "sdk"/"batch" value downgrades to "cli" at runtime if the
anthropic package is not importable, no credential is resolvable via the
SDK's auth chain — ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, or the
on-disk OAuth profile from ant auth login (ENH-2737; subscription-only
users need no console API key) — or the state's action invokes a /ll:
skill or declares tools: (BUG-2831): the sdk/batch dispatch path sends a
bare, tool-less single-turn API call, which can't run the host CLI's
agentic tool loop that a skill invocation needs, so it's downgraded
unconditionally — even under an explicit per-state request_path: sdk
override — rather than silently no-opping. The downgrade emits a one-shot
request_path_downgrade event and stderr warning, and the run still
completes normally rather than hard-failing.
"batch" trades latency for cost — results arrive asynchronously via
submit → poll (exponential backoff, capped interval) → retrieve — so it's
only suitable for latency-insensitive states/loops (e.g. ll-auto backlog
processing, verification/adversarial-verify loops, eval harness runs), never
interactive ones. The submitted batch id is persisted under
${context.run_dir}/ via fsm/batch_tracker.py's BatchTracker, so an
interrupted run resumes polling the existing batch instead of
double-submitting. See
ARCHITECTURE.md § SDK/Batches Dispatch Path
for the dispatch mechanism and
API.md § little_loops.host_runner for the
function reference.
orchestration.composer¶
Settings for the loop-composer built-in orchestration loop.
| Key | Default | Description |
|---|---|---|
max_plan_nodes |
8 |
Maximum number of steps allowed in a single loop-composer plan. |
auto |
false |
When true, skip the HITL plan-approval gate and execute the plan immediately. |
orchestration.composer.adaptive¶
Tuning knobs for the loop-composer-adaptive built-in loop (fault-tolerant re-plan-on-failure).
| Key | Default | Description |
|---|---|---|
enabled |
false |
When true, prefer the adaptive composer variant. |
max_replans |
2 |
Maximum re-plan attempts before aborting. |
reassess_min_confidence |
0.6 |
Confidence threshold below which the reassess gate triggers a re-plan. |
orchestration.cluster¶
Settings for the goal-cluster multi-goal orchestration loop.
| Key | Default | Description |
|---|---|---|
max_batch_size |
5 |
Maximum number of issues to process in a single cluster batch. |
enable_dedup |
true |
When true, deduplicate issues with overlapping goals before batching. |
propagate_context |
true |
When true, pass accumulated context from completed issues to subsequent batches. |
mcp¶
ll-mcp server configuration (FEAT-3149).
| Key | Default | Description |
|---|---|---|
transport_policy.http.allow_mutations |
false |
When false, mutating tool calls (issue_capture, issue_set_status, issue_link, issue_append_log) over the HTTP transport are refused with a JSON-RPC error. Deny-by-default because HTTP ships without authentication. |
transport_policy.http.allow_tasks |
false |
When false, tasks/* requests (poll/cancel an ll-loop run) over HTTP are refused. Independent of allow_mutations (FEAT-3145 Decision 6). |
transport_policy.stdio.allow_mutations |
true |
stdio is a same-machine, same-user channel, so mutations default open. |
transport_policy.stdio.allow_tasks |
true |
stdio is a same-machine, same-user channel, so tasks/* defaults open. |
http.host |
(loopback) | Interface to bind for the streamable HTTP transport (ENH-3173). A non-loopback value also widens TransportSecuritySettings' allowed_hosts/allowed_origins to include it, since the SDK only auto-fills that allow-list for a loopback bind. |
http.port |
8765 |
Port to bind for the streamable HTTP transport. A --host/--port flag on the ll-mcp console script takes precedence over both of these values. |
resources.issue_statuses |
(all statuses) | When set, only issues whose frontmatter status is in this list are enumerated as ll://issues/<ID> resources (ENH-3174). |
resources.docs_globs |
(all docs) | When set, only docs/** files matching one of these glob patterns are enumerated as ll://docs/<path> resources. |
resources.page_size |
500 |
Maximum resources returned per resources/list response. A client that sends the returned nextCursor back retrieves the next page. All resources.* fields default to the pre-ENH-3174 behavior (unbounded, single page) — narrowing is opt-in. |
{
"mcp": {
"transport_policy": {
"http": { "allow_mutations": false, "allow_tasks": false }
},
"http": { "host": "127.0.0.1", "port": 8765 },
"resources": {
"issue_statuses": ["open", "in_progress"],
"docs_globs": ["reference/*.md"],
"page_size": 200
}
}
}
advisor¶
Advisor (FEAT-3037) settings: host, model, capability floor, per-consult
timeout, and the per-task consult budget. The advisor block is absent from
the config by default, which means the advisor is disabled for
auto-consults; ll-advise's manual path still works (it bypasses enabled
and triggers, but is still budget-counted — see FEAT-3116).
| Key | Default | Description |
|---|---|---|
enabled |
false |
Enable auto-consults (consult_for_trigger without manual=True). Absent or false means auto-consults are disabled; ll-advise still works. |
host |
null |
Registry key for the host CLI the advisor consults: "claude-code", "codex", "opencode", "pi", "gemini", "omp", "kimi-code", or "qwen" — the same enum as orchestration.host_cli, but may differ from it. |
model |
"opus" |
Model the advisor requests from the selected host. |
min_tier |
null |
Capability floor for the advisor's model; enforced within a host, warned across hosts. |
timeout_seconds |
180 |
Per-consult timeout in seconds. Mandatory-with-a-default — a synchronous in-band consult with no timeout can hang a loop indefinitely. |
triggers |
[] |
Keywords identifying when the advisor is consulted, e.g. confidence_gate, loop_stall, pre_done. Ignored for manual (ll-advise) consults. |
max_consults_per_task |
3 |
Per-task cap on advisor consults, enforced by should_consult()/consult_for_trigger(). A task is identified by resolve_task_key() (issue ID, loop run ID, or session ID) — applies to both auto and manual consults. |
store_verdict_body |
false |
Opt-in to persisting the consult verdict body in the advisor_consults telemetry row (FEAT-3300). Absent or false means the verdict body is never written to .ll/history.db, since it can quote private code. |
hooks¶
Settings for hook adapter selection.
| Key | Default | Description |
|---|---|---|
host |
(auto-detected) | Host agent identifier for hook adapters: "claude-code", "opencode", or "codex". Adapters translate between the host's native hook protocol and LLHookEvent/LLHookResult. |
stale_ref_fix |
"report" |
Session-end stale-ref sweep mode: "report" prints findings to stderr; "auto" also rewrites them in-place. |
doc_drift_throttle_days |
7 |
Minimum days between session-start doc-drift checks (ENH-2888), tracked via a per-project timestamp state file. Set LL_DOC_DRIFT_DISABLE (any non-empty value) to opt out entirely. |
hooks.pre_compact.rubric¶
Rubric-gated compaction timing (ENH-2341). When enabled, the precompact.sh hook evaluates four structural conditions over the recent transcript before writing state. All conditions must pass; any failure causes the hook to return exit 0 without writing state (compaction still fires but without a continuation snapshot). Disabled by default.
A successful state write (rubric-gated or not) also emits a best-effort compaction row into .ll/history.db's session_lifecycle_events table (ENH-2495) — queryable via ll-session recent --kind session_lifecycle.
| Key | Default | Description |
|---|---|---|
hooks.pre_compact.rubric.enabled |
false |
Enable rubric-gated compaction timing. When false, falls back to original threshold-only behaviour. |
hooks.pre_compact.rubric.hard_ceiling_pct |
0.95 |
Reserved: context fill fraction above which state is always written. Not yet enforced (token count not exposed in PreCompact payload). |
hooks.pre_compact.rubric.signals.closed_unit_signals |
["\bdone\b", "\bcompleted\b", "\bfixed\b", "\bresolved\b"] |
Patterns indicating a reasoning unit is closed. |
hooks.pre_compact.rubric.signals.reducible_signals |
["\bin summary\b", "\bto summarize\b", "\boverall\b"] |
Patterns indicating content is summarisable. |
hooks.pre_compact.rubric.signals.progress_signals |
["\bchanged\b", "\bupdated\b", "\bmodified\b", "\bimplemented\b"] |
Patterns indicating progress since last compaction. |
hooks.pre_compact.rubric.signals.stuck_signals |
["\bsame error\b", "\bstill failing\b", "\brepeat\b"] |
Patterns indicating a stuck loop. Any match causes rubric to fail. |
{
"hooks": {
"doc_drift_throttle_days": 7,
"pre_compact": {
"rubric": {
"enabled": true,
"hard_ceiling_pct": 0.95,
"signals": {
"closed_unit_signals": ["\\bdone\\b", "\\bcompleted\\b"],
"reducible_signals": ["\\bin summary\\b"],
"progress_signals": ["\\bchanged\\b", "\\bupdated\\b"],
"stuck_signals": ["\\bsame error\\b", "\\bstill failing\\b"]
}
}
}
}
}
extensions¶
List of extension module paths to load at startup. Each entry is a "module.path:ClassName" string. Extensions implement the LLExtension protocol and receive structured LLEvent notifications from the EventBus during ll-loop, ll-parallel, and ll-sprint runs.
| Key | Type | Default | Description |
|---|---|---|---|
extensions |
array of string |
[] |
Extension module paths. Format: "module.path:ClassName". |
Authoring an extension:
# my_package/ext.py
from little_loops.events import LLEvent
class MyExtension:
# Optional: subscribe only to matching event types (fnmatch glob).
# Omit or set to None to receive all events.
event_filter = "issue.*"
def on_event(self, event: LLEvent) -> None:
print(f"{event.type} — {event.payload}")
event_filter accepts a single glob string (e.g. "issue.*") or a list of globs (e.g. ["issue.*", "parallel.*"]). The filter is matched against the event's type field using Python's fnmatch. Omit event_filter or set it to None to receive every event.
Auto-discovery via entry points:
To have your extension loaded automatically without listing it in ll-config.json, register it under the little_loops.extensions entry-point group in your package's pyproject.toml:
After installing the package, ll will discover and load it on every run alongside any config-listed extensions.
The same little_loops.extensions entry-point group also dispatches LLHookIntentExtension providers — extensions that contribute hook intent handlers via provided_hook_intents(). A single package can implement both LLExtension (event observers) and LLHookIntentExtension (request/response hook handlers); wire_extensions() duck-types each interface independently. This single shared group is the resolved design from FEAT-1116 Decision 2 (FEAT-1117 group-split is deferred). See API Reference → LLHookIntentExtension for the Protocol shape.
Extensions can also be auto-discovered via Python entry points — see API Reference → little_loops.extension.
Tip: Use
ll-create-extensionto scaffold a new extension repo with a ready-to-run entry point, skeleton handler, and example test. UseLLTestBusto replay recorded events against your extension offline without starting a live loop.
hitl¶
Human-in-the-loop communication channel selection (FEAT-1930) and wait bound (FEAT-1794). hitl.channel selects the active CommunicationAdapter, resolved by FSMExecutor.resolve_communication_adapter() from adapters registered via CommunicationAdapterExtension (see API Reference → CommunicationAdapterExtension). Not to be confused with the unrelated hitl-md/hitl-compare built-in loop family — those are loop names, not a config namespace.
The default channel, "terminal", is built in (FEAT-1931) and needs no extension or config entry: it prints a formatted prompt to stdout and blocks on stdin for the operator's approve/reject/edit verdict. Any other channel value must be contributed by an extension's CommunicationAdapterExtension.provided_adapters().
"eventbus" (FEAT-3384) is the built-in async channel for unattended runs: it is registered by EventBusAdapterExtension, always loaded via BUILTIN_EXTENSIONS — no extensions: config entry or reinstall is needed to select it. It sends nothing on send_alert() (the executor emits human_approval_requested) and resolves await_response() from a matching human_response event observed on the EventBus. An out-of-process verdict requires the run to have an inbound path: start it under ll-loop run --serve and POST /{token}/interaction with a body of {"event": "human_response", "alert_id": "...", "verdict": "approve" | "reject" | "edit", "edited_text": "...", "reason": "..."} — only alert_id/verdict/edited_text/reason are forwarded onto the bus. Without --serve, only an in-process emitter (tests, embedded callers) can answer, and the executor logs one warning per alert.
Two known limitations, tracked as follow-up issues rather than fixed here: ll-loop resume builds its executor with no inbound queue and no --serve bridge, so a run interrupted mid-wait and resumed under eventbus can only time out; and the --serve dashboard has no HITL UI — its only interaction control posts an artifact_interaction note, so out-of-process verdicts come from curl or a relay, not the browser page.
| Key | Type | Default | Description |
|---|---|---|---|
hitl.channel |
string |
"terminal" |
Registry key for the active communication adapter. Unset resolves to the built-in "terminal" adapter. "eventbus" is also built in (FEAT-3384). A non-"terminal"/"eventbus" value with no matching extension raises CommunicationAdapterNotFound at resolve time. |
hitl.default_timeout |
integer (seconds) |
1800 |
Fallback wait for an action_type: human_approval state (see FSM Loop Reference § action_type) with no state-level timeout:. Distinct from the loop-level timeout/default_timeout, which bounds action subprocesses, not human waits. ll-loop validate warns (not errors) when a human_approval state omits timeout:, naming this fallback. |
events.transports¶
List of transports to wire onto the EventBus at runtime. Transports are additive sinks that receive every event emitted on the bus (no filtering at the transport layer). Names are resolved against the registry in little_loops.transport.wire_transports; unknown names log a warning and are skipped so a typo never prevents the loop from starting.
| Key | Type | Default | Description |
|---|---|---|---|
events.transports |
array of string |
[] |
Transport names to register on the EventBus. |
Currently shipped transports:
| Name | Effect |
|---|---|
"jsonl" |
Registers a JsonlTransport writing to <log_dir>/events.jsonl (defaults to .ll/events.jsonl). |
"socket" |
Registers a UnixSocketTransport streaming newline-delimited JSON events over an AF_UNIX socket. Configured under events.socket (see below). Not available on Windows — wire_transports raises RuntimeError. |
"otel" |
Registers an OTelTransport that maps loop executions to OpenTelemetry traces/spans and exports via OTLP. Configured under events.otel (see below). Requires pip install 'little-loops[otel]'. |
"webhook" |
Registers a WebhookTransport that batches events and POSTs them as JSON arrays to an HTTP endpoint. Configured under events.webhook (see below). Requires pip install 'little-loops[webhooks]'. |
"sqlite" |
Registers a SQLiteTransport that records events into the per-project .ll/history.db unified session store. Configured under events.sqlite (see below). Queryable via the ll-session CLI. |
{
"events": {
"transports": ["jsonl", "socket"],
"socket": {
"path": ".ll/events.sock",
"max_clients": 32
}
}
}
events.socket¶
| Key | Type | Default | Description |
|---|---|---|---|
events.socket.path |
string |
".ll/events.sock" |
Preferred filesystem path for the AF_UNIX socket. This is the path a single producer binds; a concurrent second producer binds a sibling path instead (see "Multiple concurrent producers" below). The transport reclaims a genuinely stale file (crashed producer) before binding, and removes the file it bound on close(). |
events.socket.max_clients |
integer |
32 |
Maximum simultaneous clients. Connections beyond the cap are accepted-and-closed. |
The socket file is chmod 0600 immediately after bind() — owner-only, since the events stream may include issue titles, file paths, and branch names. Operators wanting wider access must relax permissions out-of-band.
Per-client buffering and slow-consumer behaviour: Each client gets a bounded outbound queue (1024 events). When a client cannot keep up, the newest event is dropped (preserving causal order) and a rate-limited warning is logged — send() never blocks the FSM thread.
ll-auto exclusion: cli/auto.py does not construct an EventBus, so listing "socket" (or any transport) under events.transports has no effect under ll-auto. The socket transport is available under ll-loop run/resume, ll-parallel, and ll-sprint parallel-wave runs.
Subscribing locally: Any AF_UNIX-aware tool can subscribe — for ad-hoc inspection, pipe nc -U .ll/events.sock | jq.
Multiple concurrent producers (BUG-3324): Two little-loops processes can have events.transports: ["socket"] configured at once (e.g. an ll-loop run and an ll-sprint run overlapping). The second producer probes the configured path before binding; if a live listener already owns it, the second producer binds a sibling path instead of evicting the first: {stem}-{pid}{suffix} next to the configured path — for example .ll/events-1234.sock alongside the default .ll/events.sock, where 1234 is the second producer's pid. This is a naming contract, not an implementation detail — a consumer that wants to see every live producer must enumerate the directory for files matching this shape rather than connecting to the configured path alone. Claiming a sibling path is logged at INFO, naming both paths.
Orphaned events-<pid>.sock files left behind by a producer that crashes without running close() are not swept by this or any other mechanism — they accumulate in .ll/ across crashes. A directory-enumerating consumer must therefore tolerate dead endpoints: connecting to a sibling file and getting ECONNREFUSED or finding it already gone is an expected, not exceptional, outcome.
events.otel¶
Requires: pip install 'little-loops[otel]' (installs opentelemetry-sdk and opentelemetry-exporter-otlp-grpc).
| Key | Type | Default | Description |
|---|---|---|---|
events.otel.endpoint |
string |
"http://localhost:4317" |
OTLP gRPC endpoint for the collector (Grafana Agent, Jaeger, Datadog, etc.). |
events.otel.service_name |
string |
"little-loops" |
OpenTelemetry service.name resource attribute applied to all emitted spans. |
Span hierarchy: Each loop run becomes an OTel trace. Loop = root span, state = child span, action = grandchild span. Events such as evaluate, route, retry_exhausted, cycle_detected, stall_detected, handoff_detected, handoff_spawned, and action_output are recorded as span events on the innermost open span.
Sub-loop behaviour: Sub-loop events (depth > 0) are no-ops with a single warning per session. Full nested-trace support is deferred.
{
"events": {
"transports": ["otel"],
"otel": {
"endpoint": "http://localhost:4317",
"service_name": "little-loops"
}
}
}
events.webhook¶
Requires: pip install 'little-loops[webhooks]' (installs httpx).
| Key | Type | Default | Description |
|---|---|---|---|
events.webhook.url |
string \| null |
null |
HTTP endpoint to POST batched events to. When null, the transport is skipped even if "webhook" is listed in transports. |
events.webhook.batch_ms |
integer |
1000 |
Flush interval in milliseconds. Events accumulate and are POSTed as a JSON array on each tick. |
events.webhook.headers |
object |
{} |
Additional HTTP headers sent with every POST (e.g. {"Authorization": "Bearer token"}). User-supplied keys override defaults; Content-Type defaults to application/json and can be overridden. |
Batching: Events are enqueued non-blocking in send() and flushed by a daemon thread. All events queued during a batch_ms window are included in one POST body as a JSON array.
Retry behaviour: Failed POSTs (5xx responses or connection errors) are retried up to 3 times with exponential backoff (0.5s → 1s → 2s, capped at 8s; 4 total attempts). After retries are exhausted the batch is dropped with a warning — exceptions never propagate to the caller.
Shutdown: close() signals the daemon thread to stop, performs one final flush of any queued events, then joins the thread with a 10s timeout.
{
"events": {
"transports": ["jsonl", "webhook"],
"webhook": {
"url": "https://hooks.example.com/ll-events",
"batch_ms": 1000,
"headers": { "Authorization": "Bearer <token>" }
}
}
}
events.sqlite¶
Records FSM loop events into the per-project session store (.ll/history.db) for indexed cross-cutting queries via the ll-session CLI.
| Key | Type | Default | Description |
|---|---|---|---|
events.sqlite.path |
string |
".ll/history.db" |
Filesystem path for the SQLite session database. |
Env-var override: LL_HISTORY_DB takes precedence if set (e.g. for test isolation). Separately, LL_ANALYTICS_CAPTURE=0 (kill switch, ENH-3449) suppresses the per-invocation cli_events/skill_events analytics rows before the db is ever resolved — no file is created, and LL_HISTORY_DB is not consulted.
The session store is a SQLite database with an FTS5 full-text index. SQLiteTransport writes events as they are emitted; ll-session search/recent/backfill query and seed it. As of ENH-1691, ll-auto writes issue lifecycle events live via AutoManager's internal transport — no additional config is required. As of ENH-2783, ll-parallel and ll-sprint do the same: their CLI entry points (cli/parallel.py, cli/sprint/run.py) attach a SQLiteTransport to the orchestrator's EventBus unconditionally (skipping the attach only if events.transports already lists "sqlite", to avoid a duplicate write), so issue-close events from worker completion, sequential merge, and the frontmatter-only lifecycle-completion path are all live-written regardless of events.transports config. Use ll-session backfill to import historical data captured before ENH-1691. As of ENH-1830, session_start automatically triggers an incremental backfill in a background thread for each interactive session, so new data is indexed without manual intervention.
events.bridge¶
FEAT-3323. Configuration for ll-artifact serve — a localhost HTTP/SSE
bridge that fans in every UnixSocketTransport producer socket in the
project and relays bus events to a browser in real time. Unlike
events.socket/events.otel/events.webhook/events.sqlite, this block
gates a server, not a transport: there is no _TRANSPORT_REGISTRY entry
and no wire_transports branch for "bridge", and listing it under
events.transports has no effect. The user opts in by running
ll-artifact serve directly.
| Key | Type | Default | Description |
|---|---|---|---|
events.bridge.port |
integer |
8766 |
TCP port on 127.0.0.1 to bind. Fixed (not 0/ephemeral) so the printed URL is stable across restarts, adjacent to ll-mcp's 8765; 0 is the test path. --port overrides. |
events.bridge.max_clients |
integer |
8 |
Maximum concurrent SSE client connections. Each holds a thread for the connection's life; a connection over the cap gets 503. |
events.bridge.keepalive_s |
number |
15 |
Interval, in seconds, between SSE keepalive comment frames (: ping) on a quiet bus. Also sets the per-connection write timeout (2 × keepalive_s) that reclaims a handler thread pinned by a peer that is alive but not reading. |
events.bridge.rescan_s |
number |
2 |
Interval, in seconds, at which the bridge rescans the producer socket directory for new/dead sockets. Also the base of the per-path connect-then-immediate-EOF backoff (doubles per consecutive flap, capped at 60s). |
events.bridge.history |
boolean |
false |
FEAT-3321. Gates the read-only GET /{token}/history route plus the dashboard.llat page served in place of the FEAT-3323 placeholder. Off by default; the route follows artifacts.export.mode — no separate --local flag. |
There is no enabled key and no host key. A config flag that refuses
an explicit command (enabled) is bad UX with no precedent (ll-loop run
--serve and ll-mcp have none); the server binds 127.0.0.1
unconditionally (host), matching LocalBridgeTransport.
{
"events": {
"bridge": {
"port": 8766,
"max_clients": 8,
"keepalive_s": 15,
"rescan_s": 2,
"history": false
}
}
}
No redaction on the live path. ENH-075's column allowlist is applied at
export time in cli/artifact/dashboard.py; there is no equivalent
redaction on the bus, and ll-artifact serve does not add one. This is safe
because the stream is the user's own project data, the listener is
loopback-only, the Host header guard blocks DNS rebinding, a per-start
unguessable token prefix blocks blind requests, no
Access-Control-Allow-Origin header is sent, and the capability is opt-in
twice: events.transports is [] by default and the user must explicitly
start ll-artifact serve. Revisit trigger: any change that makes the
endpoint reachable off-host, or any multi-user access, must re-open this
decision.
No-replay reconnect contract. The bridge sends retry: 2000 (2s) once at
stream open and never emits an SSE id: line — there is no durable buffer to
replay from, and pretending otherwise would silently lie about completeness.
A reconnect re-seeds (below) and resumes live; events emitted during the gap
between disconnect and reconnect are lost by design.
Seed-then-live may duplicate, never lose. On every SSE connect (including
reconnects), the bridge registers the client's queue in the fan-out set
first, then snapshots list_running_loops(Path(".loops")) and writes one
state_change frame per state directly to the wire, and only then starts
draining the queue. A live event emitted while the seed snapshot is being
taken is therefore already waiting in the queue rather than dropped — the
cost is a possible duplicate, never a loss: a state_enter queued during
the snapshot may describe a transition the seed frame already reflects, so a
client can see the seed at state B and then a live state_enter for B.
Consumers must treat seed frames as idempotent snapshots, not a bug report.
Seed scope: FSM loops only. ll-sprint run and ll-parallel producers
write no state files, so a mid-run SSE connect gets no seed for them —
matching today's socket seed. list_running_loops also runs
_reconcile_stale_running on its read path (BUG-3232), which can rewrite a
stale running state file to interrupted; the bridge process therefore
writes into .loops/.running/ on every SSE connect, the same side effect
every producer's socket-connect seed already has.
See API Reference → little_loops.transport for the Transport Protocol and how to author custom transports.
compression¶
Top-level block (FEAT-2675, EPIC-2456 Tier 3) governing the in-house, zero-dependency
heuristic prompt compressor hooked into FSMExecutor._run_action(). It runs only
for prompt-mode actions whose token estimate crosses a window-relative trigger, so
default behavior is unchanged for prompts under the trigger. No ML/pip dependency — the
LLMLingua-gated benchmark comparator that decides whether the heuristic underperforms
is tracked separately under FEAT-2676.
The compressor applies three extractive passes: drop tool-result messages older than
max_tool_result_age_turns user turns, dedupe exact-duplicate stable system blocks
(surviving repeated blocks are flagged as cache_control candidates for a future F1
child — no marking happens here), and tail-truncate to the most recent
max_assistant_tail_turns assistant messages. Token estimates use the project-wide
len(text) // 4 convention.
The effective trigger resolves relative to the active model's context window when
known (trigger_pct * context_window) and falls back to the absolute trigger_tokens
otherwise; when both apply the lower absolute value wins (compress sooner). At the
executor, only actions that parse as a JSON message list (the motivating case — loops
re-embedding captured message-list JSON) are compressed; arbitrary prose prompts pass
through byte-identical. Compression runs after the ENH-2486 prompt_size_guard
measurement (the guard keeps reporting the original assembled size) and before the
prompt is sent.
heuristic_underperforms is the gate FEAT-2676 flips after its offline LLMLingua
benchmark; while true the heuristic is bypassed. Default false runs it.
| Key | Type | Default | Description |
|---|---|---|---|
compression.heuristic_underperforms |
boolean |
false |
Bypass gate flipped by FEAT-2676. true disables the heuristic. |
compression.trigger_pct |
number |
0.4 |
Compress once the prompt exceeds this fraction of the model's context window. |
compression.trigger_tokens |
integer\|null |
null |
Optional absolute token trigger; lower of this and trigger_pct wins when both apply. |
compression.max_tool_result_age_turns |
integer |
5 |
Drop tool-result messages older than this many user turns. |
compression.max_assistant_tail_turns |
integer |
8 |
Keep only the most recent N assistant messages. |
{
"compression": {
"heuristic_underperforms": false,
"trigger_pct": 0.4,
"trigger_tokens": null,
"max_tool_result_age_turns": 5,
"max_assistant_tail_turns": 8
}
}
deferred_tools¶
Top-level block (FEAT-2672, EPIC-2456 F1) governing per-tool defer_loading and
the matching server-side search-tool injection. Only consulted when
orchestration.request_path is "sdk" or "batch" (FEAT-2710) — the CLI shell
path never serializes a tool-definition catalog at all, so this block has no
effect there. This same opt-in condition gates EPIC-2456's F1/F10 gates, which
docs/observability/realized-savings-verification.md (ENH-2719) found
structurally dormant under the "cli" default. BUG-2831 narrows the
"sdk"/"batch" states that actually reach this dispatch: a state whose
action invokes a /ll: skill is force-downgraded to "cli" at runtime
(since _dispatch_live never passes tool definitions at all), so
deferred_tools in practice only ever applies to pure evaluator prompt
states, not skill-invoking ones.
threshold sets the catalog index at or past which
tool_catalog.to_anthropic_tools() flags each tool defer_loading: True,
withholding its full definition from the assembled system prompt unless the
model searches for it. null (default) flags nothing — unchanged behavior.
Whenever any tool ends up flagged, host_runner.build_anthropic_request()
prepends exactly one search-tool entry to the request's tools array — without
it, defer_loading: True has no effect server-side. search_tool_variant
picks which one: "bm25" (default, general-purpose relevance) or "regex".
| Key | Type | Default | Description |
|---|---|---|---|
deferred_tools.threshold |
integer\|null |
null |
Catalog index at or past which tools get defer_loading: True. null disables deferred loading entirely. |
deferred_tools.search_tool_variant |
string |
"bm25" |
Server-side search-tool type to inject when any tool is deferred: "bm25" or "regex". |
cache¶
Top-level block (FEAT-2673, EPIC-2456 F1) governing the cache-marking oracle
(cache_marking_oracle.decide_cache_marking()), which decides whether a
stable prompt block (system / tool / stable-skill) is safe to mark with
cache_control: {"type": "ephemeral", ...}. Consulted only when
_dispatch_live reaches host_runner.dispatch_anthropic_request() /
dispatch_batch_request() — i.e. for prompt-mode states whose resolved
request_path (FEAT-2710) is "sdk" or "batch" (fsm/executor.py); the
CLI shell path never sees a cache_control parameter regardless of this
config.
require_repeat mirrors the oracle's own conservative default: a block is
marked only once its FEAT-2671 fragment key has already been observed as a
repeat, avoiding the unamortized 1.25x write premium on a block that's never
reused. Setting it to false disables this reuse gate, marking any block
that clears the per-model cacheable-prefix minimum (1024 tokens for Sonnet,
4096 for Opus) on first sight — appropriate only for callers with a
stronger external stability signal than fragment-repeat observation.
| Key | Type | Default | Description |
|---|---|---|---|
cache.require_repeat |
boolean |
true |
Whether a block must have been observed as a repeat fragment before it is marked cacheable. false disables the reuse gate and marks on first sight. |
observability¶
Top-level block (FEAT-2478) governing OTel gen_ai.* attribute shaping and the
streaming-parity check. These are an always-on, capture-time behavior that
writes OTel-shaped rows directly to history.db / usage.jsonl with no OTel
SDK or collector — independent of the opt-in OTLP transport under events.otel
(which requires pip install 'little-loops[otel]'). See
docs/observability/otel-mapping.md.
| Key | Type | Default | Description |
|---|---|---|---|
observability.otel_attributes.enabled |
boolean |
true |
Stamp canonical gen_ai.usage.* attributes onto captured usage rows (usage.jsonl). |
observability.streaming_parity.check |
boolean |
true |
Gate the 0.1% streaming-vs-blocking cache-token parity threshold (ENH-2479). |
{
"observability": {
"otel_attributes": { "enabled": true },
"streaming_parity": { "check": true }
}
}
Manual Configuration¶
The following fields are defined in config-schema.json but are not exposed through ll-init or /ll:configure. To set them, edit .ll/ll-config.json directly. All have sensible defaults and rarely need changing.
Re-init preserves these. Re-running
ll-init(without--force) deep-merges the regenerated config over your existing one, so any manually-set values here — and any other keysll-initdoes not model — survive. Pass--forceto reset to template defaults and drop them.
scan.custom_agents¶
Custom scanning agent names to include during /ll:scan-codebase:
Default: [] (empty — only built-in agents run).
context_monitor.estimate_weights¶
Weight factors for the context monitoring token estimation heuristic. Adjust if the context monitor's estimates are consistently too high or too low:
{
"context_monitor": {
"estimate_weights": {
"read_per_line": 10,
"tool_call_base": 100,
"bash_output_per_char": 0.3,
"per_turn_overhead": 800,
"system_prompt_baseline": 10000
}
}
}
| Sub-field | Default | Description |
|---|---|---|
read_per_line |
10 |
Estimated tokens per line read |
tool_call_base |
100 |
Base tokens per tool call overhead |
bash_output_per_char |
0.3 |
Estimated tokens per character of bash output |
per_turn_overhead |
800 |
Tokens per turn for Claude output and user message |
system_prompt_baseline |
10000 |
One-time token estimate for system prompt |
context_monitor.post_compaction_percent¶
After context compaction, reset the token estimate to this percentage of context_limit_estimate as a safety margin:
Default: 30 (range: 10-60).
product.analyze_user_impact / product.analyze_business_value¶
Toggle sub-features of product analysis. Both default to true when product.enabled is true:
product.goals_discovery¶
Fine-tune how product goal auto-discovery scans documentation:
{
"product": {
"goals_discovery": {
"max_files": 10,
"required_files": ["README.md", "docs/VISION.md"]
}
}
}
| Sub-field | Default | Description |
|---|---|---|
max_files |
5 |
Maximum markdown files to analyze (1-20) |
required_files |
["README.md"] |
Files that must exist (warning if missing) |
Behavioral note: These settings are active when
ll-goals.mdis absent —max_fileslimits how many files are read during discovery;required_filesentries trigger a warning if missing but never block analysis.
prompt_optimization.bypass_prefix¶
Character prefix that bypasses prompt optimization. Messages starting with this prefix are sent as-is:
Default: *.
Variable Substitution¶
Commands use {{config.*}} for configuration values:
# In command templates
{{config.project.src_dir}} # -> "src/"
{{config.project.test_cmd}} # -> "pytest"
{{config.issues.base_dir}} # -> ".issues"
Command Override¶
Projects can override plugin commands by placing files in .claude/commands/ll/.
Override priority:
1. Project .claude/commands/ll/*.md (highest)
2. Plugin commands/*.md
3. Default behavior
Example Override¶
To add project-specific verification to manage-issue: