OpsQuest architecture¶
OpsQuest separates product flow, declarative mission content, isolated execution, and durable player progress. The central seam is game.Environment: a mission session can execute commands and observe outcomes without knowing whether the attempt uses the in-memory Linux simulator or the optional Docker adapter.
System landscape¶
Editable source: system-landscape.excalidraw
The landscape diagram focuses on the command-execution and persistence path; the optional read-only companion projection is detailed separately below.
There are four materially different state domains:
| Domain | Owner | Lifetime | Host interaction |
|---|---|---|---|
| Mission definition | internal/mission |
Embedded in the binary | Reads embedded JSON only |
| Mission attempt | internal/sandbox or internal/dockerlab |
One attempt or restart | In-memory for Linux; exact labeled Docker resources for Docker labs |
| Player progress | internal/profile |
Across processes | Atomic profile.json replacement in the platform config directory |
| Companion projection | internal/webapp |
One play --web process |
Loopback HTTP only; no durable writes or command execution |
Linux mission state is never persisted. Completing, quitting, switching, or restarting a Linux mission discards its virtual filesystem, environment, processes, archives, and history. Hints, command practice, completions, XP, and achievements belong to the profile instead.
Components and dependencies¶
Editable source: component-architecture.mmd
| Package | Responsibility | Important boundary |
|---|---|---|
cmd/opsquest |
Process entry point and dependency construction | Wires concrete adapters; contains no gameplay rules |
internal/cli |
Public commands, flags, route selection, and presentation | Owns profile loading/reset and chooses missions; delegates attempts to game.Session |
internal/game |
Attempt orchestration, terminal input/editor integration, progression, and outcome evaluation | Depends on the Environment and Factory contracts, not on Docker details |
internal/mission |
Strict schema, embedded catalog, tracks, worlds, and defensive copies | Content stays declarative; rejects invalid catalogs before play starts |
internal/sandbox |
Teaching-shell lexer, parser, dispatcher, virtual filesystem/processes/archives | Never invokes a host shell or accesses host paths |
internal/dockerlab |
Optional Docker-compatible engine availability, typed teaching actions, fixtures, observations, and cleanup | Only this adapter may launch the Docker CLI, using constructed arguments and tracked resource IDs |
internal/webapp |
Optional embedded mission companion, one-time pairing, current snapshot, and server-sent events | Serves a read-only projection on IPv4 loopback; accepts no command or completion input |
internal/profile |
Versioned progress model and atomic JSON storage | The only normal durable state written by the application |
internal/ui |
Terminal capability detection and semantic ANSI roles | Styling stays out of execution results and validators |
internal/buildinfo |
Release-managed executable version | Updated by release automation |
Dependencies point toward contracts and data. In particular, dockerlab implements interfaces declared by game, while game does not import dockerlab. The composition root selects dockerlab.NewFactory(game.SandboxFactory{}), making the simulated environment the safe default and Docker an optional branch.
OrbStack does not introduce a second execution backend. On macOS it is selected through its official orbstack Docker context, while availability checks, fixed Docker CLI arguments, ownership verification, resource limits, and cleanup all remain on the same adapter path. Context inspection changes only provider-specific readiness guidance; a failed context inspection does not reject an otherwise reachable engine.
Process startup¶
cmd/opsquest/main.go performs a deliberately small composition sequence:
- Create an interrupt-aware context.
- Load and validate every embedded mission with
mission.LoadCatalog. - Resolve the profile store with
profile.DefaultStore. - Construct the CLI with standard streams, catalog, store, and the combined environment factory.
- Dispatch
os.Args[1:]throughcli.App.Run;play --webadditionally starts an ephemeral loopback companion for that command. - Render any returned error once at the process boundary and exit non-zero.
Catalog loading is fail-fast. JSON decoding disallows unknown fields, mission numbers must be globally contiguous, IDs and numbers must be unique, setup and validation fields must match the selected environment, and campaigns must remain contiguous within each track.
One mission attempt¶
Editable source: mission-runtime-sequence.mmd
The runtime flow is:
cli.Apploads the profile and selects a mission using a recommended, sequential, or world-scoped route.game.Sessionasks the configuredFactoryfor a freshEnvironmentand wraps it in managed cleanup.- The session handles mission controls and navigation itself. Other input is passed to
Environment.Execute. - Execution returns unstyled output plus safe learning metadata: practiced command names, maximum pipeline width, and an optional interactive action.
- The session records command practice and related achievements, then persists the profile.
- The validator compares output conditions directly and delegates state conditions to
Environment.Observe. - If any outcome is missing, the same attempt continues.
statusdescribes satisfied and missing outcomes without prescribing a command sequence. - Once every outcome passes, the session closes the environment before awarding XP. This prevents a Docker attempt with unresolved cleanup from being recorded as complete.
- First completion records hint-adjusted XP and achievements. Replays retain the original completion and award no duplicate XP.
- When a companion reporter is configured, the session publishes a sanitized complete snapshot after start, hints, observations, restart, pause, and persisted completion. Publishing never participates in the validation decision.
restart closes the active environment and creates a new one from the same declarative mission. Mission switching returns a validated mission ID to the CLI, which starts a separate fresh session.
The environment contract¶
An Environment represents exactly one isolated attempt:
type Environment interface {
PromptLabel() string
Execute(context.Context, string) (Execution, error)
Observe(context.Context, mission.Condition) (bool, error)
CompletionSource() CompletionSource
Close() error
}
The contract carries several design decisions:
Executeperforms only the environment's teaching subset. It does not accept an arbitrary process runner.Observeexposes outcomes rather than internal state, keeping validators environment-neutral.CompletionSourceoffers only environment-owned commands and paths. Session navigation completions are added at the game layer.Closemust tolerate partial setup and be retryable after a cleanup error.- Session calls are serial; implementations do not need concurrent
Execute/Closesupport. - Factories may return both a partial environment and an error. The managed wrapper closes that partial attempt before propagating the failure.
The simulated adapter wraps one sandbox.Sandbox. The Docker adapter maps logical mission aliases to generated names and exact container IDs, so neither the session nor player sees engine-owned resource identifiers.
Web companion contract¶
play --web starts internal/webapp on 127.0.0.1 with an ephemeral port.
The printed one-time pairing URL establishes an HTTP-only, same-site browser
session, then the browser loads static HTML, CSS, and JavaScript embedded in the
same executable. The current state is available as a JSON snapshot, and
subsequent updates use Server-Sent Events. A reconnect receives the latest
complete snapshot rather than querying the environment or replaying commands.
game.AttemptSnapshot is a dedicated public projection. It contains narrative,
suggested tool names, revealed hints, described outcome status, placement,
reward information, and completion feedback. It does not contain mission setup
objects, condition structs, player command text, terminal output, Docker IDs,
or a profile mutation capability.
The direction of authority is one-way:
- Terminal input reaches
game.Sessionand the selectedEnvironment. game.Sessionevaluates outcomes and persists progress exactly as in terminal-only play.- Only after those decisions does it publish a presentation snapshot.
internal/webappretains and broadcasts the snapshot without calling back into the session.
Completion is not published until environment cleanup and profile persistence both succeed. This retains the existing Docker cleanup-before-XP invariant.
Mission and world model¶
Each JSON file decodes into a mission.Mission containing narrative fields, suggested tools, hints, setup, validation conditions, and rewards. The catalog sorts missions by stable global number and builds lookup indexes by ID and number.
Worlds are derived views rather than persisted schema. Within each track, each contiguous campaign becomes one ordered world; a mission's world and stage placement is computed at catalog load. Linux and Docker therefore have separate world numbering even though mission numbers are global. Catalog accessors return deep copies so callers cannot mutate embedded content.
See the mission map for the current worlds and missions.
Persistence and compatibility¶
The profile schema is versioned independently from mission content. Store.Load accepts older supported data, normalizes additive fields and unsafe legacy display names, and rejects profiles written by a newer schema version. Store.Save clones map-backed state, validates the player name, writes an owner-only temporary file, syncs it, and atomically renames it over profile.json.
Compatibility-sensitive identifiers are:
- Mission IDs, used as keys for completions and hint progress.
- Global mission numbers, used by top-level public navigation.
- Profile schema version and JSON fields.
- Condition names and their allowed field shapes.
- Track/campaign ordering, which determines displayed world placement.
Changing one of these requires explicit compatibility reasoning even when the Go compiler reports no breakage.
Where changes belong¶
| Desired change | Primary location | Usually also inspect |
|---|---|---|
| Add or revise a mission | internal/mission/data |
Catalog integrity and internal/game/missions_test.go |
| Add a simulated command or flag | internal/sandbox |
Command help, regression/hardening tests, README command list |
| Add a validation outcome | internal/mission, internal/game, environment adapters |
Strict schema checks and canonical mission coverage |
| Change attempt controls or progression | internal/game |
internal/cli routes and profile persistence |
| Add a top-level command | internal/cli |
Smoke test and README examples |
| Change the browser companion or projection | internal/webapp, internal/game |
Pairing/HTTP isolation, lifecycle ordering, CLI fallback, and race tests |
| Expand Docker teaching behavior | internal/dockerlab |
Parser rejection tests, ownership cleanup, real integration gate |
| Change colors or terminal presentation | internal/ui |
CLI/session rendering tests and non-terminal output |
| Change durable progress | internal/profile |
Migration/compatibility tests and atomic-write behavior |
The repository's agent guide defines the safety invariants and quality gates for each category.