Wallfacer is a host-native Go service that coordinates autonomous coding agents, with per-task git worktree isolation and a web task board for human oversight.
Each agent turn runs as a host os/exec of the selected CLI (claude, codex, cursor, opencode, or pi) with the task's git worktree as the working directory. Isolation comes from the worktree, not a container: there is no daemon, no image pull, and no /workspace bind-mount in the shipping runtime. A sixth harness, topos, is not a subprocess at all; it runs in-process through internal/agentgraph (see the dispatch layer below).
The runner unconditionally selects executor.HostBackend (the only executor.Backend implementation) and sets hostMode = true (internal/runner/runner.go:491-498). The backend execs the CLI directly (internal/executor/host.go). Cancellation is SIGTERM then SIGKILL on the host process (internal/executor/host.go), not a runtime kill command.
Several Go symbols keep the word "Container" as deliberate legacy vocabulary: ContainerSpec, ContainerInfo, ContainerLister, buildContainerSpecForSandbox, and the launch circuit breaker's WALLFACER_CONTAINER_CB_* env vars. These name code, not behaviour. The behaviour is a host process.
graph TB
subgraph Browser
UI["Browser UI<br/>(Vue 3 + TypeScript SPA)<br/>Board + Plan (spec explorer, minimap, agent chat)"]
end
subgraph Server["Go Server (stdlib net/http)"]
Handler["Handler<br/>REST API + SSE"]
Runner["Runner<br/>orchestration + commit"]
Store["Store<br/>state + persistence"]
Automation["Automation Loops<br/>promote / test / review / submit<br/>sync / retry / routines"]
Plan["Plan Mode<br/>spec tree + agent session<br/>dispatch + undo"]
Handler --> Runner
Handler --> Plan
Runner --> Store
Automation --> Store
Plan --> Store
Store -.->|pub/sub| Automation
end
subgraph Infra["Host"]
Agents["Agent CLIs (host os/exec)<br/>claude / codex / cursor / opencode / pi<br/>CWD = task worktree"]
Worktrees["Per-task Git Worktrees<br/>~/.wallfacer/worktrees/<br/>task/ID branches"]
SpecsFS["specs/ (markdown + frontmatter)<br/>agent sessions<br/>~/.wallfacer/agent-sessions/<fp>/"]
Agents --- Worktrees
end
UI -->|"HTTP / SSE"| Handler
Runner -->|os/exec| Agents
Plan -->|os/exec| Agents
Plan -->|read/write| SpecsFS
Filesystem-first persistence. No database by default. Each task is a directory (data/<key>/<uuid>/) containing task.json, traces, outputs, and oversight blobs. Writes are atomic (temp file + rename). Easy to inspect, back up, and debug. Persistence goes through the StorageBackend seam (see below), so the filesystem layout is one implementation, not a hard dependency.
Worktree isolation, not container isolation. Every agent turn is a host process whose CWD is the task's git worktree. Tasks isolate from each other through separate worktrees and task/<id> branches, not through a sandboxed runtime. Tasks work in parallel without merge conflicts during execution; rebase and merge happen at commit time.
Activity-routed harness + model. Different activities (implementation, testing, oversight, title, commit-msg) can route to different harnesses (claude, codex, cursor, opencode, pi, or in-process topos) and models, so cheap operations use smaller models. Routing selects a CLI and model, not an image.
Automation with guardrails. Background loops handle promotion, testing, submission, sync, and retry, each with explicit controls (toggles, budgets, thresholds). Scheduled work runs through the routine engine.
The codebase moved off three older designs. The docs and symbols below reflect the current state:
sandbox.Type->harness.ID. Theinternal/sandboxpackage is deleted; harness identities now live ininternal/harness(claude,codex,cursor,opencode,pi, plus the in-processtopos).- Container -> host process. Execution is a host
os/exec; theContainer*Go names are kept as legacy vocabulary. - Refine retired. There is no
refineagent orrefine-onlyflow. Prompt refinement is the Plan task-mode chat (POST /api/agent/tool/update_task_prompt).
States: backlog, in_progress, waiting, committing, done, failed, cancelled.
archived is a boolean flag on done/cancelled tasks, not a separate state.
See Task Lifecycle for the transition diagram and what each state means.
flowchart TD
Start["Start turn<br/>(increment N)"] --> Launch["Launch agent CLI<br/>(host os/exec)<br/>with prompt + session ID"]
Launch --> Save["Save output to<br/>turn-NNNN.json"]
Save --> Usage["Accumulate<br/>usage/cost"]
Usage --> Budget{"Check budgets<br/>MaxCost / MaxTokens"}
Budget -->|over budget| WaitingBudget["WAITING<br/>(budget_exceeded)"]
Budget -->|within budget| Parse{"Parse stop_reason"}
Parse -->|end_turn| Waiting2["WAITING<br/>awaiting review"]
Parse -->|"max_tokens / pause_turn"| Start
Parse -->|"empty / unknown"| Waiting["WAITING<br/>blocks until<br/>user feedback"]
Parse -->|"error / timeout"| FailedError["FAILED<br/>(classify failure<br/>category)"]
Waiting -->|feedback received| Start
Seven long-lived watchers run, each as a single goroutine started in RunServer (internal/cli/server.go). Scheduled work, including recurring idea generation, is not a watcher; it is a routine fired by the routine engine (recurring ideation is an ordinary routine tagged system:ideation, not a distinct engine).
flowchart LR
PubSub["Store<br/>pub/sub on<br/>state changes"]
PubSub --> Promoter["Auto-promoter<br/>backlog to in_progress<br/>when capacity available<br/>+ deps met + scheduled"]
PubSub --> Tester["Auto-tester<br/>launch test verification<br/>on untested waiting tasks"]
PubSub --> Submitter["Auto-submitter<br/>waiting to done<br/>when test passed<br/>+ conflict-free"]
PubSub --> Sync["Waiting-sync<br/>rebase worktrees<br/>behind default branch"]
PubSub --> Retry["Auto-retry<br/>failed to backlog<br/>if retry budget > 0"]
PubSub --> Review["Auto-review<br/>adversarial verification<br/>on waiting session tasks<br/>(supersedes auto-test when on)"]
PubSub --> Routines["Routine engine<br/>fire scheduled routines<br/>(user-defined)<br/>spawn tasks against a flow"]
The seven entry points are StartAutoPromoter, StartAutoRetrier, StartRoutineEngine, StartWaitingSyncWatcher, StartAutoTester, StartAutoSubmitter, StartAutoReview. There is no auto-refiner.
At task execution time the runner consults two registries before it execs any CLI:
internal/agents/holds the Role descriptors. Exactly five built-ins ship (title,oversight,commit-msg,impl,test;internal/agents/builtins.go), plus any user-authored clones loaded from~/.wallfacer/agents/. A role pins a harness, declares capabilities, and optionally carries a system-prompt preamble.internal/flow/holds Flow definitions: ordered step chains that reference roles by slug. One built-in ships (implement;internal/flow/builtins.go); user flows live under~/.wallfacer/flows/. Theimplementflow runsimpl -> test -> parallel(commit-msg, title, oversight). Tasks pinned to a since-removed slug (e.g. the retiredbrainstorm) resolve toimplement(registry.go).
Both directories are fsnotify-watched; edits reload the merged registry without restarting the server.
Task execution picks one of three dispatch paths (internal/runner/execute.go):
- a flow marked
Agentic-> the in-process topos agent-graph runtime.internal/agentgraphis the single seam onto the embedded topos runtime: it compiles the flow plus agents registry into atopos.RegionandrunAgenticFlowexecutes it, persisting the resulting trace graph on the task. The built-inimplementflow does not setAgentic; this path is experimental/opt-in. A task whose resolved harness istopossimilarly runs throughrunNativeToposinstead of a subprocess. flow == "implement"-> the turn-loop path inexecute.go(impl -> test -> commit pipeline with full session-recovery semantics).- any other flow slug -> the flow engine in
internal/flow/engine.go. It walks steps linearly, fans parallel-sibling groups through anerrgroup, and launches each role viaRunner.RunAgent.
In the UI, agent definition and flow composition share a single surface: the Agent Graph page (/agent-graph). The former Agents and Flows pages are gone; /agents, /workflows, and /flows redirect to /agent-graph (frontend/src/router.ts).
See Agent Graph for the full user-facing model.
Store (internal/store/), In-memory task state guarded by sync.RWMutex, persisted through the StorageBackend seam. Enforces the state machine via a transition table. Provides pub/sub for live deltas and a full-text search index.
Runner (internal/runner/), Orchestration engine. Creates worktrees, builds launch specs, execs the agent CLI as a host process, runs the turn loop, accumulates usage, enforces budgets, runs the commit pipeline, and generates titles/oversight in the background.
Handler (internal/handler/), REST API and SSE endpoints organized by concern. Hosts automation toggle controls and the background watchers.
Executor (internal/executor/), The Backend seam (Launch/cancellation) plus HostBackend, the single shipping implementation that execs the CLI as a host process and relays its stream-json stdout.
Harness (internal/harness/), Harness identities, capabilities, and stream parsers for the five subprocess harnesses (claude, codex, cursor, opencode, pi) plus the in-process topos harness. harness.Default() returns Claude. The cursor harness adapts the cursor-agent CLI and emits Claude-style stream-json.
Webserver (internal/webserver/), Serves the SPA embedded from frontend/dist and passed in as an fs.FS (MountSPA, internal/webserver/spa.go); falls through to index.html for client-side routes.
Frontend (frontend/), Vue 3 + TypeScript SPA (Vite, Vue Router, Pinia). Task board, modals, timeline/flamegraph, diff viewer, usage dashboard. All live updates via SSE.
Workspace Manager (internal/workspace/), Manages workspace records (stable UUID identity + mutable folder set, persisted in workspaces.json), DataKey-scoped stores, and hot-swapping between workspaces without server restart.
store.StorageBackend(internal/store/backend.go) abstracts the three persistence concerns: tasks (structured, indexed), events (ordered, append-heavy), and blobs (named bytes per task). Domain concepts map onto these primitives; for exampleSaveOversightbecomesSaveBlob(id, "oversight", data). Oversight persists as a single blob (oversight.json, andoversight-test.jsonfor the test phase) viaSaveBlob, not a per-id file underoversights/.executor.Backend(internal/executor/) abstracts agent launch.HostBackendis the only implementation.
Cloud Identity is wired in RunServer (internal/cli/server.go). The request handler chain wraps the mux outside-in, so requests flow CSRF -> CookieAuth -> OptionalAuth -> BearerAuth -> (ForceLogin in cloud mode) -> mux:
handler.CSRFMiddleware(hostPort), unconditional CSRF protection (no skip flag).auth.CookieAuth(authClient, next), two args; resolves anIdentityfrom the session cookie. There is no separate jwt validator argument and no CSRF skip.auth.OptionalAuth(jwtValidator, next), populates the principal from a bearer JWT when present, without forcing auth.handler.BearerAuthMiddleware(serverAPIKey), static-key check that is bypassed once an identity is already populated, so a cookie-only browser request succeeds.Handler.ForceLogin(mux), applied only whencloudModeis true; a localwallfacer runstays reachable anonymously.auth.RequireSuperadmin(next), a per-route admin gate (server.go:921), not part of the global chain.
Tasks carry CreatedBy and OrgID (internal/store/models.go); TasksForPrincipal (internal/store/principal.go) tenant-filters listings. The principal route is GET /api/me; PATCH /api/auth/me switches org.
This section traces a single task through every component from browser click to merged commit. The sequence diagram shows the full flow; the prose below explains each step.
sequenceDiagram
participant B as Browser
participant H as Handler
participant S as Store
participant SSE as SSE Subscribers
participant R as Runner
participant C as Agent CLI (host process)
participant G as Git
B->>H: POST /api/tasks {prompt}
H->>S: CreateTaskWithOptions()
S->>S: saveTask() + notify()
S-->>SSE: SequencedDelta (new task)
H->>R: GenerateTitleBackground()
B->>H: PATCH /api/tasks/{id} {status: in_progress}
H->>S: UpdateTaskStatus()
S-->>SSE: SequencedDelta (status change)
H->>R: RunBackground()
R->>R: setupWorktrees() under worktreeMu
R->>G: CreateWorktree per workspace
R->>S: UpdateTaskWorktrees()
loop Turn loop
R->>R: generateBoardContextAndMounts()
R->>C: buildContainerSpecForSandbox() + backend.Launch() (os/exec)
C-->>R: stream-json stdout (agentOutput)
R->>S: SaveTurnOutput() + AccumulateSubAgentUsage()
R->>R: parse stop_reason
end
alt end_turn
R->>S: UpdateTaskStatus(waiting)
R->>R: GenerateOversightBackground()
end
B->>H: POST /api/tasks/{id}/done
H->>H: CompleteTask()
H->>S: ForceUpdateTaskStatus(committing)
H->>H: runCommitTransition()
R->>R: commit() - Phase 1: hostStageAndCommit()
R->>C: generateCommitMessage() (host-process agent run)
R->>G: git add + git commit in worktree
R->>R: commit() - Phase 2: rebaseAndMerge()
R->>G: RebaseOntoDefault() + FFMerge()
R->>R: commit() - Phase 3: cleanup
R->>R: cleanupWorktrees() under worktreeMu
R->>G: RemoveWorktree + delete branch
R->>S: UpdateTaskStatus(done)
S-->>SSE: SequencedDelta (done)
The browser sends POST /api/tasks with a prompt and optional goal. Handler.CreateTask (internal/handler/tasks.go) decodes the request, validates harness availability, and calls Store.CreateTaskWithOptions (internal/store/tasks_create_delete.go). The store assigns a UUID, writes task.json atomically (temp file + rename), adds the task to the in-memory map, and calls notify() which fans the new SequencedDelta to all SSE subscribers. Back in the handler, Runner.GenerateTitleBackground (internal/runner/runner.go) fires a background goroutine tracked by backgroundWg that runs a lightweight host-process agent to generate a short title from the prompt.
The browser sends PATCH /api/tasks/{id} with {status: "in_progress"}. Handler.UpdateTask (internal/handler/tasks.go) checks concurrency limits via checkConcurrencyAndUpdateStatus, transitions the store status, inserts a state_change event, and calls Runner.RunBackground (internal/runner/runner.go). RunBackground registers the goroutine label with backgroundWg.Add and launches Runner.Run in a new goroutine. Inside Run (internal/runner/execute.go), the first thing is worktree setup: setupWorktrees (internal/runner/worktree.go) acquires worktreeMu, creates one git worktree per workspace via gitutil.CreateWorktree, and returns the worktree-path map and branch name (e.g. task/abcd1234). The runner persists these paths via Store.UpdateTaskWorktrees.
The turn loop in Run increments the turn counter, refreshes the board context via generateBoardContextAndMounts (internal/runner/board.go), and calls runContainer (internal/runner/container.go). That function builds the launch spec via buildContainerSpecForSandbox, resolves the harness and model per activity, checks the circuit breaker, and invokes backend.Launch, which execs the agent CLI directly via os/exec with the worktree as CWD. The stream-json stdout is parsed into an agentOutput struct. The runner saves raw output via Store.SaveTurnOutput, accumulates token usage via Store.AccumulateSubAgentUsage and Store.AppendTurnUsage, then inspects output.StopReason to decide the next step.
When stop_reason is "end_turn", the runner transitions the task to waiting via Store.UpdateTaskStatus, inserts a state_change event, and opens a feedback_waiting span. GenerateOversightBackground fires an asynchronous oversight summary generation (a host-process agent run). The notify() call inside the status update fans a delta to SSE subscribers and wakes automation watchers (auto-tester, auto-submitter) via the SubscribeWake channels. If stop_reason is "max_tokens" or "pause_turn", the loop auto-continues by setting prompt = "" and resuming the same session.
The user clicks "Mark as Done", sending POST /api/tasks/{id}/done. Handler.CompleteTask (internal/handler/execute.go) verifies the task is in waiting, restores any missing worktrees, transitions to committing via Store.ForceUpdateTaskStatus, and calls runCommitTransition which launches Runner.Commit (internal/runner/commit.go) in a background goroutine. The commit pipeline has three phases. Phase 1 (hostStageAndCommit) stages and commits host-side: it runs git add and git commit in each worktree on the host, using a commit message produced by generateCommitMessage, which is itself a host-process agent run (the commit-msg role). Phase 2 (rebaseAndMerge) acquires the per-repo mutex via repoLock(), calls gitutil.RebaseOntoDefault with up to 3 conflict-resolution retries (each retry runs a host-process conflict-resolver agent), then gitutil.FFMerge to fast-forward the default branch. Phase 3 persists commit hashes, cleans up worktrees via cleanupWorktrees (under worktreeMu), and optionally auto-pushes.
After the commit pipeline succeeds, runCommitTransition transitions the task to done via Store.ForceUpdateTaskStatus. The store persists the status, notifies SSE subscribers, and wakes watchers. The worktree directories and task branch have already been removed in Phase 3. A TaskSummary is written for the cost dashboard.
| Mutex | Location | Protects | Lock pattern | Typical hold |
|---|---|---|---|---|
Store.mu |
internal/store/store.go |
In-memory task map, status index, search index, event maps | Write lock for all mutations (mutateTask, CreateTaskWithOptions, status updates); read lock for queries (ListTasks, GetTask) |
Microseconds (in-memory map ops + atomic file write) |
Runner.worktreeMu |
internal/runner/runner.go |
All worktree filesystem operations on worktreesDir |
Exclusive lock in setupWorktrees, ensureTaskWorktrees, cleanupWorktrees, CleanupWorktrees, PruneUnknownWorktrees |
Milliseconds to seconds (git worktree create/remove) |
Runner.repoMu (per-repo) |
internal/runner/runner.go |
Rebase + merge serialization per repository | Exclusive lock via repoLock(repoPath) in rebaseAndMerge; tasks on different repos run concurrently |
Seconds (rebase + merge + optional conflict resolution) |
Runner.oversightMu (per-task) |
internal/runner/runner.go |
Serializes oversight generation per task | Exclusive lock via oversightLock(taskID) in GenerateOversight |
Seconds (host-process agent run) |
Store.subMu |
internal/store/subscribe.go |
SSE subscriber map | Exclusive lock during Subscribe, Unsubscribe, and the fan-out in notify() |
Microseconds |
Store.wakeSubMu |
internal/store/subscribe.go |
Wake-only subscriber map | Exclusive lock during SubscribeWake, UnsubscribeWake, and the fan-out in notify() |
Microseconds |
Store.replayMu |
internal/store/subscribe.go |
Replay buffer (ring of recent deltas) | Write lock in notify(); read lock in DeltasSince() |
Microseconds |
Runner.boardCache.mu |
internal/runner/runner.go |
Board context JSON cache and mount cache | Exclusive lock for cache read/write in generateBoardContextAndMounts |
Microseconds |
Runner.storeMu |
internal/runner/runner.go |
Runner's pointer to the active *store.Store (swapped on workspace switch) |
Write lock in applyWorkspaceSnapshot; read lock in currentStore |
Microseconds |
There is no worker pool. Each task execution gets its own goroutine via Runner.RunBackground, which calls backgroundWg.Add(label) before launching go r.Run(...) and backgroundWg.Done(label) in a deferred cleanup. The same backgroundWg (trackedWg) tracks all fire-and-forget background work: title generation (GenerateTitleBackground), oversight generation (GenerateOversightBackground), and worktree sync (SyncWorktreesBackground). Each goroutine registers with a human-readable label (e.g. "run:abcd1234", "title:abcd1234"). Runner.PendingGoroutines() returns the sorted list of outstanding labels for diagnostics.
The seven automation watchers (StartAutoPromoter, StartAutoRetrier, StartRoutineEngine, StartWaitingSyncWatcher, StartAutoTester, StartAutoSubmitter, StartAutoReview) each run as a single long-lived goroutine started in RunServer (internal/cli/server.go). They block on SubscribeWake channels and wake when any task mutates, then inspect the current task list to decide whether to act.
The store provides two subscriber tiers:
-
Full-delta channels (
Subscribe): returns(int, <-chan SequencedDelta). Channels are buffered at 256 (pubsub.DefaultChannelSize). Each mutation callsnotify()which stamps a monotonicdeltaSeq, appends to a bounded replay buffer (512 entries), and fans out a deep-copiedSequencedDeltato every subscriber. If a subscriber's buffer is full, the delta is silently dropped. SSE reconnection usesDeltasSince(seq)to replay missed deltas from the buffer before falling back to a full snapshot. -
Wake-only channels (
SubscribeWake): returns(int, <-chan struct{}). Channels are buffered at 1. The capacity-1 design coalesces rapid bursts: once a signal is pending, further sends are no-ops. Automation watchers use this tier to avoid allocating fullSequencedDeltacopies when they only need a "something changed" signal.
Both fan-outs happen inside notify() (internal/store/subscribe.go), which is always called while Store.mu is held, ensuring the delta sequence is consistent with the in-memory state.
sequenceDiagram
participant Sig as OS Signal
participant Srv as HTTP Server
participant R as Runner
participant BG as Background Goroutines
Sig->>Srv: SIGTERM / SIGINT (signal.NotifyContext)
Srv->>Srv: ctx.Done() -> srv.Shutdown(5s timeout)
Note over Srv: SSE handlers exit via cancelled base context
Srv->>R: r.Shutdown()
R->>R: shutdownCancel() -> cancel shutdownCtx
R->>R: close(shutdownCh) -> board subscription exits
R->>R: boardSubscriptionWg.Wait()
R->>BG: backgroundWg.Wait()
Note over R: Logs pending goroutines every 3s while waiting
R-->>Srv: Shutdown() returns
The shutdown sequence is driven by signal.NotifyContext(ctx, SIGTERM, Interrupt) in RunServer (internal/cli/server.go). When a signal arrives, ctx.Done() fires. The HTTP server gets srv.Shutdown(5s) to drain in-flight requests; SSE handlers exit immediately because their request contexts derive from the now-cancelled base context. Then Runner.Shutdown() (internal/runner/runner.go) is called: it invokes shutdownCancel() to cancel shutdownCtx (which propagates to any agent launches or store operations using it), closes shutdownCh to stop the board-cache subscription goroutine, waits on boardSubscriptionWg, then waits on backgroundWg with a 3-second ticker that logs still-pending goroutine labels. In-progress agent processes are intentionally left running; they continue independently and are recovered by RecoverOrphanedTasks (internal/runner/recovery.go) on the next startup.
Quick-reference for common maintenance tasks. Each entry names the starting file and the typical next steps.
| If you need to... | Start here |
|---|---|
| Add a new API endpoint | internal/apicontract/routes.go -> internal/handler/<concern>.go -> run make api-contract |
| Add a field to Task | internal/store/models.go -> internal/store/migrate.go |
| Change the turn loop | internal/runner/execute.go (Run()) |
| Change the commit pipeline | internal/runner/commit.go (commit(), hostStageAndCommit(), rebaseAndMerge()) + internal/gitutil/ops.go |
| Add a new automation watcher | internal/handler/tasks_autoimplement.go (follow SubscribeWake pattern) |
| Change the agent launch spec | internal/runner/container.go (buildContainerSpecForSandbox()) + internal/executor/host.go |
| Add or change a harness | internal/harness/ (claude.go, codex.go, cursor.go, opencode.go, pi.go, topos.go, registry.go) |
| Add a new env config variable | internal/envconfig/envconfig.go |
| Change workspace switching | internal/workspace/manager.go (Switch()) |
| Debug a failing rebase | internal/gitutil/ops.go + internal/gitutil/stash.go |
| Understand why a task failed | data/<key>/<uuid>/traces/ + outputs/turn-NNNN.json |
| Add a new system prompt | internal/prompts/ dir + internal/prompts/prompts.go |
| Change the UI | frontend/src/ (Vue components, composables, Pinia stores) |
| Debug startup recovery | internal/runner/recovery.go (RecoverOrphanedTasks()) |
| Change pub/sub behaviour | internal/store/subscribe.go (notify(), Subscribe(), SubscribeWake()) |
| Change cloud auth wiring | internal/cli/server.go (middleware chain) + internal/auth/ |
Every internal/ package and its role in the system:
| Package | Purpose | Key exported types / functions |
|---|---|---|
adversarial |
Review adversarial verification: forks a task's session into proposer/critic runs and reduces to a verdict | ReviewVerifier |
agentgraph |
The single seam onto the embedded topos runtime: compiles a flow + agents registry into a topos.Region, executes it, returns final text plus a trace graph |
FromFlow(), RunFlow(), Runner, Trace |
agents |
Merged built-in + user-authored agent registry backed by YAML under ~/.wallfacer/agents/; fsnotify reload. Five built-in roles: title, oversight, commit-msg, impl, test |
Registry, Role, BuiltinAgents, NewRegistry(), Load() |
apicontract |
Single source of truth for all HTTP API routes; generates docs/internals/api-contract.json |
Route, Routes (slice), Route.FullPattern() |
auth |
JWT + cookie principal resolution, optional auth, and superadmin gating for cloud mode | OptionalAuth(), CookieAuth(), RequireSuperadmin(), Validator, Identity, PrincipalFromContext() |
cli |
CLI subcommand implementations (run, status, doctor/env, spec, auth, web) and shared helpers | RunServer(), RunStatus(), RunDoctor(), RunSpec(), RunAuth(), RunWeb(), BuildMux(), ConfigDir() |
coordinator |
Cloud coordination plane: the wallfacerd role signed-in local instances connect to over one outbound WebSocket (presence, spec comments, metadata projection) | Registry, CommentStore (memory + Postgres) |
envconfig |
.env file parsing and atomic update |
Config, Parse(), Update() |
executor |
Agent-launch seam plus the single host-process implementation | Backend, HostBackend, NewHostBackend(), ContainerSpec, Request |
flow |
Merged built-in + user-authored flow registry; composes agents into ordered step chains. One built-in flow: implement; unregistered slugs resolve to it |
Registry, Flow, Step, NewBuiltinRegistry() |
github |
GitHub integration: principal-scoped token store for the brokered "Latere AI" GitHub App credential, API client, PR/comment read-write surfaces | Store, HTTPBroker, Client |
gitutil |
Git utility operations: worktrees, rebase, merge, status | RebaseOntoDefault(), FFMerge(), CommitsBehind(), WorkspaceStatus(), WorkspaceGitStatus |
graph |
Server-side unified spec+task dependency graph (nodes, typed edges, critical path, blocked set) behind GET /api/graph |
Build() |
handler |
HTTP API handlers organised by concern; automation watchers | Handler, NewHandler(), CSRFMiddleware(), BearerAuthMiddleware(), MaxBytesMiddleware(), ForceLogin() |
harness |
Harness identities, capabilities, and stream parsers for the five subprocess harnesses (claude, codex, cursor, opencode, pi) plus in-process topos; replaces the deleted sandbox package |
ID, Claude, Codex, Cursor, OpenCode, Pi, Topos, Harness, Register(), Lookup(), Default() |
logger |
Structured logging via log/slog with per-component named loggers |
Init(), Fatal(), Main, Runner, Store, Git, Handler, Recovery, Prompts |
metrics |
Lightweight Prometheus-compatible metrics registry (no external deps) | Registry, Counter, Histogram, LabeledValue, NewRegistry() |
runner |
Orchestration, turn loop, commit pipeline, worktree management (execs agents as host processes) | Runner, NewRunner(), RunnerConfig, ContainerInfo, CircuitBreaker, Interface |
store |
Per-task persistence (via StorageBackend), data models, event sourcing, pub/sub |
Store, Task, TaskEvent, TaskUsage, SandboxActivity, TaskDelta, StorageBackend |
webserver |
Serves the SPA embedded from frontend/dist |
MountSPA() |
workspace |
Workspace lifecycle manager; stable-identity workspace records (workspaces.json, migrated from workspace-groups.json); DataKey-scoped data directories; hot-swap and per-workspace parallelism/automation settings |
Manager, Workspace, Snapshot, NewManager(), LoadGroups(), SaveGroups(), MigrateToWorkspaces() |
constants |
Consolidated system parameters: timeouts, intervals, retry counts, size limits | Named constants grouped by concern |
oauth |
OAuth 2.0 PKCE flow engine for agent-CLI sign-in, ephemeral callback server, provider configs (Claude, Codex). The latere.ai device-code sign-in is separate: internal/handler/device_auth.go drives RFC 8628 against the auth service |
Flow, StartFlow(), Provider, ClaudeProvider, CodexProvider |
agentsession |
Long-lived workspace-scoped agent-session lifecycle; per-session messages.jsonl + session.json under ~/.wallfacer/agent-sessions/<fp>/; slash-command template expansion; single-turn-at-a-time coordination |
Runtime, Manager, ConversationStore, CommandRegistry, SessionMeta, Slugify, Expand |
routine |
Routine scheduler engine that fires routine-kind tasks (user-defined) on their configured cadence | Engine, Start(), Trigger() |
spec |
Spec document model: YAML frontmatter parse/write round-trip; seven-state lifecycle state machine; recursive tree builder; per-spec + cross-spec validation; atomic scaffold (O_CREATE|O_EXCL); progress aggregation; impact analysis; roadmap README index resolution |
Spec, Status, Effort, StatusMachine, Tree, BuildTree(), ParseFile(), Scaffold(), ValidateSpec(), UpdateFrontmatter(), ResolveIndex() |
speccomment |
Domain types for inline spec comments (the coordinator-authoritative collaboration artifact of the coordination plane) | Comment, Thread, Anchor, NewID() |
prompts |
System prompt templates (title, commit, oversight, test, conflict, drift) and the data-key helpers used to scope per-workspace data directories | Manager, NewManager(), WorkspaceDataKey(), NewDataKey() |
Shared utility packages under internal/pkg/:
| Package | Purpose | Key exported types / functions |
|---|---|---|
pkg/atomicfile |
Atomic file writes (temp + rename) | Write() |
pkg/cache |
TTL cache with expiration | TTLCache[K,V] |
pkg/circuitbreaker |
Circuit breakers (lock-free and backoff variants) | Breaker, BackoffBreaker |
pkg/cmdexec |
os/exec wrapper for git and agent commands, with a rollback-capable step transaction |
Cmd, New(), Git(), Tx, NewTx() |
pkg/dagscorer |
DAG-based task dependency scoring | Score() |
pkg/dircp |
Directory tree copy with filters | Copy() |
pkg/envutil |
Environment variable parsing with defaults and validation | Int(), IntMin(), Duration() |
pkg/httpjson |
JSON request/response helpers for HTTP handlers | DecodeBody(), DecodeOptionalBody(), PathUUID(), Write() |
pkg/keyedmu |
Per-key mutex map for fine-grained locking | Map[K] |
pkg/lazyval |
Lazily-computed cached value with invalidation | Value[T], New() |
pkg/ndjson |
Newline-delimited JSON file reader/appender | ReadFile(), AppendFile(), PreferResultLine() |
pkg/pagination |
Cursor-based pagination helpers | Paginate() |
pkg/pty |
PTY relay for the WebSocket terminal integration | Open(), StartWithSize(), Setsize() |
pkg/pubsub |
Generic fan-out notification hub with replay | Hub[T] |
pkg/sanitize |
Slug and rune-safe truncation helpers | Slug(), Truncate(), TruncateTrimRight() |
pkg/set |
Generic set type | Set[T], New() |
pkg/sortedkeys |
Sorted map key iteration | Of() |
pkg/slugutil |
Kebab-case identifier validation for user-authored YAML registries | IsValid() |
pkg/sse |
Server-Sent Events writer for http.ResponseWriter |
Writer, NewWriter() |
pkg/syncmap |
Type-safe generic wrapper around sync.Map |
Map[K,V] |
pkg/tail |
Retains the last N elements of a slice | Of() |
pkg/trackedwg |
sync.WaitGroup with pending-task labels |
WaitGroup |
pkg/uuidutil |
UUID validation helper | IsValid() |
pkg/watcher |
Event-loop background watcher | Start(), Config, WakeSource |
pkg/registry |
Merges a built-in catalog with user-authored items keyed by slug; shared by the agent and flow stores | MergeUnique(), ContainsSlug() |
pkg/yamldir |
Reads YAML definition files from a user directory | ReadAll(), File, Remove() |
pkg/yamlwatch |
Debounces filesystem events on a YAML directory | Watch() |
pkg/dag |
Generic DAG operations (ReverseEdges, DetectCycles, Reachable) | ReverseEdges(), DetectCycles(), Reachable() |
pkg/livelog |
Concurrency-safe append-only byte buffer with multiple readers for live streaming | Buffer, NewBuffer(), Reader |
pkg/statemachine |
Generic state machine with transition validation | Machine[S], New(), Transition() |
pkg/tree |
Generic tree data structure with iter.Seq walk |
Node[T], Walk() |
Each handler file in internal/handler/ owns a specific concern area. The table below lists representative non-test .go files.
| File | Concern | Key endpoints |
|---|---|---|
handler.go |
Core Handler struct, constructor, autoimplement toggle state, JSON helpers, workspace snapshot subscription |
, (shared infrastructure) |
middleware.go |
Request middleware: CSRFMiddleware, BearerAuthMiddleware, MaxBytesMiddleware |
, (middleware, not endpoints) |
principal.go |
Request principal plumbing used by auth/cloud middleware | , (internal) |
force_login.go |
Force-login gate applied in cloud mode (Handler.ForceLogin) |
, (internal) |
agents.go |
User-authored agent catalog CRUD backed by ~/.wallfacer/agents/ |
GET/POST /api/agents, PUT/DELETE /api/agents/{slug} |
flows.go |
User-authored flow catalog CRUD backed by ~/.wallfacer/flows/ |
GET/POST /api/flows, PUT/DELETE /api/flows/{slug} |
routines.go |
Routine card CRUD (list, create, update schedule, trigger) | GET/POST /api/routines, PATCH /api/routines/{id}/schedule, POST /api/routines/{id}/trigger |
routines_engine.go |
Scheduler loop that fires routine tasks (user-defined) on their cadence | StartRoutineEngine() (internal loop) |
orgs.go |
Organization listing and switching for cloud-mode principals | GET /api/me, GET /api/auth/orgs, PATCH /api/auth/me |
login.go |
Cloud sign-in flow handler | GET /login, GET /callback, GET /logout, GET /logout/notify |
tasks.go |
Task CRUD, batch create, status transitions. Cancel/archive/unarchive/restore fold into PATCH /api/tasks/{id}; resume/sync/test/done stay dedicated side-effect endpoints |
POST /api/tasks, PATCH /api/tasks/{id}, POST /api/tasks/{id}/resume, etc. |
tasks_events.go |
Task event timeline, per-turn output serving, turn usage | GET /api/tasks/{id}/events, GET /api/tasks/{id}/outputs/{filename}, GET /api/tasks/{id}/turn-usage |
tasks_autoimplement.go |
Automation watchers: auto-promoter, auto-retrier, auto-tester, auto-submitter, auto-review, waiting-sync | StartAutoPromoter(), StartAutoRetrier(), StartAutoReview(), etc. |
stream.go |
SSE streaming for live task updates and agent logs | GET /api/tasks/stream, GET /api/tasks/{id}/logs |
config.go |
Server configuration (autoimplement flags, harness list, watcher health) | GET /api/config, PUT /api/config |
env.go |
Environment configuration (API tokens, model settings, harness routing) | GET /api/env, PUT /api/env, POST /api/env/test |
git.go |
Git workspace operations (status, push, sync, rebase, branches, checkout) | GET /api/git/status, POST /api/git/push, POST /api/git/sync, etc. |
execute.go |
Task execution trigger (delegates to runner) | , (internal, called by task status transitions) |
oversight.go |
Task oversight summary retrieval | GET /api/tasks/{id}/oversight (impl + test phases via ?phase=) |
spans.go |
Span timing statistics (per-task and aggregate) | GET /api/debug/spans, GET /api/tasks/{id}/spans |
debug.go |
Health check and board manifest | GET /api/debug/health, GET /api/debug/board, GET /api/tasks/{id}/board |
sandbox_gate.go |
Harness usability checks (auth validation before task launch) | , (internal helpers) |
watcher.go |
Shared two-phase watcher helper used by the autoimplement loops in tasks_autoimplement.go |
TwoPhaseWatcherConfig, runTwoPhase() |
agentsession.go |
Agent session chat: messages, streaming, interrupt, commands | GET/POST/DELETE /api/agent/messages, GET /api/agent/messages/stream, POST /api/agent/messages/interrupt, GET /api/agent/commands |
agentsession_tool.go |
Plan task-mode tools, including prompt refinement | POST /api/agent/tool/update_task_prompt |
agentsession_threads.go |
Agent session CRUD (list, create, rename, archive, unarchive, activate) | GET/POST /api/agent/sessions, PATCH /api/agent/sessions/{id} |
planning_undo.go |
Undo the caller session's most recent planning round via git revert; cancels board tasks whose dispatched_task_id was added in the reverted commit |
POST /api/agent/undo |
specs.go |
Spec tree with metadata, progress, and archive/unarchive transitions | GET /api/specs/tree, GET /api/specs/stream, POST /api/specs/transition |
specs_dispatch.go |
Atomic dispatch/undispatch pipeline that creates board tasks from validated leaf specs and writes dispatched_task_id back into the spec frontmatter |
POST /api/specs/transition (action: dispatch|undispatch) |
terminal.go |
WebSocket terminal relay for the host shell | GET /api/terminal/ws |
device_auth.go |
Local device-code sign-in (RFC 8628) against the latere.ai auth service; the done-poll mints the session cookie | POST /api/auth/device/start, GET /api/auth/device/poll, POST /api/auth/device/cancel |
github.go / github_auth.go / github_write.go |
GitHub connection status and brokered write surfaces | GET /api/github/auth/status, POST /api/github/auth/connect, POST /api/github/pulls, POST /api/github/comments |
tasks_pr.go |
Task-level pull-request panel operations | GET/POST /api/tasks/{id}/pr, POST /api/tasks/{id}/pr/comment |
whiteboard.go |
Per-workspace whiteboard document persistence | GET /api/whiteboard, PUT /api/whiteboard |
graph.go |
Unified spec+task dependency graph for Mission Control | GET /api/graph |
speccomments.go / commentrelay.go |
Inline spec comments and the coordination relay | GET/POST /api/spec-comments, GET /api/spec-comments/stream, GET /api/coordination/status, POST /api/coordination/opt-in |
There is no refine.go (prompt refinement is the agent session tool above) and no containers.go / GET /api/containers endpoint.
The internal/logger package provides named loggers built on log/slog:
| Logger | Component tag | Used by |
|---|---|---|
logger.Main |
main |
CLI startup, server lifecycle, shutdown |
logger.Runner |
runner |
Orchestration, turn loop, commit pipeline |
logger.Store |
store |
Task persistence, state transitions |
logger.Git |
git |
Worktree and git operations |
logger.Handler |
handler |
HTTP request handling, automation watchers |
logger.Recovery |
recovery |
Orphaned task recovery on startup |
logger.Prompts |
prompts |
System prompt template management |
logger.Init(format) configures all loggers. Two formats are supported:
"text"(default), Human-friendly output with ANSI colors (when stdout is a terminal), aligned columns: timestamp, 3-char level badge, 8-char component, source file:line, bold message, dim key=value pairs. RespectsNO_COLORandTERM=dumb."json", Structured JSON viaslog.NewJSONHandler, suitable for log aggregation.
logger.Fatal(msg, args...) prints a user-friendly error to stderr and exits with code 1 (used for startup errors, not for runtime failures).
Concurrency, Store.mu for task map integrity; Runner.worktreeMu for filesystem ops; per-repo mutex for rebase serialization; per-task mutex for oversight generation. See Data & Storage for the concurrency model.
Recovery, On startup, RecoverOrphanedTasks inspects in_progress and committing tasks against actual process and worktree state, recovering or failing them as appropriate.
Security, API key + cookie/JWT auth, SSRF-hardened gateway URLs, path traversal guards, CSRF protection, request body size limits, superadmin gating for admin routes.
Circuit breakers, Per-watcher exponential backoff suppresses individual automation loops on failure; an agent-launch circuit breaker blocks launches when a CLI is unavailable. See Automation.
Observability, SSE event streams, append-only trace timeline per task, span timing, Prometheus-compatible metrics. See API & Transport for the metrics reference.
Middleware, See API & Transport for the middleware chain.
Harness routing, See Workspaces & Configuration for per-activity harness and model routing.
Graceful shutdown, See API & Transport for the shutdown sequence.
- Development Setup, building from source, tests, make targets, releases
- Data & Storage, persistence, models, migrations, search index
- Task Lifecycle, states, turn loop, dependencies, board context
- Git Operations, worktrees, commit pipeline, branch management
- Workspaces & Configuration, workspace manager, AGENTS.md, harnesses, templates
- API & Transport, HTTP routes, SSE, metrics, middleware
- Automation, watchers, auto-retry, circuit breakers