v0.1.4
- Version bump to stay in sync with the monorepo release; no functional changes.
v0.1.3
ResourceStore — preserve sidecar tools across reloads
ResourceStore.reload/0now preservesstate.tools(sidecar tools) alongsideregistered_toolsandon_reloadcallbacks. Previously, any config file change that triggered a reload (e.g. editingconfig.json) silently wiped the sidecar tool list, leaving the orchestrator without sidecar tools until restart.
SidecarManager — clear RELEASE_* env vars
- Sidecar processes no longer inherit the OTP release environment. The release
start script adds
erts-<vsn>/binandreleases/<vsn>/toPATH, causing childelixirinvocations to resolve to the release's wrapper script, which hard-codes-boot ${RELEASE_BOOT_SCRIPT}and fails withcannot get bootfile.env/1now strips all/app/releaseentries fromPATHand unsets allRELEASE_*vars so sidecar processes use the systemelixiranderl.
Watcher — graceful degradation without inotify
Watcher.init/1now handles:ignorefromFileSystem.start_link/1(returned wheninotify-toolsis absent on Linux) instead of crashing with aMatchError. The watcher starts in no-op mode and logs a warning.
SidecarManager — mix setup fallback
SidecarManagernow callsmix setupbeforemix compileif the sidecar defines asetupalias, falling back tomix deps.getotherwise. Enables sidecars that require extra setup steps (e.g.npm installfor Node.js tools) to declare them inmix.exswithout any changes toplanck_headless.
PathList Windows fix
PathList.cast/1now splits on~r/;|:(?![\/\\])/instead of":", preserving Windows drive-letter colons (C:\...,C:/...) while still splitting Unix colon-separated paths. Semicolons are accepted as an alternative separator on all platforms.
v0.1.2
- Version bump to stay in sync with the monorepo release; no functional changes.
v0.1.1
Config + .env hot-reload
ResourceStore.register_on_reload/1— accepts a zero-arity closure from packages aboveplanck_headlessin the dependency tree. Closures are fired after binding invalidation on everyreload/0call and preserved across reloads. Enables callers to invalidate their own Skogsra caches without creating a reverse dependency.reload/0now invalidatesJsonBindingandEnvBindingpersistent-term caches before reloading resources, so changes toconfig.jsonand.envfiles are picked up immediately by all Skogsra keys.registered_toolsis preserved across reloads (previously wiped byload_resources/0returning a fresh struct).
Local node tools
Planck.Headless.register_tool/1— registers a tool globally inResourceStore; available to all new sessions for the lifetime of the node.Planck.Headless.unregister_tool/1— removes a globally registered tool by name; no-op if not found.start_session/1gains atools:option for per-session tools that shadow global ones without touchingResourceStore.ResourceStoregainsregistered_tools: [Tool.t()]field;put_tools/1andclear_tools/0only affect sidecar tools and never touchregistered_tools.materialize_teamtool pool expanded tobuiltins() ++ store.tools ++ store.registered_tools ++ session_tools.
Watcher GenServer + file_system dep
Planck.Headless.Watcher— new GenServer started byAppSupervisor; watches configured skill and team directories with a 300 ms debounce and callsResourceStore.reload/0automatically on file changes. Uses thefile_systemHex package (wrapsinotify/FSEvents/ReadDirectoryChangesW).file_systemadded toplanck_headlessdeps.
Dynamic skill injection
- All
AgentSpec.to_start_opts/2call sites (start_orchestrator,start_workers,start_dynamic_worker) now passskill_refresh_fn: fn -> ResourceStore.get().skills endso every agent resolves skill descriptions fresh fromResourceStoreon each LLM turn.
v0.1.0
API keys now stored under :req_llm app
anthropic_api_key,openai_api_key,google_api_keySkogsra entries now write intoApplication.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_agenttool-result messages in session history. Worker message history is fully visible after restart. - Failed
spawn_agentcalls (error results) are skipped during reconstruction. The most recent successful spawn wins when the orchestrator retried. save_metadatanow runs afterreconstruct_dynamic_workersso reconstructed worker ids are captured for subsequent resumes.
Worker duplication fix on resume
reconstruct_dynamic_workersdeduplicates 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
EnvBinding(internal) — 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 viaskip_env_config: true. Config.env_filesapp_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:(:localor:global),api_key:,base_url:,model_name:,context_window:,supports_thinking:,advanced_opts:(map fordefault_opts),default:(set asdefault_provider/default_model). Writes to JSON config file (merging with existing content, appending tomodelsarray for local providers) and to the.envfile for API keys. Acceptsconfig_file:andenv_file:overrides for test isolation.reload_resources/0now clears all Skogsra key caches (Config.reload_*) before callingResourceStore.reload/0, ensuring config file changes are immediately visible without stale persistent-term values.
Session metadata
team_descriptionadded to session metadata — populated fromteam.descriptionatstart_sessionand preserved onresume_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.mdprepended to their system prompt, on par with the orchestrator.start_workerscallsTools.prepend_agents_md/2(the now-public function fromplanck_agent) and passescwdto each worker's start opts so the field is populated in agent state. prepend_agents_md/2andfind_agents_md/1removed fromplanck_headless— replaced byPlanck.Agent.Tools.prepend_agents_md/2, which is the single implementation used by both static worker/orchestrator startup and dynamicspawn_agentcalls.
Inter-agent tools — orchestrator improvements
orchestrator_tools/6— addedgrantable_skillsparameter; orchestrators can now grant skills to dynamically spawned workers viaspawn_agent.start_orchestratorpassesstore.skillsasgrantable_skillsso all available skills are grantable by default.start_workersandstart_dynamic_workerpass the worker's own id asown_idtoworker_tools/4for deadlock detection inask_agent.list_modelstool now includesbase_urlin its output so the LLM can pass the correct base_url when callingspawn_agentfor non-default servers.
Session — agent usage persistence
start_orchestratorandstart_workersreadagent_usage:#{id}from session metadata and pass:usageand:costinit options to each agent so token counts and cost are restored on session resume.
Skills — list_skills opt-in tool
list_skillstool 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_skillis injected automatically byAgentSpec.to_start_opts/2and 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 viaPlanck.Agent.Sidecar.discover/0RPC on nodeup, wraps tools with RPCexecute_fnclosures, stores inResourceStore; clears on nodedown; forwardsPATH,MIX_ENV,PLANCK_LOCALfrom the parent environment; PubSub events on"planck:sidecar"topic;subscribe/0/unsubscribe/0APIResourceStore.put_tools/1andclear_tools/0— called bySidecarManagerto sync sidecar toolsConfig.sidecar(PLANCK_SIDECAR) — path to the sidecar Mix project directory- Removed
Config.tools_dirs,Config.compactor,ResourceStore.on_compact; per-agent compactors viaAgentSpec.compactorandCompactor.build/2 Config.JsonBinding.init/1returns:error(not{:ok, %{}}) whenskip_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 (Planck.Agent.rewind_to_message/2, sinceMessage.id == db_idfor persisted messages), then re-prompts withnew_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, startsPlanck.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 completedspawn_agentcalls 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-flightask_agentand unfinished workers, injects a recovery context message under the new orchestrator if anything was in-flight.Planck.Headless.close_session/1— stops all agents byteam_id, stops the Session GenServer; SQLite file retained.Planck.Headless.prompt/2— dispatches to the orchestrator via the agent registry (team_idis read from session metadata; no separate tracker).Planck.Headless.list_sessions/0— globs sessions dir for<id>_<name>.dbfiles; checksSession.whereis/1for active status.Planck.Headless.list_teams/0,get_team/1— wrapResourceStore.Planck.Headless.available_models/0,reload_resources/0.
Team materialization
- Orchestrators receive all four
BuiltinTools(read, write, edit, bash) in theirtool_poolso spec.tools names like"read"resolve correctly. orchestrator_tools+worker_toolsinjected on top of resolved spec tools; workers getworker_toolsonly (no spawn_agent etc.).- Default dynamic team: orchestrator's
base_urlpulled fromResourceStore.available_modelsso local servers use the correct URL.
Config
JsonBinding(internal) — SkogsraBindingthat reads~/.planck/config.jsonand.planck/config.jsonat resolution time; results cached in persistent_term;invalidate/0for cache busting before reload.config_filesapp_env (PLANCK_CONFIG_FILES) — controls which JSON files are read;config :planck_headless, :skip_json_config, truefor tests.modelsapp_env —Planck.AI.Config-format model declarations parsed to[Planck.AI.Model.t()]; replaceslocal_servers; no network at boot.- Provider atoms pre-loaded at boot via
Planck.AI.Model.providers()to avoidString.to_existing_atomfailures on lazy module load. PathListinline asPathList(internal) submodule.
ResourceStore
- Cloud models: static LLMDB catalog filtered by API key presence.
- Local/custom models: from
Config.models!()— already parsed, zero network. AppSupervisorownsResourceStore; noSessionRegistry— dropped in favour of readingteam_iddirectly from session SQLite metadata.
Session naming
Planck.Headless.SessionName— generates<adjective>-<noun>names;generate/1retries on collision;sanitize/1normalises to[a-z0-9-]+.- Session files stored as
<sessions_dir>/<id>_<name>.db;Session.find_by_id/2andfind_by_name/2use glob for O(1) lookup.
Other
DefaultPrompt(internal) — default system prompt for dynamic-team orchestrator.Moxin test deps;Planck.Agent.MockAIwired 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?/2instead ofis_map_key/2guard (MapSet is a struct, not a plain map; the guard silently never matched).
Session resume improvements
- Stable agent IDs across session resumes:
save_metadatanow persists anagent_idsmap (name→id JSON) andresume_sessionloads it, passing previous IDs tomaterialize_team,start_workers, andstart_dynamic_workerso processes restart with the same IDs they had in the original session maybe_inject_recoverysimplified: no longer needsfind_previous_orchestratorsince IDs are stable across resumes
Worker lifecycle
unfinished_workersrewrite: usesworker_unfinished?/1— a worker is considered unfinished when their most recent:usermessage (last assigned task) has nosend_responsein any assistant message that follows itsend_responsesender attribution threaded through:start_workersandstart_dynamic_workernow build asender = %{id, name}map and pass it toworker_tools/3, so every response reaches the orchestrator with full sender metadata