Appearance
System Architecture
IMPORTANT
TL;DR — LoopTroop is a local control plane: a React browser client talks to a Hono backend, durable state lives in SQLite plus .ticket/** artifacts, each ticket executes in its own git worktree, and all model work goes through OpenCode behind a session-ownership layer. On restart, the backend rebuilds runtime projections, hydrates ticket actors, and reconnects only the sessions it still owns.
This document is the canonical architecture reference for the current LoopTroop application.
LoopTroop is not a thin chat wrapper around a coding model. It is a long-running workflow system with explicit planning phases, durable storage, isolated execution worktrees, resumable ticket actors, and restart-aware OpenCode session ownership. The core architectural rule is simple: important state must survive the model and survive the browser.
1. Mental Model
LoopTroop operates as a layered local system:
- The browser runs a React SPA plus local providers for UI state, log caching, and AI question handling.
- A Hono API owns the REST and SSE boundary and applies auth, rate limiting, and JSON validation before requests reach workflow code.
- XState ticket actors hold the live workflow state machine, while the workflow runner dispatches the phase-specific handlers.
- Council orchestration and structured-output normalization turn raw model text into bounded, typed artifacts before anything becomes canonical.
- Durable truth lives in SQLite,
.ticket/**artifacts, runtime projections, and JSONL logs rather than in model transcripts. - OpenCode sessions do the model work inside isolated ticket worktrees, and Git/GitHub delivery turns the resulting change set into a PR outcome.
2. Runtime Actors
| Actor | Responsibility | Primary modules |
|---|---|---|
| Browser shell | App bootstrap, modal/URL coordination, dashboard vs ticket workspace switching, startup notices | src/main.tsx, src/App.tsx, src/components/layout/* |
| Browser state providers | Persistent UI state, AI question queue, per-ticket log cache and recovery | src/context/UIContext.tsx, src/context/AIQuestionContext.tsx, src/context/LogContext.tsx |
| React Query + SSE hooks | Fetching, cache invalidation, replay recovery, startup-status fetches | src/hooks/*, especially useTickets.ts, useTicketArtifacts.ts, useSSE.ts, useStartupStatus.ts |
| Hono API | REST routes and SSE endpoint under /api | server/index.ts, server/routes/* |
| API guard rails | CORS, token auth, per-bucket rate limiting, JSON validation | server/middleware/* |
| Ticket state machine | Canonical ticket statuses, legal transitions, blocked-error resume rules | server/machines/ticketMachine.ts, server/machines/types.ts |
| Actor persistence and hydration | Snapshot reconciliation, safe restore, startup hydration of non-terminal tickets | server/machines/persistence.ts, server/startup.ts |
| Phase orchestrators | Planning, approval, execution, retry, cleanup, and delivery logic | server/workflow/*, server/phases/* |
| Council + structured output layer | Draft/vote/refine orchestration, tagged-output parsing, schema normalization, retry diagnostics | server/council/*, server/structuredOutput/*, server/phases/parserTaggedStructuredOutput.ts, server/lib/structuredOutput*.ts |
| OpenCode integration | Session creation, prompting, event/question translation, ownership validation, blocked-error diagnostics | server/opencode/* |
| SSE broadcaster + execution logging | Ticket-scoped fan-out, replay buffer, durable log ingestion, session-status log translation | server/sse/broadcaster.ts, server/log/*, server/workflow/sessionStatusLogging.ts |
| App database | Singleton profile, attached projects, startup UI meta | server/db/index.ts, server/db/init.ts |
| Project database + ticket storage | Tickets, artifacts, phase attempts, OpenCode sessions, error history, runtime projections | server/db/project.ts, server/storage/* |
| Git and GitHub layer | Worktrees, diffs, commits, PR creation, merge/close flows | server/phases/execution/gitOps.ts, server/git/* |
| Startup bootstrap | Database init, crash recovery, WSL/runtime diagnostics, actor hydration, session reconnect | server/startup.ts, server/startupState.ts, server/runtime.ts |
3. Authoritative Data Ownership
LoopTroop deliberately splits state across several storage layers. Each layer owns a different class of truth.
| Storage location | Canonical contents | Notes |
|---|---|---|
~/.config/looptroop/app.sqlite by default | Singleton profile, attached projects, app meta such as startup restore-notice dismissal | Configurable via LOOPTROOP_CONFIG_DIR or LOOPTROOP_APP_DB_PATH |
<project>/.looptroop/db.sqlite | Tickets, runtime metadata, OpenCode session ownership, phase_artifacts, ticket_phase_attempts, status history, error occurrences | This is the project-local operational database |
<project>/.looptroop/worktrees/<ticket>/ | The isolated ticket worktree used for planning artifacts, runtime files, and code changes | Startup blocks if .looptroop is tracked by Git so stale runtime data cannot be checked out into new worktrees; .ticket/** stays local LoopTroop state and is excluded from bead commits and PR diffs |
.ticket/relevant-files.yaml | Relevant-file scan output used by later planning phases | Replaces older codebase-map.yaml terminology |
.ticket/interview.yaml and .ticket/prd.yaml | Editable review artifacts for the approved planning stages | These are user-facing canonical documents |
.ticket/beads/<flow>/.beads/issues.jsonl | The current bead plan for a given flow or base branch | Stored as JSONL, rewritten atomically on updates |
.ticket/runtime/execution-log.jsonl, .debug.jsonl, .ai.jsonl | Durable workflow, debug, and AI-detail log channels | The UI consumes these both live through SSE and on reload through /api/files/:ticketId/logs |
.ticket/runtime/state.yaml | Derived runtime projection for the active non-terminal ticket | Rebuilt from ticket state on startup; convenient to inspect, but not the only source of truth |
.ticket/runtime/execution-setup-profile.json | Concrete execution environment profile produced after approved setup runs | Separate from the reviewable execution setup plan artifact |
.ticket/runtime/execution-setup/**, especially tool-cache/ | Ticket-owned temp roots, wrapper outputs, execution-only toolchains, and reusable caches | Preserved across setup-plan rewinds when safe so retries do not throw away valid tool caches |
phase_artifacts table | Structured snapshots used by the API and UI | Holds artifact content, phase, attempt number, timestamps, approval receipts, edit receipts, cleanup/integration reports, and content hashes |
Note SQLite and the filesystem are complementary, not redundant. The database is optimized for querying, ownership, and workflow bookkeeping;
.ticket/**keeps artifacts inspectable, editable, and recoverable without polluting the target repository branch.
4. End-to-End Ticket Lifecycle
- A ticket starts in
DRAFTwith editable title, description, and priority. SCANNING_RELEVANT_FILEScreatesrelevant-files.yamlfrom the ticket description and repo context.- The interview council drafts, votes, refines the interview artifact, and iterates until interview coverage is good enough.
- The user approves the interview artifact.
- The PRD council drafts, votes, refines, and coverage-checks the spec.
- The user approves the PRD artifact.
- The beads council drafts, votes, refines, expands, and coverage-checks the execution plan.
- The user approves the beads artifact and then reviews the pre-implementation execution setup plan.
- Implementation runs bead by bead in an isolated ticket worktree, with bounded retry per bead.
- Post-implementation final testing, file-effects auditing, integration, PR creation, review follow-up, and cleanup drive the ticket to
COMPLETED,CANCELED, orBLOCKED_ERROR.
The full phase map lives in Ticket Flow & State Machine.
5. Planning Flow
Planning is intentionally artifact-driven.
| Stage | Primary input | Primary output | Why it exists |
|---|---|---|---|
| Discovery scan | Ticket details | relevant-files.yaml | Grounds planning in the actual codebase |
| Interview council | Ticket details, relevant files | Interview document and answer session | Forces ambiguity out before specs |
| PRD council | Ticket details, interview, relevant files, member-specific Full Answers | PRD document | Produces the feature contract |
| Beads council | Ticket details, PRD, relevant files | Execution bead plan | Converts the spec into execution units |
| Execution setup planning | Ticket details, PRD, beads, execution profile | Reviewable setup plan | Makes the coding environment explicit before code changes begin |
The planning phases are not one long conversation. Each stage assembles a new context window from durable artifacts and runs in its own session scope.
Councils are a reusable subsystem, not bespoke logic embedded in each phase. server/council/pipeline.ts handles the common draft -> quorum -> vote -> refine shape, while each phase provides its own context and normalization rules.
Structured output is a hard boundary. server/structuredOutput/* and server/phases/parserTaggedStructuredOutput.ts normalize, validate, and optionally repair model output before anything becomes canonical artifact content. Rejected or uncorrectable responses are preserved as diagnostics and raw attempts so downstream phases never consume malformed text as if it were approved state.
Human approval gates are content-addressed. The API exposes the current artifact hash for interview, PRD, beads, and execution setup plan views; approval requests must send expectedContentSha256; stale approvals return 409 instead of approving bytes the user did not review. Approval snapshots and receipts keep the reviewed raw content plus content_sha256, and interview/PRD receipts also record the post-stamp stored hash when approval metadata changes the YAML.
6. Execution Flow
Execution is built around beads, not around one monolithic coding prompt.
PRE_FLIGHT_CHECKverifies the ticket can enter pre-implementation setup, including worktree cleanliness before setup starts.WAITING_EXECUTION_SETUP_APPROVALpauses for setup-plan review before setup commands run.PREPARING_EXECUTION_ENVcreates the temporary execution environment described by the approved setup plan, provisioning missing required tooling under ticket-owned runtime roots, exposing setup-scoped OpenCodewebsearch/webfetchfor unresolved official launcher artifact lookup, validating declared wrappers andtooling_probe_commands, requiringtool_requirements.provisioning_attemptsevidence for failed launcher provisioning, and rejecting ready results that leave committable project changes behind.CODINGselects the next runnable bead from the scheduler.executeBead()starts or reattaches to the owned OpenCode session for that bead attempt.- The model must emit the expected structured bead status markers. Missing or malformed markers trigger a structured retry path instead of silently progressing.
- If the attempt stalls or fails, LoopTroop generates a context wipe note, resets the worktree to the bead start commit, and retries in fresh context.
- When the bead succeeds, LoopTroop finalizes it locally before marking it done: changed work must be committed, true no-op work may complete without a commit, push failures are warnings, and fatal finalization failures route to
BLOCKED_ERROR. RUNNING_FINAL_TEST,INTEGRATING_CHANGES, andCREATING_PULL_REQUESTpackage the result for post-implementation delivery, with final-test commands automatically reusing a validated setup wrapper and PR creation auditing the final candidate files before anything is pushed.- During all non-terminal execution states, runtime projections and execution logs are updated so a restarted backend or reloaded browser can restore the ticket from durable state rather than from memory.
See Beads & Execution.
7. Recovery Flow
Recovery is a first-class architectural concern.
| Failure type | Recovery strategy |
|---|---|
| Browser reload, close, or reconnect gap | REST state remains canonical; the browser keeps the last SSE event id, restores best-effort log cache detail, replays buffered live events, and then refetches tickets, artifacts, bead state, interview state, and matching server logs |
| Frontend crash or tab close | Interview drafts, approval drafts, and browser-cached logs are persisted locally and flushed on unload with best-effort keepalive behavior |
| Crash during atomic write or append | Startup promotes orphan .tmp files, repairs trailing corrupt JSONL lines when safe, and rebuilds runtime projections |
| Invalid model output | Retry with repair or explicit re-prompt, depending on phase |
| Bead execution stall | Generate context wipe note, reset worktree, retry in fresh session |
| OpenCode reconnect gap | Validate the exact owned session against remote sessions and recreate only when ownership can no longer be proven |
| Backend process restart | Reconcile persisted XState snapshots, hydrate ticket actors from durable ticket state, and immediately process restored active snapshots |
| User edits approved interview or PRD | Archive the active approved generation and downstream attempts, cancel downstream sessions intentionally, clear stale downstream artifacts/UI state, persist a user_edit_receipt:*, and restart from the next drafting phase |
| User edits or regenerates setup plan during runtime setup | Stop active runtime setup, archive both setup-plan and runtime attempts, preserve the tool cache when safe, clear stale setup outputs, and return to WAITING_EXECUTION_SETUP_APPROVAL for fresh approval |
| Stale approval | Return 409 with the expected and current SHA-256 hashes, keeping the ticket at the approval gate |
| Bead finalization failure | Keep the bead retryable, avoid bead_complete, send BEAD_ERROR with BEAD_FINALIZATION_FAILED, and route to BLOCKED_ERROR |
| Cleanup warning | Persist a cleanup_report with status: warning, expose the cleanup summary on the ticket, and still complete the ticket |
| Terminal blockage | Enter BLOCKED_ERROR with persisted error occurrence history |
LoopTroop tries hard to preserve the work product while discarding the bad conversational state that produced the failure.
If a resume point cannot be proven, recovery stops at BLOCKED_ERROR instead of continuing execution against unknown state. BLOCKED_ERROR retry requires a preserved previousStatus; CODING retry also requires a successful reset to the failed bead's beadStartCommit.
8. Restart And Session Ownership
LoopTroop pairs persisted ticket snapshots with OpenCode session ownership records so it can decide whether a remote session still belongs to the exact workflow slot that wants to use it.
Ownership keys can include:
ticketIdphasephaseAttemptmemberIdbeadIditerationstep
This lets LoopTroop safely reconnect in cases like:
- server restart while a ticket is still in the same phase
- phase retry that should resume the currently owned session
- multi-model council phases where each member has its own session identity
- bead execution where iteration and bead identity both matter
Reconnect deliberately does not mean "resume any random old transcript." Validation succeeds only if the ticket is still in the same workflow state, the project database still records that ownership slot as active, and the exact remote session still exists.
Snapshot restore is equally defensive. server/machines/persistence.ts reconciles persisted XState snapshots with the ticket row; if the snapshot is missing required structure, cannot be reconciled, or would resume from an unprovable state, the ticket is rebuilt conservatively or moved to BLOCKED_ERROR instead of guessing.
Prompt acquisition is bounded by timeout and abort signals. OpenCode create, list, getSession, and message-read calls are guarded so an OpenCode restart cannot indefinitely block the workflow runner.
9. Module Map
Frontend
| Area | Modules |
|---|---|
| App bootstrap and query client | src/main.tsx, src/lib/queryClient.ts |
| App shell, modal routing, startup overlays | src/App.tsx, src/components/layout/*, src/components/shared/StartupRestorePopup.tsx, src/components/shared/WelcomeDisclaimer.tsx |
| Ticket workspace and shared UI | src/components/ticket/*, src/components/workspace/*, src/components/shared/* |
| Browser state providers | src/context/UIContext.tsx, src/context/AIQuestionContext.tsx, src/context/LogContext.tsx |
| Data hooks and live updates | src/hooks/useTickets.ts, useTicketArtifacts.ts, useTicketPhaseAttempts.ts, useWorkflowMeta.ts, useSSE.ts, useStartupStatus.ts, useRecoveryAutoReload.ts |
API Surface
| Area | Modules |
|---|---|
| App entry and route mounting | server/index.ts |
| Middleware and request guards | server/middleware/apiAuth.ts, rateLimit.ts, validation.ts |
| Ticket routes and modular ticket handlers | server/routes/tickets.ts, server/routes/ticketHandlers/* |
| Files, beads, streaming | server/routes/files.ts, beads.ts, stream.ts |
| Profile, projects, health, models, workflow meta | server/routes/profiles.ts, projects.ts, health.ts, models.ts, workflow.ts |
Ticket Orchestration
| Area | Modules |
|---|---|
| Ticket status machine | server/machines/ticketMachine.ts, server/machines/types.ts |
| Actor persistence and restore | server/machines/persistence.ts |
| Workflow runner and phase dispatch | server/workflow/runner.ts, server/workflow/phases/* |
| Planning phases | server/phases/interview/*, server/phases/prd/*, server/phases/beads/*, server/phases/executionSetupPlan/* |
| Execution and delivery phases | server/phases/preflight/*, server/phases/executionSetup/*, server/phases/execution/*, server/phases/finalTest/*, server/phases/integration/*, server/phases/cleanup/*, server/phases/verification/* |
| Ticket initialization and relevant-file preparation | server/ticket/create.ts, server/ticket/initialize.ts, server/ticket/relevantFiles.ts, server/ticket/metadata.ts |
Council And Structured Output
| Area | Modules |
|---|---|
| Reusable council pipeline | server/council/pipeline.ts, drafter.ts, voter.ts, refiner.ts, quorum.ts |
| Structured-output schemas and normalizers | server/structuredOutput/* |
| Tagged marker extraction and repair-aware parsing | server/phases/parserTaggedStructuredOutput.ts |
| Retry policy, raw-attempt capture, prompt echo detection | server/lib/structuredOutputRetry.ts, structuredRawAttempts.ts, structuredRetryDiagnostics.ts, promptEcho.ts |
Persistence
| Area | Modules |
|---|---|
| App DB connection and schema bootstrap | server/db/index.ts, server/db/init.ts |
| Project DB bootstrap and schema | server/db/project.ts, server/db/schema.ts |
| Project attach, repo-path normalization, local exclude setup | server/storage/projects.ts, server/storage/paths.ts, server/git/repository.ts |
| Ticket artifact and attempt storage | server/storage/ticketArtifacts.ts, ticketPhaseAttempts.ts, ticketMutations.ts, ticketQueries.ts |
| Ticket runtime projection | server/storage/ticketRuntimeProjection.ts |
Observability And Recovery
| Area | Modules |
|---|---|
| SSE replay and fan-out | server/sse/broadcaster.ts, server/routes/stream.ts |
| Execution log ingestion and dedupe | server/log/executionLog.ts, readDedupe.ts, commandLogger.ts |
| Startup bootstrap and restore state | server/startup.ts, server/startupState.ts, server/runtime.ts |
| Crash-safe IO and recovery | server/io/atomicWrite.ts, atomicAppend.ts, jsonl.ts, recovery.ts |
OpenCode Integration
| Area | Modules |
|---|---|
| Adapter and factory | server/opencode/adapter.ts, server/opencode/factory.ts |
| Context assembly | server/opencode/contextBuilder.ts |
| Session ownership and reconnect | server/opencode/sessionManager.ts |
| Prompt runner and phase bridge | server/workflow/runOpenCodePrompt.ts |
| Question handling and blocked-error mapping | server/routes/ticketHandlers/openCodeQuestionHandlers.ts, server/opencode/blockedErrorDiagnostics.ts |
10. ASCII Overview
text
User
|
v
React SPA + browser providers
| REST (/api/*) SSE (/api/stream)
|-------------------------------------> Hono API <--------------------+
| | |
| v |
| auth / rate limit / validation |
| | |
| v |
| routes + ticket handlers |
| | |
| v |
| ticket machine + workflow runner |
| | | | |
| | | | |
| v v v |
| council pipeline phase logic OpenCode |
| | adapter |
| v | |
| structured output layer v |
| OpenCode |
| server |
| | |
| v |
| provider models
|
+<--------------------------- SSE broadcaster <-------------------+
^ ^
| |
project DB runtime logs/state
^ ^
| |
app DB / project DB / ticket worktree /.ticket/**
Startup bootstrap runs before normal traffic:
initialize DB -> recover temp/log files -> rebuild runtime projections ->
hydrate ticket actors -> reconnect owned OpenCode sessions11. Detailed Mermaid Diagram
12. Startup State System
On startup, LoopTroop restores durable state through server/startup.ts and server/startupState.ts. The workflow runner hydrates ticket actors from the project database, and the OpenCode layer attempts to reconnect only sessions that still match a valid ownership record.
Bootstrap Sequence
startupSequence() performs the runtime bootstrap in this order:
- Initialize the app/project databases and create runtime indexes.
- Classify the startup storage state and capture runtime diagnostics such as WSL mounted-drive warnings.
- Recover ticket runtime artifacts by promoting orphan
.tmpfiles, repairing trailing JSONL corruption where safe, and rebuilding.ticket/runtime/state.yamlprojections. - Start the WAL checkpoint timer and probe OpenCode health.
- Hydrate XState actors for non-terminal tickets from attached project databases.
- Validate and reconnect active OpenCode sessions; records that no longer map to a live owned session are marked abandoned.
Startup Classification
classifyStartupStorageKind() determines the current storage condition:
| Kind | Meaning |
|---|---|
fresh | First-ever startup — no prior app database exists. |
empty_existing | App database exists but has no attached projects. |
restored | Database found with existing projects — full state restoration. |
Restore Flow
initializeStartupState()reads the app database path, profile count, and attached project count, then persists the startup classification.getStartupStatus()exposes the cached startup snapshot throughGET /api/health/startup.- The frontend uses
useStartupStatus()andStartupRestorePopupto show restore context after a real restore. dismissStartupRestoreNotice()persists the user's dismissal in app metadata so the restore notice does not keep reappearing.
Session recovery is best-effort. If OpenCode is unavailable during startup, ticket actors are still hydrated from durable workflow state and later phase work either reconnects, creates fresh owned sessions, or blocks with a persisted error.
The startup health endpoint exposes the storage path, kind, source, runtime warning state, restored project list, dismissed state, and human-readable summary for diagnostics and UI messaging.
13. IO Utilities
The IO layer provides crash-safe file operations and recovery used by the workflow engine. All modules live in server/io/.
| Module | Purpose | Key Export |
|---|---|---|
atomicWrite.ts | Crash-safe file writes | safeAtomicWrite(filePath, content) — writes to a .tmp file, calls fsync, renames to the target path, then best-effort fsyncs the parent directory. Prevents partial overwrites on system failure. |
atomicAppend.ts | Crash-safe line appends | safeAtomicAppend(filePath, line) — opens with the a+ flag, checks trailing newline, adds a \n prefix when needed, then calls fsync. Used for durable JSONL log appends. |
jsonl.ts | JSON Lines I/O | readJsonl<T>(), writeJsonl<T>(), appendJsonl<T>() — type-safe JSONL read/write/append with graceful malformed-line skipping and newline integrity. |
recovery.ts | Crash recovery | recoverOrphanTmpFiles(folder) — recursively promotes .tmp files to their target paths after a crash; fixTrailingLineCorruption(filePath) — validates and truncates trailing corrupt JSONL lines (scans backward in 8KB chunks, stays under 4MB scan limit). |
These utilities form the durability backbone: atomic writes protect mutable state files (YAML and JSON artifacts), atomic appends protect append-only logs, and recovery handles the edge case where a process stops mid-write.
14. Session Status Logging
OpenCode session status events are translated into normalized log entries by server/workflow/sessionStatusLogging.ts. Each entry captures a retry event or phase change as a structured execution-log record so live SSE views and reload-time log reads converge on the same timeline.
buildSessionStatusLogEntries() converts OpenCode SessionStatusStreamEvent objects into SessionStatusLogEntry[] — ordered, typed log entries with:
| Field | Meaning |
|---|---|
id | Stable entry identifier |
type | info or error |
kind | session or error |
op | append, upsert, or finalize — determines how the log viewer merges this entry |
content | Human-readable description of the status event |
The log builder handles retry status events (rate limits, usage limits, timeouts, transport errors) and session phase transitions. These entries feed the normal execution log alongside phase log entries, while the separate AI-detail log keeps prompt/tool-call depth when that channel is needed.