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