CHANGELOG.md

# Changelog

## v0.1.0

### API keys now stored under :req_llm app

- `anthropic_api_key`, `openai_api_key`, `google_api_key` Skogsra entries now
  write into `Application.env(:req_llm, ...)` instead of `:planck`, so req_llm
  resolves them directly from its own config source without extra wiring.

### Dynamic worker session history preserved on resume

- On session resume, dynamic worker agents are reconstructed with their original
  agent ids extracted from the `spawn_agent` tool-result messages in session
  history. Worker message history is fully visible after restart.
- Failed `spawn_agent` calls (error results) are skipped during reconstruction.
  The most recent successful spawn wins when the orchestrator retried.
- `save_metadata` now runs after `reconstruct_dynamic_workers` so reconstructed
  worker ids are captured for subsequent resumes.

### Worker duplication fix on resume

- `reconstruct_dynamic_workers` deduplicates spawn calls by `{type, name}` —
  a worker spawned multiple times (e.g. after a recovery nudge) is only
  reconstructed once.

### API key loading from .planck/.env

- New `Planck.Headless.Config.EnvBinding` — Skogsra binding that reads API
  keys from `./.planck/.env` (project-local) and `~/.planck/.env` (global).
  Priority: system env → project .env → global .env → Elixir config.
  Standard dotenv format; skipped in tests via `skip_env_config: true`.
- `Config.env_files` app_env — configurable list of env files; defaults to
  `["~/.planck/.env", "./.planck/.env"]`.

### Runtime model configuration

- `Headless.configure_model/1` — writes a model configuration to disk and
  reloads resources. Options: `provider:`, `model_id:`, `scope:` (`:local` or
  `:global`), `api_key:`, `base_url:`, `model_name:`, `context_window:`,
  `supports_thinking:`, `advanced_opts:` (map for `default_opts`),
  `default:` (set as `default_provider`/`default_model`).
  Writes to JSON config file (merging with existing content, appending to
  `models` array for local providers) and to the `.env` file for API keys.
  Accepts `config_file:` and `env_file:` overrides for test isolation.
- `reload_resources/0` now clears all Skogsra key caches (`Config.reload_*`)
  before calling `ResourceStore.reload/0`, ensuring config file changes are
  immediately visible without stale persistent-term values.

### Session metadata

- `team_description` added to session metadata — populated from
  `team.description` at `start_session` and preserved on `resume_session`.
  Used by the Web UI to render a welcome card in the empty chat state.

### AGENTS.md prepended to all agents

- Static workers now receive `AGENTS.md` prepended to their system prompt, on
  par with the orchestrator. `start_workers` calls `Tools.prepend_agents_md/2`
  (the now-public function from `planck_agent`) and passes `cwd` to each worker's
  start opts so the field is populated in agent state.
- `prepend_agents_md/2` and `find_agents_md/1` removed from `planck_headless` —
  replaced by `Planck.Agent.Tools.prepend_agents_md/2`, which is the single
  implementation used by both static worker/orchestrator startup and dynamic
  `spawn_agent` calls.

### Inter-agent tools — orchestrator improvements

- `orchestrator_tools/6` — added `grantable_skills` parameter; orchestrators
  can now grant skills to dynamically spawned workers via `spawn_agent`.
- `start_orchestrator` passes `store.skills` as `grantable_skills` so all
  available skills are grantable by default.
- `start_workers` and `start_dynamic_worker` pass the worker's own id as
  `own_id` to `worker_tools/4` for deadlock detection in `ask_agent`.
- `list_models` tool now includes `base_url` in its output so the LLM can
  pass the correct base_url when calling `spawn_agent` for non-default servers.

### Session — agent usage persistence

- `start_orchestrator` and `start_workers` read `agent_usage:#{id}` from
  session metadata and pass `:usage` and `:cost` init options to each agent so
  token counts and cost are restored on session resume.

### Skills — `list_skills` opt-in tool

- `list_skills` tool added to the agent tool pool when skills are available.
  Agents that need autonomous skill discovery declare `"list_skills"` in their
  TEAM.json `"tools"` array. `load_skill` is injected automatically by
  `AgentSpec.to_start_opts/2` and does not need to be declared.

### Prior entries

First release.

- `Planck.Headless.SidecarManager` — manages the optional sidecar OTP
  application: builds (`mix deps.get` + `mix compile`), spawns via erlexec
  (`elixir --sname planck_sidecar --cookie <cookie> -S mix run --no-halt`),
  monitors node connections, auto-discovers the entry module via
  `Planck.Agent.Sidecar.discover/0` RPC on nodeup, wraps tools with RPC
  `execute_fn` closures, stores in `ResourceStore`; clears on nodedown; forwards
  `PATH`, `MIX_ENV`, `PLANCK_LOCAL` from the parent environment; PubSub events
  on `"planck:sidecar"` topic; `subscribe/0` / `unsubscribe/0` API
- `ResourceStore.put_tools/1` and `clear_tools/0` — called by `SidecarManager`
  to sync sidecar tools
- `Config.sidecar` (`PLANCK_SIDECAR`) — path to the sidecar Mix project directory
- Removed `Config.tools_dirs`, `Config.compactor`, `ResourceStore.on_compact`;
  per-agent compactors via `AgentSpec.compactor` and `Compactor.build/2`
- `Config.JsonBinding.init/1` returns `:error` (not `{:ok, %{}}`) when
  `skip_json_config: true` — Skogsra skips the binding without emitting warnings

### Edit-message support

- `Headless.rewind_to_message/3` — truncates the session to strictly before the
  given DB row id (`Session.truncate_after/2`), rewinds the orchestrator's
  in-memory history to before that same id (`Agent.rewind_to_message/2`, since
  `Message.id == db_id` for persisted messages), then re-prompts with `new_text`;
  powers the edit-message UI feature

### Session lifecycle

- `Planck.Headless.start_session/1` — resolves team (alias, path, or nil for
  the default dynamic team), generates a `<adjective>-<noun>` session name,
  starts `Planck.Agent.Session`, materialises agents with built-in + external
  tools and resolved skills, saves metadata (`team_id`, `team_alias`, `cwd`,
  `session_name`) to SQLite.
- `Planck.Headless.resume_session/1` — accepts session id or name, reopens the
  SQLite session, reconstructs the base team from metadata, replays completed
  `spawn_agent` calls from the previous orchestrator's history to restore
  dynamically-added workers (deduped by `{type, name}` against the base team,
  so two builders "Bob" and "Charlie" are both correctly reconstructed),
  detects in-flight `ask_agent` and unfinished workers, injects a recovery
  context message under the new orchestrator if anything was in-flight.
- `Planck.Headless.close_session/1` — stops all agents by `team_id`, stops the
  Session GenServer; SQLite file retained.
- `Planck.Headless.prompt/2` — dispatches to the orchestrator via the agent
  registry (`team_id` is read from session metadata; no separate tracker).
- `Planck.Headless.list_sessions/0` — globs sessions dir for `<id>_<name>.db`
  files; checks `Session.whereis/1` for active status.
- `Planck.Headless.list_teams/0`, `get_team/1` — wrap `ResourceStore`.
- `Planck.Headless.available_models/0`, `reload_resources/0`.

### Team materialization

- Orchestrators receive all four `BuiltinTools` (read, write, edit, bash) in
  their `tool_pool` so spec.tools names like `"read"` resolve correctly.
- `orchestrator_tools` + `worker_tools` injected on top of resolved spec tools;
  workers get `worker_tools` only (no spawn_agent etc.).
- Default dynamic team: orchestrator's `base_url` pulled from
  `ResourceStore.available_models` so local servers use the correct URL.

### Config

- `Planck.Headless.Config.JsonBinding` — Skogsra `Binding` that reads
  `~/.planck/config.json` and `.planck/config.json` at resolution time; results
  cached in persistent_term; `invalidate/0` for cache busting before reload.
- `config_files` app_env (`PLANCK_CONFIG_FILES`) — controls which JSON files
  are read; `config :planck_headless, :skip_json_config, true` for tests.
- `models` app_env — `Planck.AI.Config`-format model declarations parsed to
  `[Planck.AI.Model.t()]`; replaces `local_servers`; no network at boot.
- Provider atoms pre-loaded at boot via `Planck.AI.Model.providers()` to avoid
  `String.to_existing_atom` failures on lazy module load.
- `PathList` inline as `Planck.Headless.Config.PathList` submodule.

### ResourceStore

- Cloud models: static LLMDB catalog filtered by API key presence.
- Local/custom models: from `Config.models!()` — already parsed, zero network.
- `AppSupervisor` owns `ResourceStore`; no `SessionRegistry` — dropped in
  favour of reading `team_id` directly from session SQLite metadata.

### Session naming

- `Planck.Headless.SessionName` — generates `<adjective>-<noun>` names;
  `generate/1` retries on collision; `sanitize/1` normalises to `[a-z0-9-]+`.
- Session files stored as `<sessions_dir>/<id>_<name>.db`;
  `Session.find_by_id/2` and `find_by_name/2` use glob for O(1) lookup.

### Other

- `Planck.Headless.DefaultPrompt` — default system prompt for dynamic-team
  orchestrator.
- `Mox` in test deps; `Planck.Agent.MockAI` wired in test.exs.
- `start_session(template: alias)` exercised via ResourceStore in tests.
- Fixed in-flight detection and completed spawn_agent matching to use
  `MapSet.member?/2` instead of `is_map_key/2` guard (MapSet is a struct,
  not a plain map; the guard silently never matched).

### Session resume improvements

- Stable agent IDs across session resumes: `save_metadata` now persists an `agent_ids`
  map (name→id JSON) and `resume_session` loads it, passing previous IDs to
  `materialize_team`, `start_workers`, and `start_dynamic_worker` so processes restart
  with the same IDs they had in the original session
- `maybe_inject_recovery` simplified: no longer needs `find_previous_orchestrator` since
  IDs are stable across resumes

### Worker lifecycle

- `unfinished_workers` rewrite: uses `worker_unfinished?/1` — a worker is considered
  unfinished when their most recent `:user` message (last assigned task) has no
  `send_response` in any assistant message that follows it
- `send_response` sender attribution threaded through: `start_workers` and
  `start_dynamic_worker` now build a `sender = %{id, name}` map and pass it to
  `worker_tools/3`, so every response reaches the orchestrator with full sender metadata