Skip to content

Latest commit

 

History

History
178 lines (113 loc) · 45.3 KB

File metadata and controls

178 lines (113 loc) · 45.3 KB

EvoGit — Root

Intent

EvoGit is an evolutionary software development framework built in Elixir. It models a codebase as a hierarchical Context Tree (Spatial Dimension) and evolves it through a DAG of Git commits (Temporal Dimension). AI agents recursively build and optimize software, guided by spatial contracts in per-directory CONTEXT.md files.

This is an Elixir umbrella project with two child applications:

App Directory Purpose
:evo_git ./apps/evo_git/ Core runtime — agent execution, Git interactions, CLI
:evo_dash ./apps/evo_dash/ Phoenix LiveView dashboard — real-time visualization and task management

The full design specification is documented across the CONTEXT.md tree.

Routing Table

  • ./apps/evo_git/ → Core runtime (agents, scheduler, git adapter, runtime phases)
  • ./apps/evo_dash/ → Web dashboard (LiveView pages, components, task registry)
  • Self-reflective agent / Home chat / guide overlay → ./apps/evo_git/ (agent + repo-less runtime + task-control tools) + ./apps/evo_dash/ (Home chat page EvoDashWeb.HomeLive at GET /help — the ChatGPT-style chat entry point to the self-reflective agent, reachable via the sidebar Help entry; root / maps to the Projects page; Projects page at /projects; global guide hook/panel) — see "Self-Reflective Agent (repo-less)" section below
  • ./docs/ → Long-form design records & reference documentation (notably auto-update.md, the desktop auto-update design record)
  • ./config/ → Environment-based Elixir configuration
  • ./rel/ → Mix release overlays (rel/genesis/, rel/genesis_remote/ — vm.args + env scripts per release; distribution config for SSH remote dev)
  • ./desktop/ → Tauri desktop shell (native WebView wrapper, sidecar lifecycle management)
  • ./nix/ → NixOS build support (vendor bundling helper for local desktop builds)
  • ./.github/workflows/ → CI/CD pipelines (desktop app build on release)

API Surface

Top-Level Files

File Purpose
mix.exs Umbrella Mix project — apps_path, three releases: genesis (both apps), genesis_desktop (standard mix release with include_erts, bundled as Tauri resource), genesis_remote (headless evo_git-only daemon tarball for SSH remote dev). Version is read dynamically from VERSION (single source of truth).
VERSION Single source of truth for the project version (e.g. 0.1.0). All umbrella mix.exs files read this; the desktop manifests are synced by mix bump.version.
flake.nix Nix flake — devShells.default provides a complete NixOS toolchain (Erlang/OTP 29, Elixir 1.20, Rust, Tauri v2 native deps) for local desktop app builds. packages.default builds the app via genesis.nix (nix build). apps.default runs the app (nix run).
genesis.nix Nix derivation — builds the Genesis Mix release using beamPackages.mixRelease, with pre-fetched Rustler NIFs and vendored system binaries (ripgrep, git). Called from flake.nix.
genesis-desktop.nix Nix derivation — builds the Tauri desktop app: first builds the genesis_desktop Mix release, then builds the Tauri Rust binary with rustPlatform.buildRustPackage, and wraps them together. Called from flake.nix as packages.desktop.
README.md User-facing documentation: installation, CLI usage, architecture overview
.formatter.exs Code format configuration
LICENSE Project license

CLI Interface

# Setup — guided LLM configuration wizard
mix run -e 'EvoGit.CLI.main(System.argv())' -- setup

# Genesis — create a codebase from a prompt
mix run -e 'EvoGit.CLI.main(System.argv())' -- genesis "<prompt>" [-f file] [-p path] [-R <id:>path] [-m <model>]

# Evolution — modify an existing codebase
mix run -e 'EvoGit.CLI.main(System.argv())' -- evolve "<objective>" [-p path] [-R <id:>path] [-m <model>]

# Reflect — repo-less self-reflective Q&A (no repo, no merge)
mix run -e 'EvoGit.CLI.main(System.argv())' -- reflect "<objective>" [-m <model>]

# Run — execute command-shell commands (terminal access to the shared task-control data plane)
mix run -e 'EvoGit.CLI.main(System.argv())' -- run 'ListTasks.list_tasks'
mix run -e 'EvoGit.CLI.main(System.argv())' -- run 'StartTask.start_task evolve "Fix the bug"'

Task data plane — NO config overrides. The CLI never mutates scheduler/config state. The session-level override flags (-c/--concurrency, --tool-concurrency, -r/--retries, -t/--max-turns, --max-turns-root) were REMOVED — concurrency/retries/turn caps are set persistently in config.toml; passing a removed flag prints a helpful pointer. genesis/evolve/reflect submit REGISTERED background tasks through the SAME data plane the dashboard (NodeContext.start_task) and the self-reflective shell's StartTask.start_task use — EvoGit.TaskRegistry.start_task/2 → TaskExecutor → runtime phases — then wait on PubSub for the terminal status (foreground semantics; the CLI BEAM hosts the scheduler), printing Task <id> started / Task <id> <status>.. Task opts use the data-plane key contract: :path (resolved absolute pre-enqueue), :mode STRING (new/existing genesis, simple/custom evolve — RuntimeOpts raises on atoms), :prompt/:objective. Task-level flags: -f/--file, -p/--path, -d/--mode, -b/--build-system (genesis new), --agent <id> (custom root agent from <config_dir>/agents.toml), -R <id:>path foreign repos (repeatable, read-only), -n/--node + --starting-commit (evolve), --archive. -m/--model is pure TASK-LEVEL model selection (never a scheduler override): resolved pre-enqueue against configured [[llm.models]] profiles (exact profile id → id:provider:model id segment → a profile's model string; unknown → error listing profile ids) and passed as model_id + model_id_locked: true. run '<command>' executes the self-reflective agent's shell command language via EvoGit.CommandShell.execute/2 with approval: :auto (the terminal user IS the human; the /help-chat approval gate applies only to agent-initiated commands). evogit setup remains the persistent-config wizard (file writes, not a session override). Details: apps/evo_git/CONTEXT.md + apps/evo_git/lib/evo_git/CONTEXT.md.

Custom Agents & Custom Task Mode

Users define custom agents declaratively in <config_dir>/agents.toml (pure TOML: name, description, prompt system prompt, agent_type read/read_write, delegation_level, model_id, max_turns, tools, subagents; ids auto-derived from slugified names). An optional [model_selection] script in the same file is a short user Elixir body evaluated per agent spawn (with an agent map in scope: agent_type, custom_agent_id, depth, parent_id, task_id, objective; last expression = model profile id, nil/""/false → default) — implemented via EvoGit.CustomAgents / EvoGit.Agents.Custom / EvoGit.CustomAgents.ModelSelector (compile-once stat-validated cache, never raises user-code errors). Model priority per spawn: user-locked (-m / explicit dashboard pick) → script → per-agent default → scheduler default.

The dashboard Settings page has a dedicated Agents category (custom-agents list editor + model-selection script editor, node-aware), and the task form has an Agent select plus a model "Auto (by rules)" option.

Custom task mode (the entry point for custom root agents): a 4th dashboard task mode "Custom Agent" (combined-mode string "custom_agent") maps to {:evolve, mode: "custom", agent: <custom_agent_id>} — task type stays :evolve, mode opt value "custom" (a STRING from every entry point — dashboard, CLI, and shell all pass the string through the task data plane; RuntimeOpts normalizes). EvoGit.Runtime.Evolution.run/2 routes "custom" to a custom-root path with evolve semantics (reviewable merge_and_report); a missing/empty :agent raises a descriptive ArgumentError before any repo I/O (spec-error style, mirrors Helpers.resolve_root_agent/2's unknown-id raise); unknown mode values warn + fall back to simple (legacy compatibility). CLI: evolve --mode custom --agent <id>; genesis rejects custom mode ("evolve-only"). Custom agents are root-only this version (not spawnable as subagents); delegation_level default :low. Details: apps/evo_git/CONTEXT.md + apps/evo_dash/CONTEXT.md.

Constraints

  • Umbrella structure: all deps, build artifacts, lockfile at root (./deps/, ./_build/, mix.lock)
  • Elixir ~> 1.18 required
  • Git CLI only — no libgit2 bindings
  • No source code at root — all code under ./apps/
  • Agents commit before delegating subagents (auto-commit fallback enforced)
  • Genesis stores its runtime artifacts (agent worktrees, state) under a .genesis/ directory at each repo root. This directory must be git-ignored everywhere (root .gitignore and the .gitignore that Genesis auto-writes into new repos).
  • LLM-generated code runs under platform-appropriate sandboxing (systemd-run on Linux, bwrap/bubblewrap on Linux without systemd — e.g. Docker — filesystem isolation only, sandbox-exec on macOS, direct on Windows); backend preference configurable via [sandbox] backend = "auto" | "systemd" | "bwrap" (auto tries systemd-run first, then bwrap, then no sandbox). The bwrap backend runtime-probes its environment (EvoGit.Sandbox.Bwrap.capability/0; override via app env :bwrap_capability): in rootless podman containers (container root without CAP_SYS_ADMIN) it runs in :degraded mode — --unshare-pid dropped because the kernel blocks fresh-procfs-in-nested-pid-ns, filesystem isolation retained; an environment where even the degraded shape fails is :unusable and :auto mode falls back to no sandbox. Details: apps/evo_git/lib/evo_git/sandbox/CONTEXT.md
  • No hardcoded model or username — users configure via ~/.config/genesis/config.toml
  • User config follows XDG conventions

Architecture Summary

EvoGit has two OTP applications under an umbrella:

  • :evo_git (Core Runtime): AgentScheduler GenServer managing worktree pools, LLM/tool slot management with global backoff, agent implementations (Manager, Executor, Investigator, etc.), Git adapter, and two-phase execution (Genesis → Evolution). Uses a 3-level configuration system: built-in defaults → user TOML config → session-level runtime overrides.
  • :evo_dash (Web Dashboard): Phoenix LiveView interface with project-based task management, agent tree inspector, runtime settings panel, and in-browser config editor. Uses Bandit adapter, Tailwind CSS 4 + DaisyUI, SQLite-based persistence (xqlite).

Key design: spatial context tree for routing, phylogenetic graph for temporal evolution, transient agents in isolated worktrees, multi-repo support via absolute path resolution, slot-based concurrency with LLM rate-limit backoff, multi-platform sandboxing (systemd-run on Linux, bwrap/bubblewrap filesystem isolation on Linux without systemd, sandbox-exec on macOS), and a dynamic skills system.

Development Notes

  • mix precommit — format code and run tests before committing
  • Umbrella-aware formatting — the formatter config is umbrella-aware: the root .formatter.exs declares subdirectories: ["apps/*"] and its own inputs cover only root-level files ({mix,.formatter}.exs, {config,lib,test}/**/*.{ex,exs} — never files inside apps/*); each app carries its own .formatter.exs governing its subtree (apps/evo_dash with import_deps: [:phoenix] + plugins: [Phoenix.LiveView.HTMLFormatter] covering .heex and ~H sigils, apps/evo_git a plain default). mix format / the CI format gate mix format --check-formatted run from the root therefore cover the whole umbrella.
  • mix test — execute the test suite
  • mix deps.get — fetch dependencies
  • mix compile — compile and check for errors
  • mix bump.version <new-version> — bump the project version from the single source of truth (VERSION) and sync it to the Tauri/Cargo desktop manifests in one step

Internationalization (i18n) Development Policy

The web dashboard (:evo_dash) is internationalized with Gettext — all user-facing strings in LiveViews, components, helpers, and templates are wrapped with gettext/1,2. The CLI / :evo_git core is deliberately NOT internationalized (English-only). During development, agents do NOT need to care about PO files or translations:

  • Never run mix gettext.extract, mix gettext.merge, or mix translate during feature work. The POT/PO files and all translations are maintained centrally and updated at release time — every release runs the translation pipeline once, translating all untranslated texts in a single run (mix gettext.extractmix gettext.mergemix translate apps/evo_dash/priv/gettext/default.pot all; AI-powered via deepseek-v4-flash).
  • Just keep developing normally and write gettext-wrapped strings. When a string's meaning needs anchoring for the release-time AI translator (ambiguous short labels, domain jargon, etc.), write a Chinese comment next to the gettext call explaining the intended meaning — that is the ONLY i18n-related work expected during development.
  • Full details: apps/evo_dash/CONTEXT.md → "Internationalization (i18n)".

Versioning

The project version has a single source of truth: the root VERSION file. The three umbrella mix.exs files (./mix.exs, apps/evo_git/mix.exs, apps/evo_dash/mix.exs) all read the version dynamically from VERSION via a shared version/0 helper. The desktop manifests (desktop/src-tauri/tauri.conf.json and Cargo.toml) carry a literal copy that must stay in sync.

To bump the version, run:

mix bump.version 0.2.0

This updates VERSION, tauri.conf.json, Cargo.toml, Cargo.lock, and README.md (shields.io badge) in one command, then prints next-step guidance (compile, commit, tag). The task interactively asks whether to commit the bumped files (only the touched files are staged, never git add -A), and finally asks whether to auto-generate the changelog for the new version — accepting delegates to mix changelog. The CLI also supports --version / -v to print the version at runtime.

AI changelog generationmix changelog <version> [--from <ref>] [--to <ref>] [--model <id>] [--file <path>] (Mix.Tasks.Changelog, apps/evo_git/lib/mix/tasks/changelog.ex): PR/merge-aware collection — walks the first-parent line of the range (git log --first-parent, since the last tag via git describe --tags --abbrev=0, falling back to full history when no tag exists), turns each merge commit into one change carrying exactly the branch commits it brought in (git log --no-merges <merge>^1..<merge>) and each non-merge first-parent commit into a single-commit change; version-bump (^Bump version to) and mechanical noise (^Update mix hash, ^Update CONTEXT.md) commits are filtered out. Docs-only changes never appear in the changelog (README rewording, docs/CONTEXT.md edits, comment-only work): collection carries only commit subjects/bodies — never file paths — so their exclusion is prompt-driven — stage 1 summarizes only the user-facing CODE parts of mixed changes and replies with the fixed __NO_USER_FACING_CHANGES__ marker for entirely non-user-facing changes (dropped by summarize_prs/3 before stage 2), and stage 2 is told to omit any remaining non-code entry; a release whose changes are all non-user-facing short-circuits like the empty-range path and leaves CHANGELOG.md untouched. Summarization is two-stage map-reduce with an LLM (deepseek:deepseek-v4-flash via ReqLLM.stream_object, --model overrides): stage 1 produces one concise summary line per change, stage 2 aggregates the summaries into a user-facing Keep-a-Changelog section (## [<version>] - <date> with Added/Changed/Fixed/Removed/Security/Deprecated categories), merging related changes across PRs/merges into single entries — separate merges that add, fix, and improve the same feature collapse into ONE entry describing the net user-visible state (e.g. "Add feature xyz"), choosing the most representative category (prompt rule in build_aggregate_prompt/2); and maintains the root CHANGELOG.md — created with a # Changelog title plus an empty ## [Unreleased] section when missing, otherwise the new section is inserted after the header, or an existing same-version section is replaced in place (re-runs never duplicate). After writing it asks whether to commit the changelog file (only that file is staged). The pipeline is routed through three app-env test seams: :changelog_summarizer (whole pipeline), :changelog_pr_summarizer (stage 1, per change), :changelog_aggregator (stage 2). Release-time tool, like mix translate.

Subsystem Overviews

The sections below are condensed summaries of major subsystems. Each carries routing pointers to the child CONTEXT.md file that holds the full detail.

Runtime Data Directory (tasks.sqlite)

The SQLite task database (tasks.sqlite) lives in the platform data directory, resolved at runtime by EvoGit.Platform.data_dir/0 (apps/evo_git/lib/evo_git/platform.ex) — NOT via Tauri's path_resolver/app_data_dir. Paths: macOS ~/Library/Application Support/genesis/tasks.sqlite; Linux $XDG_DATA_HOME/genesis/tasks.sqlite (default ~/.local/share/genesis/tasks.sqlite); Windows %APPDATA%\genesis\tasks.sqlite (default ~\genesis\tasks.sqlite). Computed in EvoGit.Application.start/2 → passed to EvoGit.Store (SQLite WAL mode, sidecars beside it); EvoGit.TaskRegistry.init/1 mirrors the same resolution. Overrides, highest → lowest: the app env config :evo_git, :data_dir (only config/test.exs:29 sets it — test isolation must stay above any file config) → the config.toml [data] dir key (config :evo_git reads it via EvoGit.Platform.data_dir/0, absolute or ~-relative path, invalid value → Logger.warning + fall back, boot-time only — takes effect at next boot, no migration of an existing DB) → the platform default, indirectly influenced via HOME/XDG_DATA_HOME/APPDATA. The [data] dir override relocates the whole data dir coherently (Store sqlite + WAL sidecars, TaskRegistry mkdir, mix migrate.store, logs/backend.log, nix/sandbox caches, remote-bootstrap cache, self-reflective genesis-source) because all consumers resolve through EvoGit.Platform.data_dir/0. Per-repo .genesis/ worktree artifacts are repo-root-fixed and NOT affected. The desktop log file (<data_dir>/logs/backend.log) and the genesis_remote daemon use the same directory. Details: apps/evo_git/CONTEXT.md + config/CONTEXT.md.

Desktop Backend Port & Orphan Prevention (dynamic port)

The genesis_desktop backend port is never fixed. At startup the Rust shell resolves it once (desktop/src-tauri/src/main.rs resolve_backend_port): an explicitly-set PORT env var is honored only when that port is currently free (bind probe), otherwise a random free ephemeral port is picked — even a stale PORT=9999 from a zombie can no longer wedge the app. The same resolved port drives the sidecar env, the BackendManager readiness probe, the watchdog poll/error-page URL, and the WebView URL (window created with WebviewUrl::External). Healthy-boot re-navigation is navigate_after_ready (bounded retry, ~20 × 250ms) — the only healthy-boot mechanism; the watchdog covers crash-recovery navigation only, and there is deliberately no dashboard-liveness heartbeat/keeper thread (a wall-clock freshness window cannot distinguish a dead page from a sleeping machine — removed by design). Tray Quit waits for user confirmation (no timeout force-quit): quit-requested re-emitted at +500..+8000ms, then waits indefinitely for the dashboard's confirm dialog; the only immediate-quit exception is the backend-already-down path. Orphan prevention (TCP lifetime pipe — zero polling): the shell binds a TcpListener on 127.0.0.1:0 and passes the port as EVOGIT_LIFETIME_PORT; EvoDash.DesktopLifetime (apps/evo_dash/lib/evo_dash/desktop_lifetime.ex, wired only when EVOGIT_DESKTOP=1 AND the var is set) blocks on :gen_tcp.recv(sock, 0, :infinity) — the first socket close means the shell is gone → System.stop(0) (bounded connect retries; test seam :parent_stop_fun). The 9999 fallback in config/runtime.exs is a safety net only for manually-launched desktop releases. Details: desktop/CONTEXT.md + desktop/src-tauri/CONTEXT.md + apps/evo_dash/CONTEXT.md.

Task Cancellation Model (graceful cancel vs force kill)

Task cancellation has two distinct actions (both node-aware via the RemoteNode RPC chain): graceful cancelEvoGit.TaskRegistry.cancel_task/1 (also RemoteAPI/RemoteNode/NodeContext): sets the task to :cancelling, sends each running agent a save-work user message + sets AgentState.cancel_requested; the wrapper is NOT killed — the phase finishes normally (3-turn grace budget in Runner.loop/1) and the final-status mapping force-persists :cancelled with result/archive/usage preserved (reviewable like a completed task). :pending tasks cancel immediately; :cancelling is idempotent; new root spawns are blocked while cancelling; escalation via force_kill_task/1 works from :cancelling. Force killTaskRegistry.force_kill_task/1: kills all agents + the wrapper, persists :failed with result nil'd; works from :running and :cancelling. :cancelled means ONLY gracefully-cancelled tasks. Dashboard UI (apps/evo_dash Tasks page): visible Cancel button ([:pending, :running]) with a warning modal; force kill in the task card's three-dot dropdown ([:running, :cancelling], "Danger zone"); :cancelling renders as violet badge/label/accent, appears in sidebar active-tasks lists and the status filter. Details: apps/evo_git/CONTEXT.md + apps/evo_dash/CONTEXT.md.

Self-Reflective Agent (repo-less, chatbot-style)

A special agent with no repository of its own — the user talks to it about the Genesis system itself; it investigates the Genesis source read-only, controls tasks, and guides the user through the dashboard. It runs through the EXISTING agent machinery (Runner loop, ToolDispatch, AgentScheduler.run_agent, LLM retries/slots/compression) marked repo-less via spec.opts[:repo_less] = true — no worktree, no git ops, no subagent spawning; Tools.execute/5 blocks write tools (defense-in-depth, since its repo_path points at the REAL Genesis source). Core: EvoGit.Agents.SelfReflective (agent_type :read, delegation_level :low, read-only tools + gated WebSearch + control/guide tools + CompleteTask; NO shell/write tools) + EvoGit.Runtime.SelfReflective (run/1,2; source_root/0 env chain :self_reflective_source_rootGENESIS_SOURCE_ROOTFile.cwd!()). Task type :reflect (mode string "reflect", NO :pathTaskExecutor.execute_task(:reflect) deliberately bypasses RuntimeOpts.build_common_runtime_opts, which KeyErrors without :path), CLI reflect command. Task-control tools in apps/evo_git/lib/evo_git/agent/tools/: list_tasks, get_task, start_task (types validated against a fixed map — never String.to_atom on LLM input), cancel_task, force_kill_task, delete_task, spawn_investigator (deterministic read-only repo probe — real bounded investigation, no LLM agent spawn), guide_user (best-effort PubSub broadcast, never raises). Dashboard: the Home chat page (EvoDashWeb.HomeLive at GET /help; root / maps to Projects at /projects) — a ChatGPT-style chat that starts :reflect tasks via EvoDash.NodeContext.start_task(node, :reflect, mode: "reflect", objective: ...) with NO :path; assistant responses render as mini task-card chat messages reusing agents/tasks-page code (live EvoDashWeb.Helpers.task_status_badge status transitions, streamed text, and a collapsible zero-JS "Thought process" listing the agent's context history via EvoDashWeb.AgentsLive.ToolCallDisplay); chat state persists across LiveView sessions in memory via EvoDash.ChatHistory (ETS-backed GenServer, shape-agnostic per-chat state) + EvoDashWeb.HomeLive.ChatState (shape owner); global guide overlay via EvoDashWeb.LiveHooks.Guide hook + JS Guide hook. Details + test coverage: apps/evo_git/CONTEXT.md ("Self-Reflective Agent (repo-less)") + apps/evo_dash/CONTEXT.md.

Merge Conflict Check & Auto-Resolution (Review Page)

The review page checks merge cleanliness asynchronously (GitHub-style) and offers agent-driven auto-resolution. Async merge check: on review page load (and when the merge-target selector changes), ReviewLive spawns a supervised Task (EvoDash.TaskSupervisor) calling EvoDash.NodeContext.check_merge/4EvoGit.Review.check_merge/3 — a non-mutating dry-run: resolves both refs (identical SHAs → :clean), then runs git merge-tree --write-tree --name-only --no-messages <branch_sha> <target_sha> (requires git ≥ 2.38). No worktree is ever created, nothing is added under <repo>/.genesis/, refs untouched — nothing to clean up. Exit 0 → :clean; exit 1 → {:conflict, files}; exit 128 → error. UI (ReviewComponents.Actions.action_buttons/1 + ReviewLive.MergeCheck): merge-status block above the Merge button — checking → clean (green) → conflict (amber, conflicted-file list + Auto-resolve conflict button with phx-confirm) → error (renders nothing, old behavior). Auto-resolve (counts as resumed): marks the original task review_status: :continued, then starts a new :evolve task with opts [path:, mode: "simple", objective:, starting_commit:, merge_from: <task_id>, merge_target: <branch>]; EvoGit.TaskRegistry.MergeContext.apply_merge_context/4 strips the merge keys, loads the previous task, overrides starting_commit with prev commit_sha, carries over foreign_repos, and prepends a resolution-hints context block (commit before delegating; inspect git log/diff base..end and base..target; incremental milestone merges for hard conflicts). The merge agent runs git merge <target>, resolves, commits; merge_and_report produces a reviewable genesis/agent_* branch whose merge is clean. Test seam: Application.get_env(:evo_dash, :merge_check_runner); the spawned task rescues exceptions → {:error, :check_failed} so the status never wedges at :checking. Details: apps/evo_git/CONTEXT.md + apps/evo_dash/CONTEXT.md.

Native Directory Picker (wx backend)

The dashboard's directory picker (Browse buttons on the project/new-project/foreign-repo pickers) is implemented on the Elixir backend with Erlang's :wx (wxDirDialog), NOT via Tauri. Flow: the JS DirectoryPicker hook pushes "directory_pick"ProjectsLive (current node local only) → EvoDash.DirectoryPicker (a GenServer in apps/evo_dash, serializes wx dialog usage) runs the modal → result pushed back as "picker_result:<picker_id>"; wx picks are absolute paths and auto-submit the forms. Tauri's pick_directory command is not used (unstable: Windows invoke fails after picking, macOS NSOpenPanel never presents). When wx is unavailable (headless server, remote node, OTP built without wx) the hook shows the manual-entry fallback. Because wx is not a dependency of any umbrella app, it is listed explicitly in the applications: list of the genesis and genesis_desktop releases in ./mix.exs (wx: :load) — genesis_remote intentionally stays wx-free. CI note: the wx NIFs link against wxWidgets 3.2 sonames, so AppImage bundling requires the wx 3.2 runtime (plain apt install of libwxbase3.2-1t64 etc. on ubuntu-24.04, x64-gated — no PPA). Details: desktop/src-tauri/CONTEXT.md + apps/evo_dash/CONTEXT.md.

ReqLLM Finch Connection Pool (LLM HTTP concurrency)

The ReqLLM Finch pool is sized at boot (config/runtime.exs:23-72): pool count = EvoGit.ReqLLMPool.desired_count(total_concurrency) — the max(total + 2, 8) formula lives ONLY in desired_count/1 (apps/evo_git/lib/evo_git/req_llm_pool.ex, single source of truth shared with runtime reconciliation; do NOT inline it). total_concurrency = max(Σ per-profile concurrency, scheduler.default_llm_max_concurrency) from EvoGit.Config.resolve() (per-profile default 3; falls back to the scheduler default alone when no [[llm.models]] profiles exist). Configured via the full finch: override form (name: ReqLLM.Finch, pools: %{default: [protocols: [:http1], size: 2, count: stream_pool_count, start_pool_metrics?: true]}) with stream_pool_timeout: 300_000 and stream_pool_strategy: {Finch.Pool.Strategy.RoundRobin, round_robin} kept top-level (both read at CALL time). Pools are PER ORIGIN: Finch materializes a separate pool per origin from the single :default template, lazily; capacity = count × 2 concurrent HTTP/1 streams per origin (size: 2), NOT global — sum-based sizing is the provably safe upper bound (any single origin's demand ≤ total concurrency). Runtime reconciliation is grow-only (EvoGit.ReqLLMPool): resized on config changes (AgentScheduler.update_config, RemoteAPI.reload_config, per-task -m) and on Finch "excess queuing" errors (bump_for_excess_queuingmax(ceil(effective * 1.5), 8)); the module NEVER shrinks (Finch.set_pool_count kills in-flight streams on shrink). start_pool_metrics?: true is REQUIRED for Finch.get_pool_status(ReqLLM.Finch, :default) to enumerate origins (without it reconciliation is a silent no-op); pools are lazy so set_pool_count returns {:error, :not_found} until an origin is used — reconcile/bump no-op gracefully. A runtime default_llm_max_concurrency override ALSO raises the scheduler's live per-model LLM slot pools (floor semantics, never lowers explicit profile concurrencies). Details: apps/evo_git/CONTEXT.md + apps/evo_git/lib/evo_git/agent_scheduler/CONTEXT.md.

Peak/Off-Peak Hour Concurrency Scheduling (LLM models)

Each [[llm.models]] profile may declare five optional peak/off-peak fields: peak_concurrency (concurrency during peak windows; 0 = hard pause — zero LLM slots, the explicit 0 is never raised by the default_llm_max_concurrency floor), peak_hours (list of %{start: "HH:MM", end: "HH:MM"} daily windows, half-open [start, end), start > end = overnight wrap; missing/[] → disabled; each window may optionally carry days — a list of day identifiers "mon".."sun" and/or keywords "weekdays" (mon–fri) / "weekends" (sat–sun) scoping the window to those days only, absent = every day), off_peak_days (profile-level list of the same day identifiers/keywords — on those days the profile is off-peak the ENTIRE day: normal concurrency 24/7, every peak_hours window suppressed, peak_concurrency incl. the hard-pause 0 never applies; off_peak_days wins over window days; e.g. DeepSeek weekends-off-peak: off_peak_days = ["weekends"]), and timezone (optional IANA name, DST-aware; absent/blank → server local wall clock, never UTC; validated by PeakHours.validate_timezone/1). EvoGit.PeakHours (apps/evo_git/lib/evo_git/peak_hours.ex) is the pure single source of truth for parsing/validation and runtime math (validate_days/1 — day identifiers/keywords, case-insensitive → canonical atoms; in_peak?/2 + day-aware /3, next_transition/2 + day-aware /3 — midnight day-boundary flips ARE transitions, effective_concurrency/2 + tz-aware /3, wall_clock_in/2); the config schema delegates all checks to it — do NOT re-implement window logic elsewhere. Requires the tz database at boot (Calendar.put_time_zone_database(Tz.TimeZoneDatabase) in EvoGit.Application.start/2). EvoGit.PeakHourEngine (supervised after the scheduler) dynamically flips scheduler concurrency on start/timer (day-aware next transition + 100ms, 6h safety-net cap; profiles with ONLY off_peak_days contribute no transitions — no needless mid-day wakeups)/"scheduler_config" PubSub: computes the floored effective map — max(effective, default) per model, EXCEPT explicit 0 stays 0 (fixed point, never loops) — and applies via AgentScheduler.update_config(model_concurrency: map) only when it differs. 0-capacity blocks like paused: slot requests ENQUEUE in the per-model waiting queue instead of failing fast (caller blocks until a grant or purge); when capacity returns, the end-of-update Slots.grant_pending_on_resume/1 sweep grants queued waiters; in-flight holders are never evicted; cancellation still works (force-kill purges queued agents). Clock seams for tests: :peak_hours_now_fun / :peak_hours_utc_now_fun; public check/0. Dashboard: Settings → model profile editor edits the fields (absent/empty peak fields serialize to TOML as absent) — off_peak_days + per-window days render as chip selectors (mon–sun, weekdays, weekends). Details: apps/evo_git/CONTEXT.md + apps/evo_dash settings CONTEXT.md.

SSH Remote Development

Genesis supports a VSCode Remote-SSH-like workflow: a lightweight headless daemon runs on a remote server, and the local Phoenix dashboard controls it over an SSH tunnel via Erlang distribution. Architecture: the remote daemon (genesis_remote release — evo_git-only, include_erts, launched via systemd-run --user on Linux / launchctl + launchd plist on macOS, survives dashboard disconnection) uses EPMD-less distribution on a pinned port (default 9000) via EvoGit.EpmdDist (-epmd_module Elixir.EvoGit.EpmdDist in rel/vm.args.eex); the local dashboard establishes an SSH port-forwarding tunnel (ssh -L <local_port>:127.0.0.1:<remote_port> -N), auto-enables local distribution on-demand (EvoGit.Distribution.enable_for_remote/1), then Node.connect/1 over the tunnel (EvoGit.RemoteConnection GenServer manages the lifecycle). Data access: :erpc.call/5 to EvoGit.AgentScheduler.RemoteAPI (native BEAM terms, no serialization); PubSub uses the cluster-aware PG2 adapter (:pg) so remote broadcasts propagate to the local dashboard. Dashboard RPC UX: ALL node-aware data loads run OUTSIDE the LiveView process via EvoDash.TaskSupervisor + self-message + stale-guards — no LiveView ever blocks on a cross-node :erpc call, and there is NO periodic polling anywhere (fully push-based PubSub; event contract + per-page consumer details: apps/evo_dash/lib/evo_dash_web/live/CONTEXT.md). Bootstrap vs Connect are separate: bootstrap SCPs the release tarball, extracts, and launches the daemon (first-time setup); connect only establishes the tunnel + distribution link. All SSH ops use CLI ssh/scp via Port.open (no Erlang :ssh); remote commands must be argv arrays via run_ssh_command/3 (never quote-wrapped spawn strings) and are bash-wrapped (/usr/bin/env bash -c, EvoGit.RemoteBootstrap.bash_wrap/1) since the remote login shell is never assumed to be bash. Multiple simultaneous connections: each SSH target gets a unique per-target node name (genesis_remote_<id>@127.0.0.1) + per-target systemd unit/launchd plist label, so Node.connect can address multiple hosts; the tunnel uses a dynamically-assigned local port forwarding to the remote daemon's fixed port 9000, and the local port is polled for readiness (wait_for_tunnel/4, 10s budget) BEFORE Node.connect/1. Remote asset download is deterministic and network-free: EvoGit.RemoteBootstrap.download_url/1 returns the direct https://genesis.evox.group/dl/genesis_remote_<platform>.tar.xz URL (Cloudflare "smart download" endpoint); glibc is the default and ONLY published Linux variant — asset names are NEVER suffixed (musl disabled; do NOT reintroduce asset listing/matching or suffix checks — the desktop .tar.gz and remote .tar.xz share the _<os>_<arch> shape). Key modules: EvoGit.EpmdDist (implements erl_epmd; :persistent_term node→port registry), EvoGit.Distribution, EvoGit.RemoteConnections (TOML persistence ~/.config/genesis/remote_connections.toml), EvoGit.RemoteConnection, EvoGit.AgentScheduler.RemoteAPI, EvoDash.NodeContext, EvoDashWeb.LiveHooks.NodeAware, EvoDashWeb.NodeSelectorComponent. Details: apps/evo_git/CONTEXT.md.

Desktop App Build Pipeline

.github/workflows/build-desktop.yml automatically builds native desktop app installers on every GitHub release (trigger: version tag push v* or manual workflow_dispatch). The pipeline itself creates the release: all platform builds finish first, then the publish-release job creates a draft release with every artifact attached and flips it public with gh release edit --draft=false as its FINAL step — so releases/latest never points at a release whose assets are still uploading. Build process: each platform job runs mix release genesis_desktop (native, include_erts bundles host ERTS) → copies the release dir to desktop/src-tauri/resources/genesis-backend/tauri build; the headless genesis_remote release is also built and uploaded as a .tar.xz tarball. Three releases defined in mix.exs: genesis_desktop (full, Tauri-bundled), genesis_remote (headless evo_git-only, bakes config: [evo_git: [remote_release: true]] → EPMD-less distribution), base genesis. Five job groups on native runners: build-linux (desktop, ubuntu-24.04 x64/arm64 → .deb/.rpm/AppImage/.tar.gz; AppImage excluded on ARM64 — linuxdeploy is x86_64-only; Flatpak not built), build-linux-remote (dedicated glibc genesis_remote on ubuntu-22.04 x64/arm64 — older glibc 2.35 = wider host compatibility; musl build disabled, revival notes in .github/workflows/CONTEXT.md), build-macos-arm64 (macos-15.dmg/.app), build-windows-x64 (windows-2022.msi/.exe NSIS), publish-release. Toolchains pinned via OTP_VERSION/ELIXIR_VERSION env vars (OTP 29, Elixir 1.20.4); the Tauri CLI comes from the npm package @tauri-apps/cli (prebuilt binaries incl. linux-arm64-gnu — never cargo install, a transitive zune-jpeg dep has a known compile bug); ARM partner runners get an ImageOS env fix before erlef/setup-beam; ripgrep/git (or MinGit on Windows) are vendored into apps/evo_git/priv/vendor/{platform}/; caches: Mix deps, Mix build, Rust target, Tauri CLI. Details: .github/workflows/CONTEXT.md.

Auto-Update / Push-Update (tauri-plugin-updater v2 — implemented)

The desktop auto-update system is implemented (full design record: docs/auto-update.md). Architecture: official tauri-plugin-updater v2 in the Rust shell (NOT custom, NOT Sparkle); two-phase model (check/download/verify is safe anytime; apply is manual-click only and gated on task idle); feed = static latest.json served via the genesis.evox.group /dl/ Cloudflare proxy (mainland-China reachable — GitHub Releases is unreliable/blocked there; the manifest's per-platform payload urls also point at the proxy, and the inline .sig signature rides inside the proxied manifest, so feed + payload + sig all flow through the proxy). GitHub Releases remains the upstream upload target. Accepted /dl URL forms: /dl/<asset> and /dl/latest/<asset> (the latest alias = GitHub's latest release) resolve against the latest release, the tag-scoped /dl/<tag>/<asset> resolves against that exact tag (GitHub's releases/download/<tag>/<asset> shape), and /dl//dl/ redirect to the docs site's /#download. Shipped artifacts deliberately use the unversioned form only — the tag-scoped form lives in the worker source in the external genesis-doc repo and is live only after a manual pnpm deploy, so switching the app/CI to it before then would 404 (a manifest payload URL is fetched at update-check time by already-installed apps). Rust shell (desktop/src-tauri/): plugin 2.10.1; tauri.conf.json has bundle.createUpdaterArtifacts: true + plugins.updater (endpoints → https://genesis.evox.group/dl/latest.json primary + GitHub releases/latest/download/latest.json fallback — tried in order, real minisign public key baked since commit 4281ae8aa); commands check_update (→ up_to_date|available|not_configured|not_available|error, current_version always present), download_update, begin_update (sets the watchdog update-intent flag; on backend exit 0 the watchdog installs the staged payload and relaunches — Windows: NSIS self-relaunch; Linux: AppImage backup-rename + unpack; macOS: .app swap). Dashboard (apps/evo_dash): EvoDash.UpdateStatus state hub (broadcasts {:update_status, state} on PubSub topic "updates"); global UpdateStatus on-mount hook + JS bridge — checks on startup (~30s delay) + ~6h interval, auto-downloads after a successful check, drives the sidebar notification dot on the System nav item; SystemLive "Software Update" card (states checking/up_to_date/available/ready/error/applying). Apply: "Restart & Update" → idle gate TaskRegistry.list_task_ids([:running, :pending, :cancelling, :finalizing]) == [] (node-aware); busy → modal with Defer / "Apply & gracefully stop tasks" (graceful-cancel all: results preserved as :cancelled + reviewable) / user-warned force_kill_task/1 fallback; then begin_updateSystem.stop/0 from inside the BEAM → watchdog installs + relaunches. Gated to desktop mode + local node. Linux non-AppImage installs (deb/rpm/portable) are notify-only. CI (.github/workflows/build-desktop.yml): minisign signing key-guarded — TAURI_SIGNING_PRIVATE_KEY/_PASSWORD env; when absent a bash guard appends --config '{"bundle":{"createUpdaterArtifacts":false}}' (tauri hard-fails without the key); macOS job staples the .app then re-creates + re-signs the updater .app.tar.gz; publish-release generates dist/latest.json (version = tag minus v, per-platform {url, signature} with FULL .sig content, payload urls → https://genesis.evox.group/dl/<unversioned-filename>; platforms darwin-aarch64 / linux-x86_64 / windows-x86_64 only; linux-arm64 deliberately excluded — updater only supports AppImage on Linux). Activation state — keypair done, signed payloads + CI secrets remain: current builds never return "not_configured"; the remaining blocker is per-platform signed updater payloads reaching the release (requires the two repo secrets; without them no platform entries appear — e.g. live v0.10.9 has darwin-aarch64 + windows-x86_64 only, so Linux desktop reports "No auto update on this platform"). Remaining external blockers/deferred: Windows Authenticode cert, Linux apt/dnf repos + GPG, remote-daemon (genesis_remote) update flow, staged rollout/beta channels. Details: desktop/src-tauri/CONTEXT.md + .github/workflows/CONTEXT.md + apps/evo_dash/CONTEXT.md.

NixOS Local Build

A flake.nix at the repository root supports NixOS local builds: nix develop (dev shell with Erlang/OTP 29, Elixir 1.20, Rust, webkitgtk-4.1) and nix build .#desktop (store app: the Tauri binary resolves the backend release via sidecar_path::resolve_launcher from <exe_dir>/resources/genesis-backend, the wrapper puts the GTK/WebKit/tray runtime stack on LD_LIBRARY_PATH, and genesis.nix bakes a deterministic releases/COOKIE). Design decisions: no cargoHashgenesis-desktop.nix vendors Rust deps via cargoLock.lockFile = ./desktop/src-tauri/Cargo.lock (updating Cargo.lock never requires a hash edit; only git deps would need cargoLock.outputHashes); both derivations read version = lib.trim (lib.fileContents ./VERSION) from the repo-root VERSION file (never hardcode); the only manual flake hash is mixFodDeps.hash in genesis.nix (when mix.lock changes). Known issues: tauri-build 2.x validates at compile time that every bundle.resources path exists — genesis-desktop.nix symlinks the Mix release into the crate source in a preBuild hook; checkPhase can fail with EACCES on copied 0444 resources (tauri-build's fs::copy preserves source mode bits) — fixed by a postBuild chmod -R u+w target/.../release/resources hook (do NOT work around with doCheck = false — that silently drops the 41 cargo unit tests). The GUI needs a writable XDG_RUNTIME_DIR for the tray icon. Details: desktop/CONTEXT.md + nix/CONTEXT.md.

Research Notes: PDF/DOCX → Plain Text Extraction (for LLM objective prompts)

Status: plain-text, .docx, AND .pdf are IMPLEMENTED — as a FRONTEND feature in EvoDash.AttachedFile (apps/evo_dash/lib/evo_dash/attached_file.ex), with ex_pdf declared as a plain Hex dep of :evo_dash (pure BEAM, zero runtime deps, no release applications: changes needed). .txt/any text read verbatim; .docx extracted with OTP :zip + regex; .pdf extracted to Markdown via the pure-BEAM ex_pdf reader (each page a ## Page N heading with a visible conversion note; password-protected → {:invalid, ...}, scanned/image-only → {:empty, ...}, no OCR). The core :evo_git stays clean by design: EvoGit.PromptFile (apps/evo_git/lib/evo_git/prompt_file.ex, CLI get_input/2) reads plain text only and rejects non-UTF-8 binaries/PDFs/DOCX with {:error, {:not_text, ext}} — the genesis_remote release (evo_git only) deliberately ships no ex_pdf (files are read on the local frontend node, not the remote server). The objective stays a plain String.t() end-to-end; conversion happens only at the dashboard's objective-editor attach flow ("+" button on the objective box → native file dialog → appended Markdown/text). The ReqLLM-native file attachment alternative was investigated (Aug 2026) and NOT implemented (provider matrix too uneven: only OpenAI/Gemini/Anthropic/OpenRouter handle :file parts; DeepSeek/Qwen/etc. silently mis-encode binary as image_url data-URIs). Recommended shape was tiered: (1) zero-dep :zip+regex DOCX — DONE; (2) pure-BEAM PDF via ex_pdf — DONE; (3) optional pdftotext upgrade path via EvoGit.Executable.resolve — remains future; (4) ReqLLM-native attachments — NOT implemented. Details: apps/evo_dash/CONTEXT.md.