|
root / VISION.md
VISION.md markdown 215 lines 14.5 KB

repoman — Vision Doc

Status: v0.4.4 shipped 2026-05-11
Origin: Bash prototype at ~/.local/bin/repoman (~200 LOC), April 2026. Productized rewrite in reef-lang began v0.1 (2026-04-29); current release is v0.4.4.


Vision sentence

repoman is a safety harness for AI-assisted development: it isolates each project inside its own Incus (LXC) container, bind-mounts the repo into it, and mirrors everything to NFS — so AI coding agents can work with broad permissions inside the container while the host OS, sibling projects, and the source repository on disk stay protected.

Why this exists

AI coding agents — Claude Code, Aider, Cline, Continue, Gemini CLI, and the wave of others — are dramatically more productive when granted broad permissions. They are also dramatically more capable of "fixing" things you did not ask them to touch: editing the wrong file tree, running install scripts against the host, mass-replacing across a repo, deleting paths they misread, pushing stray commits.

The original problem (which seeded the bash prototype): run Claude Code with --dangerously-skip-permissions without taking the host down with it. The solution: per-project Incus containers + bind-mounted repos + NFS backup + ZFS snapshots on the file server. Doing it manually with raw incus and rsync works but accretes ad-hoc shell scripts. repoman makes the workflow first-class and turns the safety properties into invariants of the tool, not habits of the operator.

What "safety harness" actually means

  1. Host OS sealed off. Containers run with their own root filesystem, init, package manager, services. An agent can apt install, mangle /etc, scribble into /usr, install systemd units — none of it crosses the boundary. Worst case is "throw away the container."
  2. Projects isolated from each other. Every project gets its own container in the repoman Incus project namespace. An agent in container A cannot read or write container B's files. No accidental cross-edits between sibling repos, no shared node_modules corruption, no rogue file watchers.
  3. Source code preserved. The repo lives on the host at ~/repos/<name> and is exposed to the container via a uid-shifted bind mount (shift=true, since v0.4.2). repoman remove deletes the container — never the repo. The confirmation prompt spells out exactly what gets removed (v0.4.3).
  4. Backup as recoverability. repoman sync rsyncs every repo to NFS. Run that NFS on ZFS and snapshots give you point-in-time history. If an agent does mangle the working tree, you have last night's copy.
  5. Authenticated agent state shared safely. The vendor profile library lets you bind-mount things like ~/.claude (auth + history + plugins) into every container — one login, every project — without exposing SSH keys, browser cookies, or shell config.

Audience

Homelab developer running:

  • Incus for containerization (Linux-only).
  • Local SSD/NVMe for active project working copies (latency matters for the agent's iteration loop).
  • NFS-mounted remote storage for durable backup (ideally ZFS-backed, for snapshot history).
  • One or more AI coding agents (Claude Code, etc.) that they want to grant generous permissions to.

One developer, multiple projects. Not multi-user, not multi-host coordination.

Realistic audience size: small. Niche tool serving its narrow audience well > generic tool nobody loves.

Mission (re-anchored in v0.4)

repoman has exactly four jobs:

  1. Provision per-project Incus containers that bind-mount the user's git/hg repos (repoman new).
  2. Backup repos to NFS via rsync (repoman sync).
  3. Share host-side resources (configs, agents, runtimes) into containers via Incus profiles (repoman profile).
  4. Operate the above with quality-of-life subcommands (list, status, remove, shell, setup).

What repoman is NOT:

  • A host-configuration tool. It does not install or configure host services (ollama, hermes, kernel modules).
  • A container-image builder. It uses upstream Incus images; image curation is somebody else's tool's job.
  • An arbitrary install-script runner inside containers. Per-container provisioning is the user's shell script in their project repo.

The v0.3 attempt to bind-mount hermes from the host failed because hermes pins to a uv-vendored host-only Python. The v0.4 brainstorm explored image management to fix that, then rejected it as ongoing maintenance overhead. The conclusion: agents fall into two categories — bind-mountable (claude works) and non-shareable (hermes needs per-container install). repoman does the bind-mountable case via profiles; the rest is shell-script territory in the user's project repo.

Differentiators (vs rolling your own scripts)

  1. Incus project for ownership. All repoman-managed containers live in the incus project create repoman namespace — clean scoping, no leakage into the user's other Incus work, easy to nuke the entire estate.
  2. Single tool covering container lifecycle + project conventions + backup + profile management. Existing tools each cover one slice (incus for containers, restic/borg for backup, distrobox for dev shells); none do all four with conventions tuned for "AI-agent-in-a-Linux-VPS on a homelab."
  3. Opinionated backup story. rsync mirror + autofs-aware mount checks + ZFS snapshots assumed on the file server. No bespoke history layer — ZFS is the time machine.
  4. Profile library with vendor/user split. Shareable host-side things (auth state, runtime binaries, dotfiles) are first-class via Incus profiles. Vendor ships sensible defaults; user shadows them in ~/.config/repoman/profiles.d/. ${HOST_LAN_IP}, ${USER}, ${HOME} substituted at install time.
  5. Both interactive and flag-driven. Same code path serves a guided wizard and a shell-scriptable CLI.
  6. Native single-binary distribution via reef compiling to native code. No runtime install for users.

Subcommands as shipped (v0.4.4)

Subcommand Purpose
repoman setup [--non-interactive] First-time host bootstrap. Creates Incus project repoman, detects host LAN IP, writes initial registry, ensures ~/.config/repoman/profiles.d/ exists. Idempotent.
repoman profile {list,install,diff,remove,show} Manage Incus profiles from /usr/local/share/repoman/profiles/ (vendor) and ~/.config/repoman/profiles.d/ (user shadow). Renders ${HOST_LAN_IP} / ${USER} / ${HOME} at install time.
repoman new <name> [--repo <dirname>] [--image <img>] Create a managed container. Pre-validates profiles are installed, bind-mounts the repo with shift=true, applies profiles, restarts. Interactive form prompts for unset values.
repoman list Print a table of managed containers: name, repo path, image, created, last sync, backup flag. Reads from registry.
repoman shell <name> [--cwd <path>] Drop into the container as the local user, in the repo's directory. Replaces the verbose incus shell --user 1000 --cwd ... invocation.
repoman sync [name] [--no-delete] [--dry-run] rsync mirror local → NFS, all projects or one. NFS auto-mount aware. ZFS snapshots on the file server provide history.
repoman remove <name> [--yes | -y] [--keep-incus] Destroy the container; the repo on host is never touched. Prompts for confirmation by default; --yes skips the prompt.
repoman status [name] Health check: container state, mounts, NFS reachability, last sync.

Out of scope

Brainstormed and rejected during the v0.4 design conversation (see memory: project_repoman.md for full rationale):

  • Other container runtimes (Docker, Podman). Incus only — we depend on its full-Linux-VPS shape.
  • Other backup destinations (S3, restic, borg). NFS only.
  • macOS/Windows. Linux only.
  • Multi-host coordination, replication.
  • ZFS snapshot management (file-server-side; user runs sanoid/syncoid themselves).
  • Cron installation (provide a doc snippet, not a subcommand).
  • IDE integration.
  • Image building / curation (repoman image build/refresh/list/prune). Rejected: maintenance burden outweighs benefit for homelab usage volumes.
  • [install] block in override files (per-project install commands run in container at create). Rejected: users put scripts/install.sh in their project repo and run it after repoman shell. Not worth a TOML schema + lifecycle + security model.
  • Setup wizard configuring host services (ollama auto-install, systemd overrides, model pulls). Rejected: scope creep across the host-config boundary.
  • --hermes / per-container hermes provisioning. Hard-rejected after v0.3 iterations failed on uv-vendored Python portability.

Configuration

Central registry: ~/.config/repoman/repoman.toml (schema 3 as of v0.4)

schema = 3

[repoman]
output = "quiet"

[host]
lan_ip = "192.168.1.42"

[defaults]
repos_root     = "~/repos"
backup_root    = "/nfs/repos"
logdir         = "~/.local/state/repoman"
incus_project  = "repoman"
default_image  = "images:ubuntu/26.04/cloud"
profiles       = ["default", "claude-share"]

[[project]]
name        = "isurus"
repo        = "isurus-project"
image       = "images:ubuntu/26.04/cloud"
profiles    = ["default", "claude-share"]
created     = "2026-04-28T15:00:00Z"
last_sync   = "2026-04-28T20:17:03Z"
backup      = true

Per-project override (optional): ~/.config/repoman/repos.d/<name>.toml

[container]
image    = "images:debian/12/cloud"
profiles = ["default", "claude-share", "node-dev"]

[[mount]]
source = "~/.npm"
path   = "/home/ctusa/.npm"

[env]
NODE_ENV = "development"

Implementation: reef-lang

Reef has the right shape for this:

  • Native compilation → C → GCC/Clang → static binary. Single-file distribution.
  • Stdlib coverage in use:
    • encoding.toml — central registry + per-project overrides + profile YAML helpers
    • sys.args / sys.flag — CLI parsing
    • sys.env — env var reads
    • sys.process — argv-list process_run / process_spawn for rsync/incus shell-outs (no process_spawn_shell: user-derived names cannot trigger shell injection)
    • io.console — interactive prompts (confirm, confirm_default_no)
    • io.file, io.dir, io.path, io.stream — local FS ops
    • core.result_generic, core.option — error handling primitives
  • Multi-platform reef = future macOS/BSD/illumos support comes for free if/when needed.
  • Currently pins reef-lang 0.5.20 (source at ~/reef-lang-0.5.20-source/).

Resolved implementation questions

  • Subprocess execution. sys.process provides the POSIX surface needed. Rule: argv-list process_run / process_spawn exclusively; never process_spawn_shell for user-derived inputs.
  • Incus integration. Decision: CLI shell-out (incus invoked with argv-list, --format json where useful). REST-via-unix-socket migration deferred until a concrete need arises.
  • Interactive UX. Line-based prompts using io.console. Rich TUI was out of scope and stayed out.
  • uid mapping for bind mounts. Resolved in v0.4.2: every cmd_new repo bind is created with shift=true so host uid 1000 maps to container uid 1000.

Versions shipped

  • v0.1 (2026-04-29): new + sync subcommands, Incus project repoman namespace, central registry.
  • v0.2 (2026-05-05): added list, status, remove, shell.
  • v0.3 (2026-05-08): setup wizard, llm-share profile (ollama-over-LAN), schema-2 migration. --hermes experiment reverted before tag.
  • v0.4 (2026-05-09): profile subcommand family, vendor profile library with user-shadow override, schema-3 migration (drops [defaults].llm, adds [host].lan_ip), setup wizard trimmed (no more --with-llm).
  • v0.4.2 (2026-05-11): cmd_new bind-mounts with shift=true so host uid maps into the container — agents in containers can actually write to the repo as the in-container user.
  • v0.4.3 (2026-05-11): cmd_remove confirmation prompt spells out exactly what gets removed; reassures the user that the source repo on host is not touched.
  • v0.4.4 (2026-05-11): --version reports the actual version; build-warning cleanup.

Success criteria for v0.x

  • Builds clean from a fresh reef-lang install on Linux x86_64.
  • repoman setup runs idempotently and produces a working baseline on a fresh host.
  • repoman new creates a container, matches the bash prototype's behavior, and persists in the registry with per-project override support.
  • repoman sync matches the bash prototype's behavior with autofs check + mirror semantics.
  • All subcommands have both interactive and flag-driven invocation paths.
  • Bind mounts respect host UID/GID so containers can write to the repo.
  • License chosen (CDDL 1.0, Leafscale, LLC as steward/Initial Developer).
  • Single static binary distributable via tarball or per-distro package.
  • README + VISION + per-feature doc.
  • Public repo on a forge with a contribution guide.

Open product questions

  1. Forge. GitHub, Codeberg, self-hosted? Distribution implications (binaries, releases).
  2. Versioning toward 1.0. Current cadence is rapid patch releases. What's the 1.0 bar? Probably "stable schema + public forge + tarball releases."
  3. Logging / observability. Stderr + per-invocation log files today. Structured (JSONL) output mode for automation — needed yet?
  4. Configuration migration policy. Schema 1 → 2 → 3 worked as implicit lossless migration on parse. Same approach for the next bump?

Naming convention

  • Container name = <name> (positional, required).
  • Repo dirname = <name> by default, overridable with --repo <dirname>.
  • Repo absolute path = <repos_root>/<repo-dirname>.
  • No magic suffix logic. Explicit > implicit.

Differentiation reality check

Why someone would pick repoman:

  • They want to run AI agents with --dangerously-skip-permissions (or its equivalent) without taking the host down.
  • They want the Incus-project-as-namespace pattern but don't want to wire it themselves.
  • They want a guided onboarding into "this style of homelab dev workflow."
  • They want a unified registry of "what projects do I have, where are they, when did they last back up."

Why they wouldn't:

  • They already have ansible/nix/etc. covering this.
  • They use Docker, not Incus.
  • They don't separate local working copies from backup destinations.
  • They run their agents under tighter constraints (microVMs, gVisor, restricted unix users) and don't need a container-shaped harness.

That's fine. The audience is small and that's the intentional design.