This demo is a Next.js App Router game collection used to demonstrate agentic-lore-coding. It opens to a card-based menu with Classic games like Snake, Tetris, Breakout, Minesweeper, Asteroids, Tank Patrol, and many others.
All the code in this repository was created using AI agents and the Lore Coding Method. The code was reviewed, but never edited in an IDE. Instead, all edits were performed using an AI agent while working on a specific task.
- Ten classic games with deterministic gameplay rules and polished browser controls.
- Tank Patrol provides a single-player campaign across 35 maps and the original 70-stage difficulty cycle, including Stage 1-35 selection, NES-pixel movement, partial wall destruction, NTSC-paced tank and shell lifecycles, enemy waves, timed results, and a headquarters to defend. Its online private-room mode adds server-authoritative two-player co-op starting from Stage 1 with original-style rules and spawn points.
- Asteroids includes vector-style ship thrust, wraparound movement, asteroid splitting, UFO saucers, waves, lives, bonus lives, scoring, and persistent board power-ups, and difficulty-scoped records.
- Simon uses Easy, Medium, and Hard difficulty presets for target sequence length while keeping records scoped to the selected difficulty.
- Minesweeper uses traditional Easy, Medium, and Hard minefield presets with records scoped to the selected difficulty.
- Snake includes level progression through key-and-door exits, timed special foods, obstacle islands, and a full-board win state.
- Space Invaders marks provoked aliens with rising embers and a pixel exclamation mark for two seconds before they fire, keeping the warning distinct from protective shields. A brief yellow-white muzzle flash beneath each firing alien identifies the source of its shot. Blue aliens fire cyan needles that player shots cannot destroy; player shields still absorb them safely.
- SQLite-backed, parameter-scoped top-three leaderboards for every game. Minesweeper ranks fastest clears; the other games rank higher scores.
- Name-and-password player accounts with private profile stats for signed-in play sessions, including total play time and per-game best metrics.
- Snake, Tetris, Breakout, Minesweeper, Space Invaders, Pong, Simon, 2048, Asteroids, and the Tank Patrol single-player campaign support replay recording with server-issued runs, signed-in replay saves, and profile playback for the latest saved run.
- Closable in-game Help screens and Escape-to-menu abandon confirmations.
- Local game-card artwork for every game in the launcher.
docker run --rm -p 3000:3000 aleksandrvoitenko/lore-coding-demo-classic-gamesOpen http://localhost:3000 to choose a game from the menu.
Install dependencies:
npm installRun the development server:
npm run devOpen http://localhost:3000 to choose a game from the menu.
For local multiplayer testing on another device in the same network, run both the sidecar and Next dev server through the wrapper:
npm run dev:multiplayerThe wrapper binds Next and the sidecar to 0.0.0.0, discovers a private LAN IPv4
address for browser WebSocket URLs, prints the shareable app URL, and adds that
exact host to Next's development-origin allowlist. Override the detected address
when the machine has multiple network interfaces:
MULTIPLAYER_DEV_PUBLIC_HOST=10.125.3.39 npm run dev:multiplayerThe detected or overridden public host must be a bare dotted-decimal IPv4 address. The wrapper rejects protocols, ports, paths, hostnames, IPv6 literals, and wildcards instead of forwarding them to Next's development-origin matcher.
Optional wrapper overrides include MULTIPLAYER_DEV_NEXT_PORT,
MULTIPLAYER_DEV_SIDECAR_PORT, and
MULTIPLAYER_DEV_SNAPSHOT_INTERVAL_MS. Sidecar-specific settings such as
MULTIPLAYER_SIDECAR_MAX_ROOMS are inherited by the child process. Next still
talks to the sidecar through
127.0.0.1; only browser-facing URLs use the detected LAN address.
The wrapper also generates one ephemeral room-service bearer secret and passes
it to both child processes unless a matching secret is already configured.
Build and run the experimental multiplayer room sidecar in a separate terminal:
npm run start:sidecarThe start script rebuilds the sidecar before launching it so local runtime code matches the current TypeScript source.
The sidecar defaults to 127.0.0.1:3001, exposes GET /healthz, accepts public
room WebSocket upgrades on /multiplayer/rooms, and serves the internal JSON
room service on /_internal/multiplayer/rooms:
POST /_internal/multiplayer/rooms/v6creates a room.GET /_internal/multiplayer/rooms/<code>reads a room snapshot.POST /_internal/multiplayer/rooms/v6/<code>applies a parsed room command.POST /_internal/multiplayer/rooms/v6/_accountsapplies a protected presence or authenticated account-party command.
Override the bind address and path with MULTIPLAYER_SIDECAR_HOST,
MULTIPLAYER_SIDECAR_PORT, MULTIPLAYER_SIDECAR_WEBSOCKET_PATH, and
MULTIPLAYER_SIDECAR_ROOM_SERVICE_PATH when wiring it behind a local proxy. The
sidecar retains at most 256 rooms by default; set
MULTIPLAYER_SIDECAR_MAX_ROOMS to a positive integer when a deployment needs a
different per-process capacity. A party allows eight watchers and four live
WebSocket tabs per participant by default. Tune those independent fanout bounds
with MULTIPLAYER_SIDECAR_MAX_OBSERVERS_PER_PARTY (a non-negative integer) and
MULTIPLAYER_SIDECAR_MAX_CONNECTIONS_PER_PARTICIPANT (a positive integer).
The sidecar pushes fresh snapshots for subscribed running rooms every 33ms by
default; tune that with MULTIPLAYER_SIDECAR_SNAPSHOT_INTERVAL_MS during
latency testing. Use 16 for local full-snapshot experiments, keep 33 for
production-like smoothing work, and use 50 as a lower-bandwidth fallback. Full
16ms snapshots are useful for local comparison against
the fastest game ticks, but production multiplayer should rely on client visual
interpolation, projection, local input prediction, and reconciliation over
authoritative sidecar snapshots instead of broadcasting every render frame. Next
API routes continue using their local in-process room store unless
MULTIPLAYER_ROOM_SERVICE_URL points at the sidecar room service base URL, for
example http://127.0.0.1:3001/_internal/multiplayer/rooms.
Multiplayer room state is intentionally volatile. The sidecar keeps room ordering, live cursor catch-up, and game state in process memory only; it does not write inputs, ticks, power-up awards, or other per-event room history into SQLite. Restarting the sidecar abandons waiting and active rooms owned by that process, so players should create a new room after a restart instead of expecting replay-log recovery.
Tank Patrol private rooms require both Player 1 and Player 2 seats and always begin at Stage 1. Each player starts at the original P1/P2 spawn point with three lives and keeps an individual score, upgrade tier, and stage kill totals. A friendly shell is consumed on impact and briefly stuns an unprotected teammate instead of destroying them. The co-op enemy field allows six simultaneous tanks and advances enemy spawning faster than solo play; the team remains in the battle until both players are eliminated or the headquarters is destroyed. Star, helmet, tank, pickup score, and score-earned extra-life rewards belong to the collecting player, while grenade, clock, and shovel effects apply to the shared battlefield. Stage results show both players separately, and a surviving strict kill leader receives the original 1,000-point bonus; ties receive no bonus.
These multiplayer sessions remain separate from Tank Patrol's single-player replay and leaderboard paths. Private-room inputs and outcomes are not saved as profile replays or submitted to the solo campaign leaderboard.
Private-room codes now identify a volatile party rather than one disposable
match. The signed-in host automatically occupies Player 1, and a guest choosing
Join game atomically receives Player 2 while it is available between matches;
Watch joins without taking a seat, and a play join falls back to watching when
both seats are occupied or a match is active. In the lobby or after a finished
match, the host can choose any supported multiplayer game without creating or
sharing another link. That atomic replacement keeps the party code and members,
maps each occupied slot to the same ordinal seat in the next game, increments the
match generation, and clears the previous game runtime. Seats and games cannot
be changed during a running or paused match. When an adapter reaches its natural
terminal state, the room becomes finished automatically so the host can choose
the next game. The Party panel remains visible beside active and finished games,
separates the two player slots from Watching, and shows watcher capacity. A
watcher can join an open slot between matches or enter the FIFO Next match
queue while play is active; queued players are promoted only at a restart or
game-replacement boundary, never into a running game. Watch instead releases a
seat only between matches and cannot exceed the watcher limit. A full or active
play attempt falls back to watching only while watcher capacity remains.
Successful host lifecycle actions return focus to the persistent party heading,
while choosing another game focuses its new heading. Every connected member,
including observers, receives a polite game/lifecycle announcement without a
remote action stealing focus. If a remote update removes the focused
control, focus returns to the persistent party heading instead of the document.
Leaving the party is distinct from navigating back to the library. A seated player who leaves has held input cleared and frees the slot without changing the completed match's participant attribution. If the host leaves, ownership moves to the earliest connected signed-in member; when no such member exists, the party closes for everyone. The closing and departing-member messages are terminal client events, clear room-scoped credentials, and do not reconnect the old membership.
Volatile rooms also have bounded idle retention. An unconnected lobby expires after 60 minutes without a successful participant or host command; an unconnected running or paused room expires after two hours; and a completed game or explicitly finished room expires after 30 minutes. Passive invite reads, connection handshakes, diagnostics pings, snapshot delivery, and server-owned game ticks do not refresh those clocks. Public participant ids in room snapshots are not credentials. Room creation and guest admission return a separate opaque participant capability only to that participant, and the browser keeps it in account-scoped session storage for same-tab navigation and reloads. A WebSocket authenticated with a capability that still belongs to the room protects it from expiry, and the final recognized disconnect grants the room its full state-specific grace period again.
The sidecar sweeps expired rooms about once a minute and also checks retention
during room operations. At capacity it removes expired rooms first, then the
oldest unconnected terminal room, then the oldest unconnected lobby. It never
capacity-evicts nonterminal running or paused games or rooms with recognized
participant connections; if no safe candidate exists, creation returns a
retryable 503. Expired room codes return 410 room-expired for up to five
minutes so clients can explain what happened, then fall back to the ordinary
404 response. Both retained expired-code markers and live rooms remain
bounded by the configured capacity.
Append multiplayerDiagnostics=1 to a room URL to show a small client-side
diagnostics overlay with snapshot rate, jitter, ping, stream sequence gaps, and
recent tick catch-up and projection-reconciliation rates. Use
multiplayerDiagnostics=log to also emit the same summary to the browser
console while testing real network conditions.
Use the latency proxy to put repeatable internet-like conditions between browser clients and the sidecar WebSocket stream. The proxy models target round-trip time by delaying each WebSocket direction by roughly half of the configured RTT, with optional jitter and message loss.
Run the sidecar, proxy, and Next dev server in separate terminals:
npm run start:sidecar
MULTIPLAYER_LATENCY_PROXY_PROFILE=normal npm run start:latency-proxy
MULTIPLAYER_ROOM_SERVICE_URL=http://127.0.0.1:3001/_internal/multiplayer/rooms \
NEXT_PUBLIC_MULTIPLAYER_WEBSOCKET_URL=ws://127.0.0.1:3002/multiplayer/rooms \
npm run dev -- --hostname 0.0.0.0 --port 3000Open a private room with multiplayerDiagnostics=1, for example
http://127.0.0.1:3000/?room=<code>&multiplayerDiagnostics=1.
Latency proxy profiles:
| Profile | Target ping | Jitter | Drop rate | Use |
|---|---|---|---|---|
lan |
0ms | 0ms | 0% | Baseline comparison |
good |
40ms | 5ms | 0% | Good nearby internet |
normal |
80ms | 15ms | 0% | Typical friend play |
rough |
120ms | 30ms | 0% | Noticeably delayed play |
bad |
180ms | 50ms | 2% | Stress testing only |
Override any profile with:
MULTIPLAYER_LATENCY_PROXY_RTT_MSMULTIPLAYER_LATENCY_PROXY_JITTER_MSMULTIPLAYER_LATENCY_PROXY_DROP_RATEMULTIPLAYER_LATENCY_PROXY_SEED
Healthy manual test signals:
Snapshotsstays close to the configured sidecar rate, about30/sat the 33ms default.Pingroughly matches the selected profile.Stream gapsstays at0; any growth means the live room stream is missing authoritative room sequence numbers.Jittershould stay in the healthy or warning band for playable sessions.Tick catch-upandReconciledare recent per-second rates, not lifetime totals. They can be non-zero in healthy games, but high rates should not produce visible snapping. Asteroids is the most sensitive game because thrust, rotation, saucers, shots, and rocks are all continuous motion.
The sidecar latency smoke can be run through the proxy with:
npm run test:e2e:sidecar:latency -- --grep "Multiplayer diagnostics"Browser room streams are enabled with
NEXT_PUBLIC_MULTIPLAYER_WEBSOCKET_URL. Use an absolute endpoint such as
ws://127.0.0.1:3001/multiplayer/rooms for local sidecar testing, or a
same-origin path such as /multiplayer/rooms when a proxy routes WebSocket
upgrades beside the Next app. Existing rooms require this WebSocket stream for
live lobby snapshots, guest join/seat commands, and game input; when it is unset
or unavailable, the room UI surfaces a stream connection error instead of using
browser HTTP polling. Host lifecycle and settings commands continue to use the
Next HTTP API so signed-in-host authorization stays on the authenticated route
until the WebSocket sidecar has its own authenticated host session model.
The gateway resolves participant capabilities during resume and derives seat and
game-input actors from the bound socket. Participant ids submitted without a
valid capability cannot resume or authorize commands, and public observer joins
cannot attach an account identity. Anonymous invite previews receive one
bootstrap snapshot but are not retained as room subscribers; only recognized
members receive ongoing fanout. Per-participant connection ceilings prevent one
capability from creating unbounded subscriber tabs.
The sidecar room service endpoints are internal service endpoints; public HTTP
room creation and host authorization should still flow through the Next API
routes. Set the same bearer secret in
MULTIPLAYER_ROOM_SERVICE_CLIENT_BEARER_TOKEN for the Next process and
MULTIPLAYER_SIDECAR_ROOM_SERVICE_BEARER_TOKEN for the sidecar process.
The account-authority endpoint and every room-creation mutation always require
that configured secret. A tokenless sidecar advertises those capabilities as
unavailable and refuses creation rather than trusting a caller-supplied account
id. npm run dev:multiplayer generates and shares a secret automatically.
Protocol v6 adds expiring browser presence leases, one-party-per-account
membership, authenticated friend admission, bounded multi-tab capability
reacquisition, and exact admission compensation. The Next process
preflights the authenticated internal collection endpoint, then sends create and
command POSTs through its advertised /v6 mutation path with
x-multiplayer-room-protocol-version: 6; the sidecar rejects legacy paths or a
missing/mismatched header before reading a mutation body. Browser create and
host-command POSTs likewise use /api/multiplayer/rooms/v6 and
/api/multiplayer/rooms/<code>/v6; the v5, v4, v3, v2, and unversioned POST
routes return 426. Browser
connection.hello and connection.resume messages and the corresponding
bootstrap carry the same version, and a mismatched bootstrap closes without
reconnecting. During a rolling mixed-version deployment, room mutations and
stream activation therefore fail closed until the selected processes and the
browser bundle are compatible. Room snapshots expose a positive matchId;
game snapshots, reconnect cursors, acknowledgements, input, seat, settings, and
lifecycle commands carry the same generation. Restart creates a fresh runtime
and advances the match id, while stale-generation commands are rejected before
the current runtime advances.
Room creation, settings updates, and match replacement admit at most 32 nested JSON containers and 16 KiB of serialized UTF-8 settings. The normalized settings object counts as container 1 and its parameters object as container 2; the byte budget includes the game id, parameter keys, JSON punctuation, and string escaping. Current game presets fit comfortably within these limits. Violations of these structural, depth, or size rules return HTTP 400 before room state or gameplay advances. Public room creation and command bodies are limited to 64 KiB before JSON parsing, matching the internal sidecar budget; oversized bodies return HTTP 413.
The collection preflight also requires
membershipOnlyReacquisition: true. This capability bit distinguishes a
sidecar that implements party.reacquireAuthenticated atomically from an older
v6 sidecar, so accepted-invitation retries fail before mutation instead of
falling back to broader authenticated admission.
- Node.js 22
- TypeScript
- Next.js App Router
- React with the React Compiler enabled
- SQLite through
better-sqlite3 - Tailwind CSS v4
- shadcn/ui
- Vitest
- Playwright
- ESLint
- Docker
Leaderboards, player accounts, signed-in sessions, profile stats, saved
replays, and the durable social graph use the same SQLite database. Schema
version 7 includes one pending friend request per canonical account pair,
accepted canonical friendships, directed blocks, short-lived party invitations,
recipient-wide acceptance claims, and fixed-window API counters for exact
discovery, friend-request creation, and party-invitation creation. Pending
incoming and outgoing friend requests are capped per account. Party invitations
carry play/watch intent and accepted, declined, canceled, revoked, or expired
terminal states. Blocking another account removes requests and friendships and
revokes pending invitations in both directions in one transaction; removing a
friend
also revokes pending invitations for that pair, while unblocking restores
nothing automatically. Party invitations use a server-owned five-minute expiry;
all client-facing invitation summaries omit the reusable party code, including
after trusted admission succeeds.
Leaderboard records are stored under game-and-parameter keys such as
snake|mode=levels or tetris|board=10x20|level=3. Signed-in play sessions also
store the selected leaderboard key so profile stats can report both per-game
totals and parameter-aware history.
Player names are unique after trimming, whitespace collapsing, and case-insensitive comparison. Passwords are salted and hashed in SQLite; existing passwordless demo names remain reserved, cannot be claimed through sign-up, and are not eligible for social discovery or relationships. Social discovery is defined as an exact lookup through the same normalized display-name key, not a fuzzy search or public directory, and exposes only the matching account's minimal public identity when that relationship is allowed.
The default database path is .data/snake-leaderboard.sqlite, kept for
compatibility with existing Snake deployments. On a VPS, set
GAME_LEADERBOARD_SQLITE_PATH to durable storage.
Authenticated social APIs now expose the durable overview and exact discovery,
presence renewal/release, friend-request decisions, friend removal, blocks, and
party-invitation creation/resolution under /api/social. Mutations derive the
actor from the HTTP-only session cookie, require same-origin JSON when they have
a body, reject bodies over 16 KiB before JSON parsing, and return no-store
responses. Same-origin checks accept the framework URL or the transport Host
with the same protocol, because local and proxied Next.js runtimes can
canonicalize the URL hostname; a different host or protocol is still rejected.
Presence resolution enriches friends with only available, busy,
in-party, offline, or unknown; a room-service failure falls back to
unknown and keeps invitation actions disabled. Exact
discovery allows 30 requests per account per minute, friend-request creation
allows 10, and party-invitation creation allows 20; all return 429 with
Retry-After when exhausted. Each account may hold at most 100 pending incoming
and 100 pending outgoing friend requests, plus 20 pending incoming and 20
pending outgoing party invitations.
The browser social foundation validates every successful payload before
exposing it to UI state. Signed-in library and leaderboard screens expose a
Friends button while room screens keep the same entry point available for
relationship management. Solo games and replays omit the button to avoid an
interrupting social surface during play. The dialog supports privacy-preserving
exact-name discovery, incoming and outgoing friend requests, removal and
blocking confirmations, invitation decline/cancel actions, and explicit
availability labels. It never renders a private room code or participant
capability. Incoming party invitations remain visible on busy/in-party surfaces,
but acceptance is disabled unless presence is available and the current
launcher surface can adopt the returned private credentials. Library and
leaderboard acceptance opens the party directly without a link: the launcher
validates the accepting account, invalidates any older room-creation request,
pushes only the canonical room URL, stores the participant capability in
account-scoped session storage, and boots the room from the authoritative room,
game, and sequence snapshot. The adopted credentials and snapshot are also
bound to the current sign-in epoch, so switching away and back cannot revive an
older account's handoff. Busy and in-party rows explain what must finish or be
left before acceptance, while Decline remains available. Watch explicitly joins
Watching; Play may join a player spot or fall back to Watching, and the
destination records the role the server initially admitted so the message stays
accurate if an observer later claims a player spot. An interrupted
acceptance retries the idempotent server endpoint, while a completed acceptance
whose local launcher handoff fails retries only that in-memory handoff.
The authenticated current party host also gets an Invite friends panel beside the Party roster in both lobby and active-game layouts. Available friends have explicit Play and Watch actions; current members, busy players, players in another party, offline players, and unknown availability stay visible with disabled actions and a reason. Play currently offers an open player spot or, when watcher capacity allows, Watching; Watch requests Watching, and watchers can use the existing next-match queue themselves. Observer capacity can leave Play enabled for an open player spot while disabling Watch. The server revalidates host ownership, friendship, presence, and capacity at creation and acceptance time, so the UI never promises admission before acceptance. The panel never displays a party code or participant capability, and Copy invite link remains the guest fallback.
A signed-in document uses one cryptographic per-document presence id, renews its 45-second lease every 15 seconds only while visible, renews again on focus, and sends a keepalive release when hidden or unloaded. Logout gives that advisory release a short opportunity to finish before clearing the authenticating session cookie; the TTL remains the fallback. Presence operations carry an increasing per-document generation, and the room authority suppresses delayed replays for up to five minutes within a bounded 64-client tombstone set per account. Generationless legacy operations remain valid only until that client identity first uses the sequenced contract. Social overview loading keeps the last valid same-account graph during refresh, suppresses stale account and request results, refreshes on focus, and otherwise polls every 30 seconds only while visible. Successful overviews and explicit refresh attempts carry monotonic browser request generations so the host invitation panel can ignore pre-creation poll results while still retiring an invitation that resolves before its first post-creation overview.
Presence leases, effective busy/in-party status, parties, matches, observer queues, and participant capabilities remain volatile and are never reconstructed from social rows. Invitation creation validates the relationship, asks the sidecar to confirm host ownership and current eligibility, revalidates the relationship, and only then inserts or returns the durable invitation. Busy, in-party, offline, and unknown recipients cannot receive or accept an invitation; an existing pending invitation is suspended until the recipient becomes available again or its TTL expires.
Acceptance reads the private party code only inside the trusted server boundary and first obtains a 30-second, recipient-wide SQLite claim. The claim serializes concurrent accepts, extends the invitation through a two-minute recovery grace, and allows only its matching token to finalize. Releasing a failed attempt restores the original invitation expiry, or expires it immediately when that deadline has passed. The server then admits the signed-in account, atomically accepts the chosen invitation while revoking the recipient's other pending invitations, and returns the participant capability only after that durable step succeeds. A new admission is compensated if persistence fails; the claim is released only after compensation succeeds, and a pre-existing membership reacquisition is never removed. A retry after a lost acceptance response uses a membership-only authority command that cannot admit a new participant; an old accepted invitation therefore cannot rejoin a party after the account leaves.
Before an overview returns invitations, private invitation tuples are reconciled with the sidecar so terminal parties are revoked even after WebSocket closure, expiry, eviction, or sidecar restart. Host transfer does not invalidate an already-issued party invitation. Reconciliation and friend-availability resolution use at most four concurrent authority calls. Resolved nonaccepted invitation history is bounded to 1,000 rows, and only the newest accepted row per recipient is retained as the lost-response reconnect index. Client-facing invitation JSON never contains a party code or stores a participant capability.
Snake, Tetris, Breakout, Minesweeper, Space Invaders, Pong, Simon, 2048,
Asteroids, and the Tank Patrol single-player campaign record replay events
during play after the server issues a run id and seed. Replay events include
active-play elapsed time so playback mirrors player hesitation while excluding
Pause and Help time.
Minesweeper and Simon replays also include board-local schematic cursor streams
sampled from mouse movement every 50ms.
Signed-in players can save the completed run from the final screen; the profile
page stores one latest replay per user and game and links back into the
launcher at /?replay=snake, /?replay=tetris, /?replay=breakout,
/?replay=minesweeper, /?replay=space-invaders, /?replay=pong,
/?replay=simon, /?replay=twenty-forty-eight, /?replay=asteroids, or
/?replay=battle-city for client playback.
Build the production image locally with:
docker build -t lore-coding-demo .Run it with a mounted data directory so SQLite state survives container replacement:
docker run --rm -p 3000:3000 \
-v "$PWD/.data:/data" \
lore-coding-demoThe image uses Next.js standalone output and listens on port 3000.
| Command | Purpose |
|---|---|
npm test |
Run the deterministic Vitest suite. |
npm run test:agent |
Write JSON/JUnit results and coverage under reports/. |
npm run test:coverage |
Generate broad all-source coverage under reports/coverage/. |
npm run test:coverage:core |
Run the thresholded core coverage gate under reports/coverage-core/. |
npm run test:e2e:install |
Install the Chromium browser used by Playwright. |
npm run test:e2e |
Run the focused Chromium Playwright smoke suite. |
npm run test:e2e:sidecar |
Build the multiplayer sidecar, then run the isolated Chromium Playwright smoke with Next wired to the sidecar WebSocket and internal room service. |
npm run test:e2e:headed |
Run the Playwright suite in a visible browser. |
npm run test:e2e:ui |
Open Playwright's interactive test runner. |
npm run typecheck |
Run TypeScript without emitting. |
npm run lint |
Run ESLint. |
npm run check:deps |
Run dependency-cruiser to enforce source dependency boundaries. |
npm run check:unused |
Run Knip to catch unused files, exports, and dependencies. |
npm run lore-coding -- --file <path> |
Validate a Lore Coding commit message file. |
npm run build |
Build the Next.js app. |
GitHub Actions runs these checks on pushes to main and pull requests that
change code or build-affecting files: npm ci, npm run build,
npm run lint, npm run typecheck, npm run check:deps,
npm run check:unused,
npm run test:coverage:core, npm run test:e2e, and
npm run test:e2e:sidecar. The sidecar suite rebuilds and starts the emitted
multiplayer sidecar before exercising its isolated browser flows.
Documentation-only changes such as Markdown, docs/**, and LICENSE are
ignored by CI.
After those checks pass on a push to main, GitHub Actions builds the
linux/amd64 and linux/arm64 Docker images and pushes a multi-platform
manifest to Docker Hub using the DOCKERHUB_USERNAME and DOCKERHUB_IMAGE
repository variables plus the DOCKERHUB_TOKEN repository secret. The
published tags are latest, main, and the short commit SHA.
The repository includes a dependency-free Agentic Lore Coding validator at
.githooks/lore-coding.mjs, a local Git commit-msg hook wrapper at
.githooks/commit-msg, and an install-time hook setup script at
.githooks/install-lore-coding-hooks.mjs.
npm install runs the package prepare script, which configures this clone
with:
git config core.hooksPath .githooksThe installer only does this inside a Git worktree, skips in CI, and does not
overwrite an existing custom core.hooksPath. Set
LORE_CODING_INSTALL_HOOKS=0 before installing dependencies to skip automatic
hook setup. If your package manager skips lifecycle scripts, run the Git config
command manually.
After the hook path is configured, git commit passes the proposed
commit-message file to the hook. The hook runs:
node .githooks/lore-coding.mjs --edit "$1"If validation fails, Git aborts the commit and prints stable LORE### error
codes with the offending line, expected format, fix guidance, and an example.
The validator rejects assistant wrapper prose, Markdown code fences, malformed
subjects, unsupported task types, malformed scopes, missing or empty required
sections, out-of-order sections, missing or malformed Lore-ID: trailers,
legacy Links: sections, malformed Lore-Link: trailers, and linked Lore IDs
that do not exist in reachable history.
Run it manually against a message file with:
npm run lore-coding -- --file .git/COMMIT_EDITMSGExplain an error code with:
npm run lore-coding -- explain LORE047Every task commit must end with a Lore-ID: trailer. Related tasks use
repeatable Lore-Link: trailers with a stable Lore ID, an em dash, and a reason:
Lore-ID: LC-20260530-4D61
Lore-Link: LC-20260529-18A1 — established validator behavior extended here
This first version is local-only; CI does not run Lore Coding validation yet.
shadcn/ui is initialized with Tailwind CSS v4, the base-nova preset, and the
@/* import alias. Add components with:
npx shadcn@latest add <component>