Repository navigation
feat(config): named profiles in ~/.lich/profiles - #172
Conversation
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Ingest decision-model and Ollama research, add the decision-models concept page, note ollama.com cloud auth and stale defaults on the providers page, and link the fast lane from action-terminal mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Ingest a read of Hermes upstream at bed0d535 and record how it mixes models (fallback chain plus per-task auxiliary models), where embeddings live (memory plugins), and how its 14 community Jev plugins work. Add comparison rows and point decision models and plugin hooks at the revised #148/#149 scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
llama3.2 (3B) is weak at tool calling; qwen3:8b is the common local pick for agents. Switch the README, docs, persona example and the setup wizard default to qwen3:8b, keeping llama3.2 as the low-memory note. The Ollama provider already sends a Bearer header when api_key or api_key_env resolves, so Ollama cloud works today. Document it (LICH_BASE_URL=https://ollama.com, LICH_API_KEY_ENV=OLLAMA_API_KEY) and replace "unused by ollama" with "optional for ollama". Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…press Optional `models` block names provider chains per role. `chat` sets the main loop's failover order; `compress` routes context compression and falls back to the chat chain on failure. ProviderRouter.for_role shares built clients. Without `models`, behavior is unchanged. The TUI labels the first chat-role provider. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
… fallback Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…hook
Plugin entries may be { path, settings?, models? } beside a bare path.
Hooks and plugin tools receive the frozen settings and models.chat(role,
...), which refuses roles the entry was not granted. A new
before_llm_call hook may return a note that is capped, sent as a
trailing system message on that one main-loop call, and never saved to
history; throwing hooks fail open.
Part of #149.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…, deep-freeze loader settings Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…gin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
A plugin-owned client for Ollama's local /v1/systemone endpoint asks one choice question per enemy action (and target) before each LLM turn. Shadow mode logs one JSONL line per decision; act mode queues orders through game_bridge's enemy_actions tool and meteor veto when every answer clears the threshold, else the LLM decides. Requests are checked against Ollama's limits before sending; failures fall back and are logged. bench.mjs replays saved snapshots for latency, coverage and agreement with the LLM. Part of #148. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…old, keep log under .lich/game Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…ed; no tools on the last turn - A tools_enabled list (top-level or gateway) now applies to plugin tools, including the gatekeeper's git_commit. The commander persona lists its three game_bridge tools explicitly. - A before_tool_call hook that throws blocks the call (blocked_by_plugin: hook_error) instead of allowing it. - Tool calls on the max_turns turn are not executed; they are closed with a turn_budget_exhausted result so history stays valid. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…pts persist them Also document the last-turn skip in docs/architecture/agent-loop.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…st-turn tool skip) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
- CLI one-shot and chat pass an abort signal to agent.run. Ctrl+C aborts the one-shot run (exit 1) or the running chat turn (session kept); at the chat prompt it ends chat. A second Ctrl+C while cancelling exits 130. - TUI: Esc cancels a running turn; an aborted run shows "run cancelled". - write_lich_config update writes a temp file and renames it into place, keeping the existing mode, so readers never see a partial config. - gatekeeper, tools, run_tests, skills_search and kanban_audit tests remove the dirs they create under test/.tmp. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…old reply On abort the loop's `final` is the last assistant so far (often the previous turn's reply) and `messages` ends with the cancelled user line. Chat and the TUI now show only the cancel notice and keep history_after_abort(messages), which drops trailing user lines. The config temp file is created with the existing mode. Tests cover a second Ctrl+C (exit 130) and the chat turn after a cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
reply_after_abort(outcome) returns `final` only when it comes after the last user line, i.e. this run wrote it. Chat prints it and the TUI shows it before the cancel notice; an earlier turn's reply is still skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Roadmap map lists #133 (P0), #134 (P1), #117 (P2), #144, #148 and #149 with their wiki pages; the kanban skill names the phase children. Supersedes the stale #135. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…fig (#117) Phase 0 + 1 of #117. Discovery is now project .lich/config.json merged over ~/.lich/config.json (shallow, project wins per key). A project providers array replaces the global one and drops the global models unless the project sets its own. The global layer ignores work_dir and session_dir, and its relative plugin paths resolve against ~/.lich/. ~/.config/lich/config.json is read only when ~/.lich/config.json is absent, with a hint to move it; nothing writes there. --config still replaces the whole chain. Bare lich no longer pins an existing config as --config when it skips the wizard, so the merge applies there too. first_run tests use an empty HOME instead of mocking the home lookup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…n is absent With work_dir = home the global file is the project file; the legacy ~/.config/lich file was then picked as the base under it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…117) lich init --global writes the starter config to ~/.lich/config.json (never overwrites; no work_dir/session_dir). The setup wizard ends with "Save as the global default?" (default no). Yes writes the answers to ~/.lich/config.json; discovered .lich/plugins entries stay in the project file since they are project paths. The wizard no longer pins its written file as --config, so discovery merges project over global. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…s home With work_dir = home the project file is ~/.lich/config.json, so the plugins-only project write hit "already exists" and dropped plugins. Test also asserts the merged TUI plugins. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…json from home Other projects load that file as the global layer and resolve relative plugin paths against ~/.lich, so .lich/plugins/x would become ~/.lich/.lich/plugins/x. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Phase 2 of #117. ~/.lich/profiles/<name>.json, plus an optional <name>.md used as the system prompt, merges between the global and the project config (global < profile < project). Selection: --profile, then LICH_PROFILE, then a project `profile` key, then the default in ~/.lich/config.json. Profile plugin paths resolve against ~/.lich/profiles; work_dir/session_dir in a profile are ignored. New `lich profile list|show|create|use`: create runs the wizard into the profile file (never overwrites), use sets `profile` in the global file. A requested profile skips the first-run wizard; --profile with --config is an error. File tools now refuse .lich/profiles/. tui/chat/serve/gateway no longer replace every config error with "no model configured"; only that case gets the hint. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
There was a problem hiding this comment.
Stale comment
PR #172 — Risk: HIGH (new public CLI:
lich profile,--profile,LICH_PROFILE; trust-boundary change inassert_file_tool_access; 463 additions / 11 files, not a docs-only patch)
Findings: Critical 0 / Warning 2 / Suggestion 2 (src/cli_config.ts:152merge profile over the home config;src/cli_config.ts:110andsrc/cli_profile.ts:81drop absolute paths;src/cli.ts:422rejectLICH_PROFILEwith--config;src/tools/guard.ts:82list_dir/disk_usageskip the new deny)
Regressions of settled findings: none
Not reviewed: CI conclusions (typecheck_and_test20/22 andossuarywere still running). Static review of the 11-file diff only; tests were not run.
Action: escalated to @Moikapy. Auto-merge left off.needs-reviewadded. Not merged.
Log: 2026-10-05T02:30:34Z, escalated, HIGH public CLI plus file-tool guard, two warnings (home-dir profile merge, absolute paths in new errors). Also a merge blocker: commits on this branch are authored by Claude, not @Moikapy, and this run has no approved-bot list.Sent by Cursor Automation: PR REVIEW
…t_dir/disk_usage - work_dir = home: ~/.lich/config.json is the project file, so the profile now merges over it instead of under it. - "profile not found" / "already exists" errors carry no absolute path. - list_dir and disk_usage check the root with assert_file_tool_access, and list_dir skips denied entries, so .lich/profiles is not listed. - Docs: LICH_PROFILE is ignored with --config. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
There was a problem hiding this comment.
Stale comment
PR #172 — Risk: HIGH (public CLI
lich profile,--profile, andLICH_PROFILE; file-tool deny for.lich/profiles; 510 additions)
Findings: Critical 0 / Warning 2 / Suggestion 0 (src/cli_config.ts:154legacy config dropped whenwork_diris home and~/.lich/config.jsonis absent;src/tools/builtin/disk_usage.ts:71parentdisk_usagestill measures forbidden children)
Regressions of settled findings: none. Home profile-over-global,profile not found/already existswithout paths are fixed.LICH_PROFILEwith--configstays ignored, as decided.
Not reviewed: none
Action: escalated to @Moikapy
Log: 2026-10-05T02:40:00Z, escalated, HIGH after the fix commit; two warnings ineaadb017..ae7e9d84. Auto-merge left off.needs-reviewalready present. Not merged. CI green (typecheck 20/22, ossuary, wiki_lint). Head commit authorclaude. Static review only.Sent by Cursor Automation: PR REVIEW
…children The home-dir branch now applies only when ~/.lich/config.json exists, so a legacy ~/.config/lich file stays the base. disk_usage leaves out entries file tools may not read (.lich/profiles, .lich/config.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
There was a problem hiding this comment.
PR #172 — Risk: HIGH (public CLI lich profile, --profile, and LICH_PROFILE; file-tool deny for .lich/profiles; 536 additions)
Findings: Critical 0 / Warning 0 / Suggestion 0
Regressions of settled findings: none. With work_dir = home, a legacy ~/.config/lich/config.json stays the base when ~/.lich/config.json is absent, and the profile still merges over that file when it exists. disk_usage of .lich skips denied children (profiles, config.json) and leaves them out of TOTAL.
Not reviewed: CI conclusions for typecheck_and_test (20) and (22), still in progress. Static review of ae7e9d84..2d64bbf only; tests were not run.
Action: escalated to @Moikapy
Log: 2026-10-05T02:39:06Z, escalated, HIGH after the fix commit; prior warnings are fixed and not regressed. Auto-merge left off. needs-review already present. Not merged. Head commit author claude (outside the example approved-bot set). Static review only.
Sent by Cursor Automation: PR REVIEW
New entities/lich-config.md: global < profile < project merge, writers, file-tool guard, and config profile vs runtime Profile. Guardrails page and runtime-profile-session updated; index providers line fixed (#165). SCHEMA gains the `config` tag. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
* wiki: log merges of #115 #127 #137 #139 #141 #142 #143 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision models and Ollama model research (#148) Ingest decision-model and Ollama research, add the decision-models concept page, note ollama.com cloud auth and stale defaults on the providers page, and link the fast lane from action-terminal mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: Hermes model roles, memory embedders and Jev plugins (#148, #149) Ingest a read of Hermes upstream at bed0d535 and record how it mixes models (fallback chain plus per-task auxiliary models), where embeddings live (memory plugins), and how its 14 community Jev plugins work. Add comparison rows and point decision models and plugin hooks at the revised #148/#149 scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: default local Ollama to qwen3:8b and document Ollama cloud (#148) llama3.2 (3B) is weak at tool calling; qwen3:8b is the common local pick for agents. Switch the README, docs, persona example and the setup wizard default to qwen3:8b, keeping llama3.2 as the low-memory note. The Ollama provider already sends a Bearer header when api_key or api_key_env resolves, so Ollama cloud works today. Document it (LICH_BASE_URL=https://ollama.com, LICH_API_KEY_ENV=OLLAMA_API_KEY) and replace "unused by ollama" with "optional for ollama". Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: fix Hermes Jev plugin count and update providers after #152 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): per-role provider chains via models.chat and models.compress Optional `models` block names provider chains per role. `chat` sets the main loop's failover order; `compress` routes context compression and falls back to the chat chain on failure. ProviderRouter.for_role shares built clients. Without `models`, behavior is unchanged. The TUI labels the first chat-role provider. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * test, docs: cover models.compress through Agent; clarify omitted-role fallback Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(plugins): entry settings, host model access and before_llm_call hook Plugin entries may be { path, settings?, models? } beside a bare path. Hooks and plugin tools receive the frozen settings and models.chat(role, ...), which refuses roles the entry was not granted. A new before_llm_call hook may return a note that is capped, sent as a trailing system message on that one main-loop call, and never saved to history; throwing hooks fail open. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(plugins): deep-copy hook messages, honor run abort in models.chat, deep-freeze loader settings Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: ingest Ollama System One/Clef check; record model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: index one-liners match model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: bump index updated date Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(examples): decision_lane plugin for game_bridge action selection A plugin-owned client for Ollama's local /v1/systemone endpoint asks one choice question per enemy action (and target) before each LLM turn. Shadow mode logs one JSONL line per decision; act mode queues orders through game_bridge's enemy_actions tool and meteor veto when every answer clears the threshold, else the LLM decides. Requests are checked against Ollama's limits before sending; failures fall back and are logged. bench.mjs replays saved snapshots for latency, coverage and agreement with the LLM. Part of #148. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(decision_lane): zero off-criteria answers, require numeric threshold, keep log under .lich/game Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record the decision_lane prototype (#159) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision_lane egress mentions the Bearer key Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(agent): tools_enabled filters plugin tools; guard hooks fail closed; no tools on the last turn - A tools_enabled list (top-level or gateway) now applies to plugin tools, including the gatekeeper's git_commit. The commander persona lists its three game_bridge tools explicitly. - A before_tool_call hook that throws blocks the call (blocked_by_plugin: hook_error) instead of allowing it. - Tool calls on the max_turns turn are not executed; they are closed with a turn_budget_exhausted result so history stays valid. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(loop): emit tool_call_end for last-turn skipped calls so transcripts persist them Also document the last-turn skip in docs/architecture/agent-loop.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record #161 (plugin-tool allowlist, fail-closed guard hooks, last-turn tool skip) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: move the #161 fixed note out of the open-holes list Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: http_request status, terminal_timeout_ms, .. path guard, OpenAI tool ids, MCP name collisions - http_request returns ok:false (error http_<status>) for non-2xx, keeping status and body in output. - terminal defaults its command timeout to config terminal_timeout_ms. - The path guard rejects only .. and ../ prefixes, so names like ..notes.txt inside work_dir are allowed. - OpenAI-compatible tool calls without an id get a unique generated id. - MCP names that collide after sanitizing log a warning; first wins. - Backoff comment and provider docs no longer claim the deterministic jitter desynchronizes simultaneous callers. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(openai): treat a null tool-call id as missing; correct http_request note Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: webhook binds loopback by default; drop stale version from overview; wiki #163 - godot.md and the persona orchestrator README said the CLI webhook binds 0.0.0.0; it binds 127.0.0.1 unless LICH_GATEWAY_HOST is set, and a non-loopback bind requires LICH_GATEWAY_TOKEN. - docs/architecture/overview.md named 0.8.0 as the current package; point to CHANGELOG instead of a version that goes stale. - wiki: S-11 holes fixed by #163. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * refactor(providers): share HTTP/error helpers in providers/http.ts (#133) openai, anthropic and ollama carried byte-identical copies of the fetch skeleton (abort signal, request init, do_fetch, body read, success JSON, Retry-After, status mapping, http error) plus is_record / is_abort_like. Move them into src/providers/http.ts; ollama passes its wider overflow regex to to_http_error. failover.ts reuses is_abort_like. Anthropic's explicit 529 check is dropped as redundant (529 >= 500 already maps to rate_limit). No behavior change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: Ctrl+C/Esc cancel, atomic config update, test tmp cleanup (#133) - CLI one-shot and chat pass an abort signal to agent.run. Ctrl+C aborts the one-shot run (exit 1) or the running chat turn (session kept); at the chat prompt it ends chat. A second Ctrl+C while cancelling exits 130. - TUI: Esc cancels a running turn; an aborted run shows "run cancelled". - write_lich_config update writes a temp file and renames it into place, keeping the existing mode, so readers never see a partial config. - gatekeeper, tools, run_tests, skills_search and kanban_audit tests remove the dirs they create under test/.tmp. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: on cancel, drop the unanswered user line and do not reprint the old reply On abort the loop's `final` is the last assistant so far (often the previous turn's reply) and `messages` ends with the cancelled user line. Chat and the TUI now show only the cancel notice and keep history_after_abort(messages), which drops trailing user lines. The config temp file is created with the existing mode. Tests cover a second Ctrl+C (exit 130) and the chat turn after a cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: keep this turn's reply when a cancel lands during tools reply_after_abort(outcome) returns `final` only when it comes after the last user line, i.e. this run wrote it. Chat prints it and the TUI shows it before the cancel notice; an earlier turn's reply is still skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: map #113 phase children and related issues Roadmap map lists #133 (P0), #134 (P1), #117 (P2), #144, #148 and #149 with their wiki pages; the kanban skill names the phase children. Supersedes the stale #135. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): global ~/.lich/config.json merged under the project config (#117) Phase 0 + 1 of #117. Discovery is now project .lich/config.json merged over ~/.lich/config.json (shallow, project wins per key). A project providers array replaces the global one and drops the global models unless the project sets its own. The global layer ignores work_dir and session_dir, and its relative plugin paths resolve against ~/.lich/. ~/.config/lich/config.json is read only when ~/.lich/config.json is absent, with a hint to move it; nothing writes there. --config still replaces the whole chain. Bare lich no longer pins an existing config as --config when it skips the wizard, so the merge applies there too. first_run tests use an empty HOME instead of mocking the home lookup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(config): read the legacy user config only when ~/.lich/config.json is absent With work_dir = home the global file is the project file; the legacy ~/.config/lich file was then picked as the base under it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): lich init --global and a "save as global" wizard step (#117) lich init --global writes the starter config to ~/.lich/config.json (never overwrites; no work_dir/session_dir). The setup wizard ends with "Save as the global default?" (default no). Yes writes the answers to ~/.lich/config.json; discovered .lich/plugins entries stay in the project file since they are project paths. The wizard no longer pins its written file as --config, so discovery merges project over global. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): write the config once, plugins included, when work_dir is home With work_dir = home the project file is ~/.lich/config.json, so the plugins-only project write hit "already exists" and dropped plugins. Test also asserts the merged TUI plugins. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): store absolute plugin paths when writing ~/.lich/config.json from home Other projects load that file as the global layer and resolve relative plugin paths against ~/.lich, so .lich/plugins/x would become ~/.lich/.lich/plugins/x. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): named profiles in ~/.lich/profiles (#117) Phase 2 of #117. ~/.lich/profiles/<name>.json, plus an optional <name>.md used as the system prompt, merges between the global and the project config (global < profile < project). Selection: --profile, then LICH_PROFILE, then a project `profile` key, then the default in ~/.lich/config.json. Profile plugin paths resolve against ~/.lich/profiles; work_dir/session_dir in a profile are ignored. New `lich profile list|show|create|use`: create runs the wizard into the profile file (never overwrites), use sets `profile` in the global file. A requested profile skips the first-run wizard; --profile with --config is an error. File tools now refuse .lich/profiles/. tui/chat/serve/gateway no longer replace every config error with "no model configured"; only that case gets the hint. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): profile over the home config, stable errors, guard list_dir/disk_usage - work_dir = home: ~/.lich/config.json is the project file, so the profile now merges over it instead of under it. - "profile not found" / "already exists" errors carry no absolute path. - list_dir and disk_usage check the root with assert_file_tool_access, and list_dir skips denied entries, so .lich/profiles is not listed. - Docs: LICH_PROFILE is ignored with --config. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): keep the legacy base at home; disk_usage skips denied children The home-dir branch now applies only when ~/.lich/config.json exists, so a legacy ~/.config/lich file stays the base. disk_usage leaves out entries file tools may not read (.lich/profiles, .lich/config.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: config layers page after #170-#172 New entities/lich-config.md: global < profile < project merge, writers, file-tool guard, and config profile vs runtime Profile. Guardrails page and runtime-profile-session updated; index providers line fixed (#165). SCHEMA gains the `config` tag. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: scope config writer and error-path claims on lich-config Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava --------- Co-authored-by: Claude <[email protected]>
…awaited shutdown, webhook usage/502 (#174) * wiki: log merges of #115 #127 #137 #139 #141 #142 #143 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision models and Ollama model research (#148) Ingest decision-model and Ollama research, add the decision-models concept page, note ollama.com cloud auth and stale defaults on the providers page, and link the fast lane from action-terminal mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: Hermes model roles, memory embedders and Jev plugins (#148, #149) Ingest a read of Hermes upstream at bed0d535 and record how it mixes models (fallback chain plus per-task auxiliary models), where embeddings live (memory plugins), and how its 14 community Jev plugins work. Add comparison rows and point decision models and plugin hooks at the revised #148/#149 scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: default local Ollama to qwen3:8b and document Ollama cloud (#148) llama3.2 (3B) is weak at tool calling; qwen3:8b is the common local pick for agents. Switch the README, docs, persona example and the setup wizard default to qwen3:8b, keeping llama3.2 as the low-memory note. The Ollama provider already sends a Bearer header when api_key or api_key_env resolves, so Ollama cloud works today. Document it (LICH_BASE_URL=https://ollama.com, LICH_API_KEY_ENV=OLLAMA_API_KEY) and replace "unused by ollama" with "optional for ollama". Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: fix Hermes Jev plugin count and update providers after #152 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): per-role provider chains via models.chat and models.compress Optional `models` block names provider chains per role. `chat` sets the main loop's failover order; `compress` routes context compression and falls back to the chat chain on failure. ProviderRouter.for_role shares built clients. Without `models`, behavior is unchanged. The TUI labels the first chat-role provider. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * test, docs: cover models.compress through Agent; clarify omitted-role fallback Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(plugins): entry settings, host model access and before_llm_call hook Plugin entries may be { path, settings?, models? } beside a bare path. Hooks and plugin tools receive the frozen settings and models.chat(role, ...), which refuses roles the entry was not granted. A new before_llm_call hook may return a note that is capped, sent as a trailing system message on that one main-loop call, and never saved to history; throwing hooks fail open. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(plugins): deep-copy hook messages, honor run abort in models.chat, deep-freeze loader settings Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: ingest Ollama System One/Clef check; record model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: index one-liners match model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: bump index updated date Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(examples): decision_lane plugin for game_bridge action selection A plugin-owned client for Ollama's local /v1/systemone endpoint asks one choice question per enemy action (and target) before each LLM turn. Shadow mode logs one JSONL line per decision; act mode queues orders through game_bridge's enemy_actions tool and meteor veto when every answer clears the threshold, else the LLM decides. Requests are checked against Ollama's limits before sending; failures fall back and are logged. bench.mjs replays saved snapshots for latency, coverage and agreement with the LLM. Part of #148. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(decision_lane): zero off-criteria answers, require numeric threshold, keep log under .lich/game Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record the decision_lane prototype (#159) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision_lane egress mentions the Bearer key Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(agent): tools_enabled filters plugin tools; guard hooks fail closed; no tools on the last turn - A tools_enabled list (top-level or gateway) now applies to plugin tools, including the gatekeeper's git_commit. The commander persona lists its three game_bridge tools explicitly. - A before_tool_call hook that throws blocks the call (blocked_by_plugin: hook_error) instead of allowing it. - Tool calls on the max_turns turn are not executed; they are closed with a turn_budget_exhausted result so history stays valid. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(loop): emit tool_call_end for last-turn skipped calls so transcripts persist them Also document the last-turn skip in docs/architecture/agent-loop.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record #161 (plugin-tool allowlist, fail-closed guard hooks, last-turn tool skip) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: move the #161 fixed note out of the open-holes list Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: http_request status, terminal_timeout_ms, .. path guard, OpenAI tool ids, MCP name collisions - http_request returns ok:false (error http_<status>) for non-2xx, keeping status and body in output. - terminal defaults its command timeout to config terminal_timeout_ms. - The path guard rejects only .. and ../ prefixes, so names like ..notes.txt inside work_dir are allowed. - OpenAI-compatible tool calls without an id get a unique generated id. - MCP names that collide after sanitizing log a warning; first wins. - Backoff comment and provider docs no longer claim the deterministic jitter desynchronizes simultaneous callers. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(openai): treat a null tool-call id as missing; correct http_request note Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: webhook binds loopback by default; drop stale version from overview; wiki #163 - godot.md and the persona orchestrator README said the CLI webhook binds 0.0.0.0; it binds 127.0.0.1 unless LICH_GATEWAY_HOST is set, and a non-loopback bind requires LICH_GATEWAY_TOKEN. - docs/architecture/overview.md named 0.8.0 as the current package; point to CHANGELOG instead of a version that goes stale. - wiki: S-11 holes fixed by #163. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * refactor(providers): share HTTP/error helpers in providers/http.ts (#133) openai, anthropic and ollama carried byte-identical copies of the fetch skeleton (abort signal, request init, do_fetch, body read, success JSON, Retry-After, status mapping, http error) plus is_record / is_abort_like. Move them into src/providers/http.ts; ollama passes its wider overflow regex to to_http_error. failover.ts reuses is_abort_like. Anthropic's explicit 529 check is dropped as redundant (529 >= 500 already maps to rate_limit). No behavior change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: Ctrl+C/Esc cancel, atomic config update, test tmp cleanup (#133) - CLI one-shot and chat pass an abort signal to agent.run. Ctrl+C aborts the one-shot run (exit 1) or the running chat turn (session kept); at the chat prompt it ends chat. A second Ctrl+C while cancelling exits 130. - TUI: Esc cancels a running turn; an aborted run shows "run cancelled". - write_lich_config update writes a temp file and renames it into place, keeping the existing mode, so readers never see a partial config. - gatekeeper, tools, run_tests, skills_search and kanban_audit tests remove the dirs they create under test/.tmp. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: on cancel, drop the unanswered user line and do not reprint the old reply On abort the loop's `final` is the last assistant so far (often the previous turn's reply) and `messages` ends with the cancelled user line. Chat and the TUI now show only the cancel notice and keep history_after_abort(messages), which drops trailing user lines. The config temp file is created with the existing mode. Tests cover a second Ctrl+C (exit 130) and the chat turn after a cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: keep this turn's reply when a cancel lands during tools reply_after_abort(outcome) returns `final` only when it comes after the last user line, i.e. this run wrote it. Chat prints it and the TUI shows it before the cancel notice; an earlier turn's reply is still skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: map #113 phase children and related issues Roadmap map lists #133 (P0), #134 (P1), #117 (P2), #144, #148 and #149 with their wiki pages; the kanban skill names the phase children. Supersedes the stale #135. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): global ~/.lich/config.json merged under the project config (#117) Phase 0 + 1 of #117. Discovery is now project .lich/config.json merged over ~/.lich/config.json (shallow, project wins per key). A project providers array replaces the global one and drops the global models unless the project sets its own. The global layer ignores work_dir and session_dir, and its relative plugin paths resolve against ~/.lich/. ~/.config/lich/config.json is read only when ~/.lich/config.json is absent, with a hint to move it; nothing writes there. --config still replaces the whole chain. Bare lich no longer pins an existing config as --config when it skips the wizard, so the merge applies there too. first_run tests use an empty HOME instead of mocking the home lookup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(config): read the legacy user config only when ~/.lich/config.json is absent With work_dir = home the global file is the project file; the legacy ~/.config/lich file was then picked as the base under it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): lich init --global and a "save as global" wizard step (#117) lich init --global writes the starter config to ~/.lich/config.json (never overwrites; no work_dir/session_dir). The setup wizard ends with "Save as the global default?" (default no). Yes writes the answers to ~/.lich/config.json; discovered .lich/plugins entries stay in the project file since they are project paths. The wizard no longer pins its written file as --config, so discovery merges project over global. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): write the config once, plugins included, when work_dir is home With work_dir = home the project file is ~/.lich/config.json, so the plugins-only project write hit "already exists" and dropped plugins. Test also asserts the merged TUI plugins. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): store absolute plugin paths when writing ~/.lich/config.json from home Other projects load that file as the global layer and resolve relative plugin paths against ~/.lich, so .lich/plugins/x would become ~/.lich/.lich/plugins/x. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): named profiles in ~/.lich/profiles (#117) Phase 2 of #117. ~/.lich/profiles/<name>.json, plus an optional <name>.md used as the system prompt, merges between the global and the project config (global < profile < project). Selection: --profile, then LICH_PROFILE, then a project `profile` key, then the default in ~/.lich/config.json. Profile plugin paths resolve against ~/.lich/profiles; work_dir/session_dir in a profile are ignored. New `lich profile list|show|create|use`: create runs the wizard into the profile file (never overwrites), use sets `profile` in the global file. A requested profile skips the first-run wizard; --profile with --config is an error. File tools now refuse .lich/profiles/. tui/chat/serve/gateway no longer replace every config error with "no model configured"; only that case gets the hint. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): profile over the home config, stable errors, guard list_dir/disk_usage - work_dir = home: ~/.lich/config.json is the project file, so the profile now merges over it instead of under it. - "profile not found" / "already exists" errors carry no absolute path. - list_dir and disk_usage check the root with assert_file_tool_access, and list_dir skips denied entries, so .lich/profiles is not listed. - Docs: LICH_PROFILE is ignored with --config. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): keep the legacy base at home; disk_usage skips denied children The home-dir branch now applies only when ~/.lich/config.json exists, so a legacy ~/.config/lich file stays the base. disk_usage leaves out entries file tools may not read (.lich/profiles, .lich/config.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: config layers page after #170-#172 New entities/lich-config.md: global < profile < project merge, writers, file-tool guard, and config profile vs runtime Profile. Guardrails page and runtime-profile-session updated; index providers line fixed (#165). SCHEMA gains the `config` tag. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: scope config writer and error-path claims on lich-config Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(gateway): one session queue, LRU eviction, telegram-only /start, awaited shutdown, webhook usage/502 (#144) Slice 1 of #144 (A1 plus the G-10 and webhook fixes): - GatewayBus queues conversations on SessionManager instead of its own promise chains (one queue implementation). - max_conversations evicts the least recently used conversation. - Only telegram's /start (/start, /start@bot, /start <payload>) becomes "hello"; /started and other platforms pass through. - Shutdown awaits adapter stop (bounded by 5 s) before exit. - bus.reply() returns { text, usage, failed }; the webhook returns the run's usage and 502 {"error"} on agent failure. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(gateway): enforce max_conversations on store; doc webhook 502 Concurrent new chats could each pass the pre-run eviction and all store, leaving the map over the cap. store_history now trims least recently used entries after each write. Adds a keep-alive stop regression test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava --------- Co-authored-by: Claude <[email protected]>
* wiki: log merges of #115 #127 #137 #139 #141 #142 #143 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision models and Ollama model research (#148) Ingest decision-model and Ollama research, add the decision-models concept page, note ollama.com cloud auth and stale defaults on the providers page, and link the fast lane from action-terminal mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: Hermes model roles, memory embedders and Jev plugins (#148, #149) Ingest a read of Hermes upstream at bed0d535 and record how it mixes models (fallback chain plus per-task auxiliary models), where embeddings live (memory plugins), and how its 14 community Jev plugins work. Add comparison rows and point decision models and plugin hooks at the revised #148/#149 scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: default local Ollama to qwen3:8b and document Ollama cloud (#148) llama3.2 (3B) is weak at tool calling; qwen3:8b is the common local pick for agents. Switch the README, docs, persona example and the setup wizard default to qwen3:8b, keeping llama3.2 as the low-memory note. The Ollama provider already sends a Bearer header when api_key or api_key_env resolves, so Ollama cloud works today. Document it (LICH_BASE_URL=https://ollama.com, LICH_API_KEY_ENV=OLLAMA_API_KEY) and replace "unused by ollama" with "optional for ollama". Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: fix Hermes Jev plugin count and update providers after #152 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): per-role provider chains via models.chat and models.compress Optional `models` block names provider chains per role. `chat` sets the main loop's failover order; `compress` routes context compression and falls back to the chat chain on failure. ProviderRouter.for_role shares built clients. Without `models`, behavior is unchanged. The TUI labels the first chat-role provider. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * test, docs: cover models.compress through Agent; clarify omitted-role fallback Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(plugins): entry settings, host model access and before_llm_call hook Plugin entries may be { path, settings?, models? } beside a bare path. Hooks and plugin tools receive the frozen settings and models.chat(role, ...), which refuses roles the entry was not granted. A new before_llm_call hook may return a note that is capped, sent as a trailing system message on that one main-loop call, and never saved to history; throwing hooks fail open. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(plugins): deep-copy hook messages, honor run abort in models.chat, deep-freeze loader settings Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: ingest Ollama System One/Clef check; record model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: index one-liners match model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: bump index updated date Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(examples): decision_lane plugin for game_bridge action selection A plugin-owned client for Ollama's local /v1/systemone endpoint asks one choice question per enemy action (and target) before each LLM turn. Shadow mode logs one JSONL line per decision; act mode queues orders through game_bridge's enemy_actions tool and meteor veto when every answer clears the threshold, else the LLM decides. Requests are checked against Ollama's limits before sending; failures fall back and are logged. bench.mjs replays saved snapshots for latency, coverage and agreement with the LLM. Part of #148. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(decision_lane): zero off-criteria answers, require numeric threshold, keep log under .lich/game Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record the decision_lane prototype (#159) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision_lane egress mentions the Bearer key Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(agent): tools_enabled filters plugin tools; guard hooks fail closed; no tools on the last turn - A tools_enabled list (top-level or gateway) now applies to plugin tools, including the gatekeeper's git_commit. The commander persona lists its three game_bridge tools explicitly. - A before_tool_call hook that throws blocks the call (blocked_by_plugin: hook_error) instead of allowing it. - Tool calls on the max_turns turn are not executed; they are closed with a turn_budget_exhausted result so history stays valid. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(loop): emit tool_call_end for last-turn skipped calls so transcripts persist them Also document the last-turn skip in docs/architecture/agent-loop.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record #161 (plugin-tool allowlist, fail-closed guard hooks, last-turn tool skip) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: move the #161 fixed note out of the open-holes list Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: http_request status, terminal_timeout_ms, .. path guard, OpenAI tool ids, MCP name collisions - http_request returns ok:false (error http_<status>) for non-2xx, keeping status and body in output. - terminal defaults its command timeout to config terminal_timeout_ms. - The path guard rejects only .. and ../ prefixes, so names like ..notes.txt inside work_dir are allowed. - OpenAI-compatible tool calls without an id get a unique generated id. - MCP names that collide after sanitizing log a warning; first wins. - Backoff comment and provider docs no longer claim the deterministic jitter desynchronizes simultaneous callers. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(openai): treat a null tool-call id as missing; correct http_request note Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: webhook binds loopback by default; drop stale version from overview; wiki #163 - godot.md and the persona orchestrator README said the CLI webhook binds 0.0.0.0; it binds 127.0.0.1 unless LICH_GATEWAY_HOST is set, and a non-loopback bind requires LICH_GATEWAY_TOKEN. - docs/architecture/overview.md named 0.8.0 as the current package; point to CHANGELOG instead of a version that goes stale. - wiki: S-11 holes fixed by #163. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * refactor(providers): share HTTP/error helpers in providers/http.ts (#133) openai, anthropic and ollama carried byte-identical copies of the fetch skeleton (abort signal, request init, do_fetch, body read, success JSON, Retry-After, status mapping, http error) plus is_record / is_abort_like. Move them into src/providers/http.ts; ollama passes its wider overflow regex to to_http_error. failover.ts reuses is_abort_like. Anthropic's explicit 529 check is dropped as redundant (529 >= 500 already maps to rate_limit). No behavior change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: Ctrl+C/Esc cancel, atomic config update, test tmp cleanup (#133) - CLI one-shot and chat pass an abort signal to agent.run. Ctrl+C aborts the one-shot run (exit 1) or the running chat turn (session kept); at the chat prompt it ends chat. A second Ctrl+C while cancelling exits 130. - TUI: Esc cancels a running turn; an aborted run shows "run cancelled". - write_lich_config update writes a temp file and renames it into place, keeping the existing mode, so readers never see a partial config. - gatekeeper, tools, run_tests, skills_search and kanban_audit tests remove the dirs they create under test/.tmp. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: on cancel, drop the unanswered user line and do not reprint the old reply On abort the loop's `final` is the last assistant so far (often the previous turn's reply) and `messages` ends with the cancelled user line. Chat and the TUI now show only the cancel notice and keep history_after_abort(messages), which drops trailing user lines. The config temp file is created with the existing mode. Tests cover a second Ctrl+C (exit 130) and the chat turn after a cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: keep this turn's reply when a cancel lands during tools reply_after_abort(outcome) returns `final` only when it comes after the last user line, i.e. this run wrote it. Chat prints it and the TUI shows it before the cancel notice; an earlier turn's reply is still skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: map #113 phase children and related issues Roadmap map lists #133 (P0), #134 (P1), #117 (P2), #144, #148 and #149 with their wiki pages; the kanban skill names the phase children. Supersedes the stale #135. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): global ~/.lich/config.json merged under the project config (#117) Phase 0 + 1 of #117. Discovery is now project .lich/config.json merged over ~/.lich/config.json (shallow, project wins per key). A project providers array replaces the global one and drops the global models unless the project sets its own. The global layer ignores work_dir and session_dir, and its relative plugin paths resolve against ~/.lich/. ~/.config/lich/config.json is read only when ~/.lich/config.json is absent, with a hint to move it; nothing writes there. --config still replaces the whole chain. Bare lich no longer pins an existing config as --config when it skips the wizard, so the merge applies there too. first_run tests use an empty HOME instead of mocking the home lookup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(config): read the legacy user config only when ~/.lich/config.json is absent With work_dir = home the global file is the project file; the legacy ~/.config/lich file was then picked as the base under it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): lich init --global and a "save as global" wizard step (#117) lich init --global writes the starter config to ~/.lich/config.json (never overwrites; no work_dir/session_dir). The setup wizard ends with "Save as the global default?" (default no). Yes writes the answers to ~/.lich/config.json; discovered .lich/plugins entries stay in the project file since they are project paths. The wizard no longer pins its written file as --config, so discovery merges project over global. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): write the config once, plugins included, when work_dir is home With work_dir = home the project file is ~/.lich/config.json, so the plugins-only project write hit "already exists" and dropped plugins. Test also asserts the merged TUI plugins. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): store absolute plugin paths when writing ~/.lich/config.json from home Other projects load that file as the global layer and resolve relative plugin paths against ~/.lich, so .lich/plugins/x would become ~/.lich/.lich/plugins/x. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): named profiles in ~/.lich/profiles (#117) Phase 2 of #117. ~/.lich/profiles/<name>.json, plus an optional <name>.md used as the system prompt, merges between the global and the project config (global < profile < project). Selection: --profile, then LICH_PROFILE, then a project `profile` key, then the default in ~/.lich/config.json. Profile plugin paths resolve against ~/.lich/profiles; work_dir/session_dir in a profile are ignored. New `lich profile list|show|create|use`: create runs the wizard into the profile file (never overwrites), use sets `profile` in the global file. A requested profile skips the first-run wizard; --profile with --config is an error. File tools now refuse .lich/profiles/. tui/chat/serve/gateway no longer replace every config error with "no model configured"; only that case gets the hint. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): profile over the home config, stable errors, guard list_dir/disk_usage - work_dir = home: ~/.lich/config.json is the project file, so the profile now merges over it instead of under it. - "profile not found" / "already exists" errors carry no absolute path. - list_dir and disk_usage check the root with assert_file_tool_access, and list_dir skips denied entries, so .lich/profiles is not listed. - Docs: LICH_PROFILE is ignored with --config. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): keep the legacy base at home; disk_usage skips denied children The home-dir branch now applies only when ~/.lich/config.json exists, so a legacy ~/.config/lich file stays the base. disk_usage leaves out entries file tools may not read (.lich/profiles, .lich/config.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: config layers page after #170-#172 New entities/lich-config.md: global < profile < project merge, writers, file-tool guard, and config profile vs runtime Profile. Guardrails page and runtime-profile-session updated; index providers line fixed (#165). SCHEMA gains the `config` tag. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: scope config writer and error-path claims on lich-config Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(gateway): one session queue, LRU eviction, telegram-only /start, awaited shutdown, webhook usage/502 (#144) Slice 1 of #144 (A1 plus the G-10 and webhook fixes): - GatewayBus queues conversations on SessionManager instead of its own promise chains (one queue implementation). - max_conversations evicts the least recently used conversation. - Only telegram's /start (/start, /start@bot, /start <payload>) becomes "hello"; /started and other platforms pass through. - Shutdown awaits adapter stop (bounded by 5 s) before exit. - bus.reply() returns { text, usage, failed }; the webhook returns the run's usage and 502 {"error"} on agent failure. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(gateway): enforce max_conversations on store; doc webhook 502 Concurrent new chats could each pass the pre-run eviction and all store, leaving the map over the cap. store_history now trims least recently used entries after each write. Adds a keep-alive stop regression test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(gateway): GatewayPolicy and declared adapter capabilities (#144) Slice 2 of #144 (section A, items 2-3): - access.ts gains GatewayPolicy (allowlists + gateway toolset), built once from config; the runner builds it and hands it to the bus, which accepts an injected policy. - PlatformAdapter declares capabilities { kind: "text", max_reply_chars }. telegram 4096, discord 2000 (was a bare literal), twitch 450, webhook uncapped; idle adapters keep theirs. The runner logs them at start. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava --------- Co-authored-by: Claude <[email protected]>
* wiki: log merges of #115 #127 #137 #139 #141 #142 #143 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision models and Ollama model research (#148) Ingest decision-model and Ollama research, add the decision-models concept page, note ollama.com cloud auth and stale defaults on the providers page, and link the fast lane from action-terminal mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: Hermes model roles, memory embedders and Jev plugins (#148, #149) Ingest a read of Hermes upstream at bed0d535 and record how it mixes models (fallback chain plus per-task auxiliary models), where embeddings live (memory plugins), and how its 14 community Jev plugins work. Add comparison rows and point decision models and plugin hooks at the revised #148/#149 scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: default local Ollama to qwen3:8b and document Ollama cloud (#148) llama3.2 (3B) is weak at tool calling; qwen3:8b is the common local pick for agents. Switch the README, docs, persona example and the setup wizard default to qwen3:8b, keeping llama3.2 as the low-memory note. The Ollama provider already sends a Bearer header when api_key or api_key_env resolves, so Ollama cloud works today. Document it (LICH_BASE_URL=https://ollama.com, LICH_API_KEY_ENV=OLLAMA_API_KEY) and replace "unused by ollama" with "optional for ollama". Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: fix Hermes Jev plugin count and update providers after #152 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): per-role provider chains via models.chat and models.compress Optional `models` block names provider chains per role. `chat` sets the main loop's failover order; `compress` routes context compression and falls back to the chat chain on failure. ProviderRouter.for_role shares built clients. Without `models`, behavior is unchanged. The TUI labels the first chat-role provider. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * test, docs: cover models.compress through Agent; clarify omitted-role fallback Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(plugins): entry settings, host model access and before_llm_call hook Plugin entries may be { path, settings?, models? } beside a bare path. Hooks and plugin tools receive the frozen settings and models.chat(role, ...), which refuses roles the entry was not granted. A new before_llm_call hook may return a note that is capped, sent as a trailing system message on that one main-loop call, and never saved to history; throwing hooks fail open. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(plugins): deep-copy hook messages, honor run abort in models.chat, deep-freeze loader settings Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: ingest Ollama System One/Clef check; record model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: index one-liners match model roles and plugin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: bump index updated date Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(examples): decision_lane plugin for game_bridge action selection A plugin-owned client for Ollama's local /v1/systemone endpoint asks one choice question per enemy action (and target) before each LLM turn. Shadow mode logs one JSONL line per decision; act mode queues orders through game_bridge's enemy_actions tool and meteor veto when every answer clears the threshold, else the LLM decides. Requests are checked against Ollama's limits before sending; failures fall back and are logged. bench.mjs replays saved snapshots for latency, coverage and agreement with the LLM. Part of #148. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(decision_lane): zero off-criteria answers, require numeric threshold, keep log under .lich/game Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record the decision_lane prototype (#159) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: decision_lane egress mentions the Bearer key Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(agent): tools_enabled filters plugin tools; guard hooks fail closed; no tools on the last turn - A tools_enabled list (top-level or gateway) now applies to plugin tools, including the gatekeeper's git_commit. The commander persona lists its three game_bridge tools explicitly. - A before_tool_call hook that throws blocks the call (blocked_by_plugin: hook_error) instead of allowing it. - Tool calls on the max_turns turn are not executed; they are closed with a turn_budget_exhausted result so history stays valid. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(loop): emit tool_call_end for last-turn skipped calls so transcripts persist them Also document the last-turn skip in docs/architecture/agent-loop.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: record #161 (plugin-tool allowlist, fail-closed guard hooks, last-turn tool skip) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: move the #161 fixed note out of the open-holes list Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: http_request status, terminal_timeout_ms, .. path guard, OpenAI tool ids, MCP name collisions - http_request returns ok:false (error http_<status>) for non-2xx, keeping status and body in output. - terminal defaults its command timeout to config terminal_timeout_ms. - The path guard rejects only .. and ../ prefixes, so names like ..notes.txt inside work_dir are allowed. - OpenAI-compatible tool calls without an id get a unique generated id. - MCP names that collide after sanitizing log a warning; first wins. - Backoff comment and provider docs no longer claim the deterministic jitter desynchronizes simultaneous callers. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(openai): treat a null tool-call id as missing; correct http_request note Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * docs: webhook binds loopback by default; drop stale version from overview; wiki #163 - godot.md and the persona orchestrator README said the CLI webhook binds 0.0.0.0; it binds 127.0.0.1 unless LICH_GATEWAY_HOST is set, and a non-loopback bind requires LICH_GATEWAY_TOKEN. - docs/architecture/overview.md named 0.8.0 as the current package; point to CHANGELOG instead of a version that goes stale. - wiki: S-11 holes fixed by #163. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * refactor(providers): share HTTP/error helpers in providers/http.ts (#133) openai, anthropic and ollama carried byte-identical copies of the fetch skeleton (abort signal, request init, do_fetch, body read, success JSON, Retry-After, status mapping, http error) plus is_record / is_abort_like. Move them into src/providers/http.ts; ollama passes its wider overflow regex to to_http_error. failover.ts reuses is_abort_like. Anthropic's explicit 529 check is dropped as redundant (529 >= 500 already maps to rate_limit). No behavior change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: Ctrl+C/Esc cancel, atomic config update, test tmp cleanup (#133) - CLI one-shot and chat pass an abort signal to agent.run. Ctrl+C aborts the one-shot run (exit 1) or the running chat turn (session kept); at the chat prompt it ends chat. A second Ctrl+C while cancelling exits 130. - TUI: Esc cancels a running turn; an aborted run shows "run cancelled". - write_lich_config update writes a temp file and renames it into place, keeping the existing mode, so readers never see a partial config. - gatekeeper, tools, run_tests, skills_search and kanban_audit tests remove the dirs they create under test/.tmp. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: on cancel, drop the unanswered user line and do not reprint the old reply On abort the loop's `final` is the last assistant so far (often the previous turn's reply) and `messages` ends with the cancelled user line. Chat and the TUI now show only the cancel notice and keep history_after_abort(messages), which drops trailing user lines. The config temp file is created with the existing mode. Tests cover a second Ctrl+C (exit 130) and the chat turn after a cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix: keep this turn's reply when a cancel lands during tools reply_after_abort(outcome) returns `final` only when it comes after the last user line, i.e. this run wrote it. Chat prints it and the TUI shows it before the cancel notice; an earlier turn's reply is still skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: map #113 phase children and related issues Roadmap map lists #133 (P0), #134 (P1), #117 (P2), #144, #148 and #149 with their wiki pages; the kanban skill names the phase children. Supersedes the stale #135. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): global ~/.lich/config.json merged under the project config (#117) Phase 0 + 1 of #117. Discovery is now project .lich/config.json merged over ~/.lich/config.json (shallow, project wins per key). A project providers array replaces the global one and drops the global models unless the project sets its own. The global layer ignores work_dir and session_dir, and its relative plugin paths resolve against ~/.lich/. ~/.config/lich/config.json is read only when ~/.lich/config.json is absent, with a hint to move it; nothing writes there. --config still replaces the whole chain. Bare lich no longer pins an existing config as --config when it skips the wizard, so the merge applies there too. first_run tests use an empty HOME instead of mocking the home lookup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(config): read the legacy user config only when ~/.lich/config.json is absent With work_dir = home the global file is the project file; the legacy ~/.config/lich file was then picked as the base under it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): lich init --global and a "save as global" wizard step (#117) lich init --global writes the starter config to ~/.lich/config.json (never overwrites; no work_dir/session_dir). The setup wizard ends with "Save as the global default?" (default no). Yes writes the answers to ~/.lich/config.json; discovered .lich/plugins entries stay in the project file since they are project paths. The wizard no longer pins its written file as --config, so discovery merges project over global. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): write the config once, plugins included, when work_dir is home With work_dir = home the project file is ~/.lich/config.json, so the plugins-only project write hit "already exists" and dropped plugins. Test also asserts the merged TUI plugins. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(wizard): store absolute plugin paths when writing ~/.lich/config.json from home Other projects load that file as the global layer and resolve relative plugin paths against ~/.lich, so .lich/plugins/x would become ~/.lich/.lich/plugins/x. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(config): named profiles in ~/.lich/profiles (#117) Phase 2 of #117. ~/.lich/profiles/<name>.json, plus an optional <name>.md used as the system prompt, merges between the global and the project config (global < profile < project). Selection: --profile, then LICH_PROFILE, then a project `profile` key, then the default in ~/.lich/config.json. Profile plugin paths resolve against ~/.lich/profiles; work_dir/session_dir in a profile are ignored. New `lich profile list|show|create|use`: create runs the wizard into the profile file (never overwrites), use sets `profile` in the global file. A requested profile skips the first-run wizard; --profile with --config is an error. File tools now refuse .lich/profiles/. tui/chat/serve/gateway no longer replace every config error with "no model configured"; only that case gets the hint. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): profile over the home config, stable errors, guard list_dir/disk_usage - work_dir = home: ~/.lich/config.json is the project file, so the profile now merges over it instead of under it. - "profile not found" / "already exists" errors carry no absolute path. - list_dir and disk_usage check the root with assert_file_tool_access, and list_dir skips denied entries, so .lich/profiles is not listed. - Docs: LICH_PROFILE is ignored with --config. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(profiles): keep the legacy base at home; disk_usage skips denied children The home-dir branch now applies only when ~/.lich/config.json exists, so a legacy ~/.config/lich file stays the base. disk_usage leaves out entries file tools may not read (.lich/profiles, .lich/config.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: config layers page after #170-#172 New entities/lich-config.md: global < profile < project merge, writers, file-tool guard, and config profile vs runtime Profile. Guardrails page and runtime-profile-session updated; index providers line fixed (#165). SCHEMA gains the `config` tag. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * wiki: scope config writer and error-path claims on lich-config Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(gateway): one session queue, LRU eviction, telegram-only /start, awaited shutdown, webhook usage/502 (#144) Slice 1 of #144 (A1 plus the G-10 and webhook fixes): - GatewayBus queues conversations on SessionManager instead of its own promise chains (one queue implementation). - max_conversations evicts the least recently used conversation. - Only telegram's /start (/start, /start@bot, /start <payload>) becomes "hello"; /started and other platforms pass through. - Shutdown awaits adapter stop (bounded by 5 s) before exit. - bus.reply() returns { text, usage, failed }; the webhook returns the run's usage and 502 {"error"} on agent failure. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * fix(gateway): enforce max_conversations on store; doc webhook 502 Concurrent new chats could each pass the pre-run eviction and all store, leaving the map over the cap. store_history now trims least recently used entries after each write. Adds a keep-alive stop regression test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(gateway): GatewayPolicy and declared adapter capabilities (#144) Slice 2 of #144 (section A, items 2-3): - access.ts gains GatewayPolicy (allowlists + gateway toolset), built once from config; the runner builds it and hands it to the bus, which accepts an injected policy. - PlatformAdapter declares capabilities { kind: "text", max_reply_chars }. telegram 4096, discord 2000 (was a bare literal), twitch 450, webhook uncapped; idle adapters keep theirs. The runner logs them at start. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava * feat(serve): queue frames per session on each connection (#144) Frames are now queued by params.session_id instead of one tail per connection, so a long prompt.submit no longer holds up other sessions or session-less frames (health, session.list, session.create) on the same WebSocket. One session's frames keep their order; prompt.abort still bypasses every queue. Idle lanes are dropped. Docs: the stale "runs are serialized" note is replaced (events are per-run since #136). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava --------- Co-authored-by: Claude <[email protected]>


Part of #117. This is PR C, covering Phase 2: named profiles. It builds on the global config work in #170 and #171. With it, every acceptance criterion in #117 is met.
Behavior
~/.lich/profiles/<name>.jsonholds any config keys. An optional~/.lich/profiles/<name>.md(the soul file) becomes the profile'ssystem_promptwhen it isn't blank, replacing anysystem_promptin the JSON. A profile can also be just the.mdfile.providersdrops the earliermodelsunless that layer sets its own). A profile ignoreswork_dirandsession_dir, and its relative plugin paths resolve against~/.lich/profiles/.--profile <name>, thenLICH_PROFILE, then aprofilekey in the project config, then the default profile (aprofilekey in~/.lich/config.json). Theprofilekey is removed from the merged config.profile not found: <name>. Names must match^[a-z0-9][a-z0-9_-]*$, so../xis rejected.--profiletogether with--configis an error.--profileorLICH_PROFILEis set, barelichskips the first-run wizard.lich profile(newsrc/cli_profile.ts):list:*marks the default,(soul)marks a profile that has a.mdfile.show <name>: prints the JSON and the soul file's path and length.create <name>: runs the setup wizard into the profile file. It needs a TTY and never overwrites, and it doesn't pick up project plugins.use <name>: setsprofilein~/.lich/config.jsonthrough the atomic update writer, keeping the file's other keys..lich/profiles/for both reads and writes. This adds a read block alongside the existing.lichwrite rule. Whenwork_diris the home directory, these are the global identity files.A related fix
lich tui,chat,serveandgatewayused to replace every config error with "no model configured". That would have turnedprofile not foundinto a misleading hint. Now only the missing-model error gets the hint; any other error passes through aslich <mode>: <message>.Decisions (asked before implementing)
profilekey in~/.lich/config.json.createis included.Not included: showing the active profile in the TUI banner, which #117 lists as an optional follow-up.
Tests
cli_config.test.ts:system_promptand the projectsystem_prompt, and a blank soul file being ignored;first_run.test.ts(each test uses an emptyHOME, withLICH_PROFILEcleared):profile createthrough the wizard, plus a refused second create;profile usekeeping the other global keys, theprofile listoutput, anduseof an unknown name;--profileskipping the wizard and loading the profile;--profilewith--configrejected;lich tui: profile not found: ghost.tools.test.ts:read_fileandwrite_fileon.lich/profiles/are refused.first_runmock ofwrite_lich_confignow passes theupdateargument through. It used to drop it.Checks
tsc --noEmit: clean.serve_transport"accepts ::1 clients" (no IPv6) andtools_review_shouldfixS-6 (process-group zombies).vitepress build docs: passes. There's a new guide,docs/user-guide/profiles.md, added to the sidebar;docs/user-guide/cli.mdand the CHANGELOG are updated.🤖 Generated with Claude Code
https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Generated by Claude Code