Decisions¶
Every significant design decision in lab is recorded as an ADR — one decision per file, in the order it was taken, and kept in place when a later ADR supersedes it. This list is generated from docs/adr/ at build time, so a new ADR appears here as soon as its file lands.
- ADR-0001 — Greenfield Go rewrite in its own repo, single-operator and product-shaped
- ADR-0002 — Nix flake is the first-class packaging; NixOS module; no Docker
- ADR-0003 — One store layer over SQLite and Postgres; the DB is never the only witness
- ADR-0004 — Auth model: built-in argon2id login, PATs, trusted-proxy mode, scoped CSRF
- ADR-0005 — SolidJS SPA over a JSON API with SSE; clients refetch on event
- ADR-0006 — Credentials encrypted at rest; per-op materialization; the agent never sees a forge token
- ADR-0007 — Repositories are added explicitly and cloned by lab into bare reference clones
- ADR-0008 — Instances run in per-instance worktrees under tmux, behind an AgentProvider seam
- ADR-0009 — One Tracker vocabulary over Forgejo REST and the built-in issue tracker
- ADR-0010 — The AFK engine ports v0's model with four deltas, and the row-as-candidate inversion needs the start guard
- ADR-0011 — Change requests merge from the UI: three-dot diff, ancestry-checked ff, merge commits in a throwaway worktree
- ADR-0012 — Skills seeded into every worktree; incogni is leak-proof by construction, not by hope
- ADR-0013 — Hardening: domain metrics at terminal chokepoints, an honest offline shell, and a checkable definition of done
- ADR-0014 — Agent triage surface: deliberate workflow actions write through the tracker seam
- ADR-0015 — Forge flavor on the credential; GitHub tracker client
- ADR-0016 — Embedded chat: view and reply to an instance's conversation inside lab's UI
- ADR-0017 — Optional capability interfaces on the provider seam
- ADR-0018 — Chat message rendering: hand-rolled markdown, tool-run grouping, copy-raw
- ADR-0019 — Responsive UI overhaul: hamburger nav, spacing scale, vendored icon system
- ADR-0020 — Answerable interactive dialogs: capture the pending dialog live via a PreToolUse hook spool
- ADR-0021 — AFK spawn defaults diverge from the manual pre-fill, and provider options ride a generic bag beside typed model/effort
- ADR-0022 — Chat composer send/interrupt morph: disable Send + one-tap interrupt while working
- ADR-0023 — CI split: a fast native gate on every PR, the hermetic
nix flake checkgate only on nix/dep changes - ADR-0024 — Landing a PR from the agent surface: one shared merge service, server-credentialed forge merges
- ADR-0025 — Chat-first shell: runs rail, composer-first Home, warm reskin
- ADR-0026 — Provider seam v2: generalize the agent-CLI contract so lab's chat works with any agent CLI
- ADR-0027 — AFK seed prompt override layers repo over global over the built-in template, replacing it wholesale with no silent edits
- ADR-0028 — Answered dialogs stay dialog messages carrying their recorded outcome; the pending dialog renders as an inline card placed by the client
- ADR-0029 — Chat composer always-Send: reverse the working-state morph, relocate Interrupt to the header
- ADR-0030 — The agent CLI becomes a three-level layered spawn knob, defaults resolve skip-layer against the effective catalog, and one searchable select replaces every spawn picker
- ADR-0031 — Composer bare Enter sends on fine-pointer devices, reversing ADR-0022/0029's always-newline rule
- ADR-0032 — Agent-readable PR checks: a lab-computed aggregate over both forge backends, polled client-side
- ADR-0033 — The NixOS module keys agent CLIs by provider ID, adds a fixed tools baseline, and keeps claudePackage as a deprecated alias
- ADR-0033 — Attribution markers become a single provider-declared pattern set, boot-validated and unioned across every registered provider, enforced identically by the pre-push hook and the body sanitizer
- ADR-0034 — config keys per-provider host settings by provider ID with generic
-provider-bin/-provider-configmaps, and keeps the claude-named fields as deprecated aliases - ADR-0034 — Per-provider login sessions:
lab-login-<providerID>, matched by prefix predicate - ADR-0035 — Every provider is seeded the same skills bundle; a provider without native skill discovery gets a frontmatter-derived skills index appended to its context file, whose name must be lab-owned and never repo-tracked
- ADR-0036 — The definition of done for an AgentProvider adapter becomes a two-tier conformance bar: one CI-run providertest suite, plus four required live spikes evidenced by a committed compat record
- ADR-0037 — The Codex CLI becomes the second
AgentProvider(codex): full-access never-ask spawn, argv-first config with two verified global-state exceptions, device-code auth as a new generic seam kind, and rollout-JSONL read-through — every fragile coupling live-pinned against codex-cli 0.133.0 - ADR-0038 — State composition becomes adapter-owned: ReadChat(runID, runtimeDir, transcriptPath) replaces ReadTranscript, PendingDialog/BlockedState leave the LiveSignals seam, and core keeps run lifecycle only
- ADR-0039 — Web Push notifications ride a lab-generated VAPID keypair with master-key file semantics; device-level subscriptions; the service worker never suppresses a notification
- ADR-0040 — Run title is a display overlay on top of the label; identity stays immutable after spawn
- ADR-0041 — Composer autocomplete gets tiered ranking; Tab completes, Enter always sends, click sends unless hinted — partially reversing ADR-0031's popover-Enter-accept precedence
- ADR-0042 — The AFK scheduler reads
## Blocked byas a machine contract, gating a claimable issue on its referenced blockers still being open - ADR-0043 — The codex model/effort catalog is probed from
codex debug modelsonce at provider construction — the 0.133.0 pinned list demoted to a probe-failure fallback — and the provider seam grows per-model effort lists, with spawn effort validated against the resolved model's own list because codex does not clamp - ADR-0044 — Presence-based push suppression is one server-side filter inside
Sender.Broadcast: a device whose app is visible gets no push, every other enrolled device still does, and the service worker's always-show rule stands - ADR-0045 — Remote Control becomes a fourth typed spawn knob —
Remote boolonSpawnSpec, one lab setting every provider sees, deliberately kept OUT of the ADR-0021 options bag — defaulting OFF, with providers advertising only whether they honor it - ADR-0046 — Operator issue edit routes title/body through
Tracker.EditIssueon every binding — state stays builtin-only behind a pinned 400 - ADR-0047 — The chat stream's SSE reaction is tail-only with a content-hash identity:
run.messages.changedcarries state and a back-patch cursor, messages carry a server-computed hash, and the client never rebuilds DOM for content that did not change - ADR-0048 — Autoland: a state-derived poller closes ADR-0024's deferred reject→re-queue loop — verdict-marker PR comments as the lander's channel, hybrid with human native reviews, default-off
- ADR-0049 — One priority-ordered spawn pass replaces the two loops that raced for the AFK/autoland fleet's instance cap — drain the pipeline before filling it, one cap consumer by construction, and lander outcomes leave the three-strikes counter untouched
- ADR-0050 — The done-signal is a per-branch pull lookup and every tracker listing is bounded — no read grows with repo history
- ADR-0051 — Per-provider agent-tools OCI images, built from sha256-pinned upstream artifacts and injected read-only at
/opt/lab, carry the agent surface into any user-chosen dev container — a delivery vehicle, not a revisit of lab's nix-first packaging - ADR-0052 — Containerized runner: rootless podman panes — tmux keeps the session, the pane command becomes
podman run -it --rm, and a per-reporepos.runnerseam keeps the unsandboxed host pane as labeled break-glass - ADR-0053 — Per-repo dev images: a nullable
repos.image_refresolved tag→digest and pinned on save, the global--container-imagedemoted to an optional default, and pull-if-missing moved to spawn ahead of the claim - ADR-0054 — Container-ready by default:
services.lab.container.enabledefaults on, the agent-tools default refs follow the committed versions.env pin, the deploy pin advances only behind a completed publish, and preflight re-pulls the tags - ADR-0055 — Server-owned credential authority: one refresher per provider grant, a blind periodic poke against the master store, immediate fan-out, and eager adopt-back checked before every wipe
- ADR-0056 — The NixOS module ships a working dev-image default:
container.defaultImagedefaults to a digest-pinnedbuildpack-deps:stable-scmso a stock deployment spawns container sessions out of the box, while the binary still ships none - ADR-0057 — Containerized provider login: the login pane and every provider-CLI invocation become
podman runagainst the rw-mounted master store — the agent-tools image is a container-mode host's only CLI distribution - ADR-0058 — Restart-safe container cgroup layout: DelegateSubgroup=main + a lab-owned payload/ subtree replaces --cgroups=split
- ADR-0059 — A static
lab-payload.serviceholder unit owns the delegated payload cgroup:DelegateSubgroup=never protects the main-process spawn, solab.servicecan never delegate - ADR-0060 — Agent-readable CI logs: a server-credentialed proxy of Forgejo's web log route, redacted, advertised at the point of need
- ADR-0060 — Containers become transient scopes under the lab user's lingering
user@manager: cgroup v2 gates PID migration on the common ancestor, so an unprivileged lab can never place a container in another unit's subtree - ADR-0061 — Context pressure is adapter-composed derived data on
Chat, not a capability:ReadChatwidens to aReadSpeccarrying the spawn-time model, and each adapter fillsContextUsage {Used, Limit}from token info it already parses past - ADR-0062 — Schedules: per-repo cron cadences fire scheduled runs — a fourth unattended run kind gathered by the one spawn pass, terminated by the budget clock alone, striking only themselves
- ADR-0063 — Read-only imports: a consumer-declared
repo_importsgrant materializes each target's freshly-fetchedorigin/<default>as a.git-less snapshot in the per-run directory, mounted read-only at a host-identical path and refreshed only by/pull-base - ADR-0064 — Split forge: GitHub Actions becomes CI's home and agent-tools publishes to
ghcr.iounder the built-inGITHUB_TOKEN, while Forgejo keeps exactly one workflow — the private-infra deploy, now a 5-minute poll that reads its rev from GitHub - ADR-0065 — Prebuilt static binaries ship from a
v*tag as a GitHub Release, cross-compiled withGOOS/GOARCHrather than through nix, with the tag as the version - ADR-0066 — The docs site is a MkDocs Material rendering of this repository published to GitHub Pages from
v*tags, with a build-time hook that injects the three root Markdown files and rewrites every relative link rather than copying anything intodocs/ - ADR-0067 — The OneCLI credential gateway: repo secrets stop entering runs at all — a sidecar proxy injects them into outbound HTTPS against the grants on that repo's agent identity, three optional settings leave the integration entirely off until an operator turns it on, and a run refuses to spawn without a reachable gateway
- ADR-0068 — The Warpgate SSH bastion: a run reaches operator-defined SSH targets without ever holding their credentials — one Warpgate user and one role per repo carry the access, a fresh per-run key registered on that user authenticates it, a per-run ssh_config that lab's own
ssh/scp/sftpwrappers pass with-Fand a host key trusted by configuration — or, left unconfigured, as the bastion presents it — makessh <target>work, and only a repo that has SSH targets refuses to spawn without a reachable bastion