Initial Commit
This commit is contained in:
106
openspec/changes/enterprise-game-server-core/proposal.md
Normal file
106
openspec/changes/enterprise-game-server-core/proposal.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Enterprise migration: `@suaveplan/game-server` (core slice)
|
||||
|
||||
**Package**: `@suaveplan/game-server`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/game-server` on the reference branch
|
||||
`claude/review-game-engine-arch-Kaykr` (main repo
|
||||
`packages/universal/game-server/`) is a 192-file framework with session
|
||||
lifecycle, plugin chain, networking, state-sync, matchmaking, and an
|
||||
"enhanced" variant (anti-cheat / lag-compensation / interest management /
|
||||
replay / tournament / persistence / leaderboard / state-sync / timing).
|
||||
The branch's last commit (`2fb25cdb35`) made the engine generic by
|
||||
removing all game-specific adapters (bingo / tic-tac-toe / gem-crush).
|
||||
|
||||
Genesis needs that engine, but porting all 192 files in one slice would
|
||||
either skip the genesis quality gates (100% test coverage, sibling `.md`
|
||||
per `.ts`, kebab-case + 5-file layout, typed errors, Zod boundaries,
|
||||
OTel) or land partially-built code. Six required workspace dependencies
|
||||
(`controller`, `keys`, `network-webrtc`, `network-websockets`,
|
||||
`storage-browser`, `workflow`) do not yet exist on genesis, blocking the
|
||||
`network/`, `plugins/`, and parts of `state/`.
|
||||
|
||||
This change ships the **dependency-light foundation** that every later
|
||||
slice depends on, behind the `@suaveplan/game-server` name at `0.1.0`,
|
||||
and documents the remaining scope as explicitly deferred follow-on
|
||||
changes. The package is fully usable today by downstream code that wants
|
||||
typed errors, Zod-validated boundary types, or the DI container.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `@suaveplan/game-server` package at
|
||||
`packages/gaming/games/game-server/` (universal tier, `sideEffects:
|
||||
false`, version `0.1.0`).
|
||||
- ADDED: `GameServerError` + `GAME_SERVER_CODES` registry via
|
||||
`defineCodes` — four initial codes:
|
||||
`SERVICE_NOT_REGISTERED`, `INVALID_GAME_CONFIG`,
|
||||
`INVALID_SESSION_METADATA`, `INVALID_PLAYER_INFO`.
|
||||
- ADDED: `GameConfig`, `SessionMetadata`, `PlayerInfo`,
|
||||
`MoveValidationResult`, `GameResult`, `SessionType`, `DeepReadonly`
|
||||
with Zod schemas (`GameConfigSchema`, `SessionMetadataSchema`,
|
||||
`PlayerInfoSchema`, `MoveValidationResultSchema`, `GameResultSchema`,
|
||||
`SessionTypeSchema`) and validators (`parseGameConfig`,
|
||||
`parseSessionMetadata`, `parsePlayerInfo`,
|
||||
`tryParseMoveValidationResult`, `tryParseGameResult`).
|
||||
- ADDED: `DIContainer` with singleton/transient lifecycles, typed error
|
||||
on miss, and **re-register-invalidates-cache** semantics (genesis
|
||||
improvement over upstream).
|
||||
- ADDED: Subpath exports `./errors`, `./types`, `./engine/di-container`
|
||||
for tree-shaking.
|
||||
|
||||
## Out of scope (deferred)
|
||||
|
||||
| Surface area | Why deferred | Follow-up change |
|
||||
|---|---|---|
|
||||
| `core/{AbstractGameLogic, interfaces/*, events/*}` | Each contract file needs its own 5-file layout + tests + docs; bundling all of them in one slice would force this PR over 30 files. | `enterprise-game-server-core-contracts` |
|
||||
| `engine/{GameEngine 487, GameLoop 355, PlayerManager 345, PluginManager 340, SessionLifecycle 295, SessionNode 173, SessionStore 128, SessionTree 429}` | Six of eight files exceed the 300-line genesis hard limit and require splitting. Each needs Zod boundary schemas, OTel instruments, typed `GAME_SERVER.*` codes, 100% coverage, ≥200-word sibling `.md`. | `enterprise-game-server-engine` |
|
||||
| `state/{DeltaCompressor 221, StateDiffer 280, StateManager 316, StateSync 375}` | StateManager and StateSync exceed the 300-line limit; StateSync also reaches into networking which is blocked. | `enterprise-game-server-state` |
|
||||
| `matchmaking/{MatchmakingEngine 152, QueueManager 169, SpectatorManager 122, skill-matcher 40, winner-stays-on 56}` | Dependency-clean but still requires the engine/state slices to be useful end-to-end. | `enterprise-game-server-matchmaking` |
|
||||
| `performance/{NetworkTierAdapter, ObjectPool, SessionPool}` → `telemetry/` | Folds into engine slice (`SessionPool` consumes `Session` from engine). | `enterprise-game-server-engine` |
|
||||
| `network/{NetworkAdapter, ReconnectionManager, WebRTCTransport, WebSocketTransport}` | BLOCKED on missing genesis deps `@suaveplan/network-webrtc` and `@suaveplan/network-websockets`. | After transport packages land. |
|
||||
| `plugins/{ABTest, Analytics, BasePlugin, FeatureFlag, Logging, Persistence, Rules, Workflow}` | BLOCKED on missing genesis deps `@suaveplan/workflow` and `@suaveplan/keys`. | After workflow/keys packages land. |
|
||||
| `enhanced/` (73 files: anti-cheat, components, interest-management, lag-compensation, leaderboard, persistence, replay, service-discovery, state-sync, timing, tournament) | Priority 2 per migration brief; the enhanced variant sits above a stable engine. Several enhanced features have transitive deps on the blocked packages. | `enterprise-game-server-enhanced` |
|
||||
| `integrations/components/game-lobby/*` | Priority 3 — React-coupled UI that belongs in a `react-game-lobby` package, not a universal-tier engine. | `enterprise-react-game-lobby` |
|
||||
| `integrations/enhanced-server/*` | Bundles every enhanced feature; depends on `enterprise-game-server-enhanced`. | `enterprise-game-server-enhanced` |
|
||||
| `stories/*`, `e2e/*`, `benchmarks/*` | Storybook/Playwright/bench tooling lands with the surfaces they exercise. | per-slice. |
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change derives from `../suaveplan/enterprise/game-server.md` (not
|
||||
present in the worktree's parent repo — the reference is the branch
|
||||
`claude/review-game-engine-arch-Kaykr` itself, since the package was
|
||||
under active redesign).
|
||||
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Typed error registry `GAME_SERVER.*` | `src/engine/DIContainer.ts:30` (only throw site in the migrated slice; upstream used plain `Error`) | reworded — typed error replaces plain throw. |
|
||||
| `GameConfig`/`SessionMetadata`/`PlayerInfo`/`MoveValidationResult`/`GameResult` types | `src/core/types/index.ts:1-47` | extended — added Zod schemas + validators (no source line). |
|
||||
| `DIContainer` singleton/transient resolver | `src/engine/DIContainer.ts:1-61` | extended — re-register-invalidates-cache is new in genesis. |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| Zod schemas for every boundary type | Genesis rule: every public boundary is Zod-validated. |
|
||||
| Typed `GameServerError` instead of plain `Error` | Genesis rule: every package throws via its own `defineCodes` registry. |
|
||||
| `readonly` modifiers on every interface field | Genesis lint profile rejects `Record<string, any>` and prefers immutable shapes. |
|
||||
| Re-register-invalidates-cache on `DIContainer` | Bug-class fix: upstream silently kept stale singletons after re-register. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/errors/game-server-error.test.ts` — registry shape, error
|
||||
construction, code discrimination.
|
||||
- `src/types/common.test.ts` — every schema accept-path, reject-path,
|
||||
and validator throw-path with the matching code.
|
||||
- `src/engine/di-container/di-container.test.ts` — singleton, transient,
|
||||
symbol identifiers, fluent chaining, miss-throws-typed-error,
|
||||
has/unregister/clear/getRegisteredServices, re-register cache
|
||||
invalidation.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. No consumer on genesis currently imports
|
||||
`@suaveplan/game-server`.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Capability Spec Delta: game-server (enterprise-game-server-core)
|
||||
|
||||
**Package**: `@suaveplan/game-server`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Typed error namespace
|
||||
|
||||
The package SHALL expose a single `GameServerError` class extending
|
||||
`SuaveplanError<GameServerCode>` and a `GAME_SERVER_CODES` registry
|
||||
produced by `defineCodes("GAME_SERVER", [...])`. The registry SHALL
|
||||
enumerate only codes thrown by an actual production code path.
|
||||
|
||||
#### Scenario: registry only contains codes thrown by production paths
|
||||
|
||||
- **WHEN** the test suite inspects the registered codes
|
||||
- **THEN** the registry contains exactly `SERVICE_NOT_REGISTERED`,
|
||||
`INVALID_GAME_CONFIG`, `INVALID_SESSION_METADATA`, and
|
||||
`INVALID_PLAYER_INFO`, each fully-qualified as `GAME_SERVER.<NAME>`.
|
||||
|
||||
#### Scenario: errors carry code, message, cause, and context
|
||||
|
||||
- **WHEN** a caller constructs `new GameServerError(code, message, { cause, context })`
|
||||
- **THEN** the resulting instance is an `instanceof SuaveplanError`, has
|
||||
`.name === "GameServerError"`, exposes the typed `.code`, and forwards
|
||||
`.cause` and `.context` to the standard logging/OTel adapters.
|
||||
|
||||
**Verified by:**
|
||||
`src/errors/game-server-error.test.ts::"registers all expected codes under GAME_SERVER namespace"`
|
||||
AND
|
||||
`src/errors/game-server-error.test.ts::"constructs an error with code, message, cause, and context"`
|
||||
AND
|
||||
`src/errors/game-server-error.test.ts::"constructs without optional cause/context"`
|
||||
AND
|
||||
`src/errors/game-server-error.test.ts::"supports discriminated access via the code union"`.
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Zod-validated boundary types
|
||||
|
||||
The package SHALL expose Zod schemas
|
||||
(`GameConfigSchema`, `SessionMetadataSchema`, `PlayerInfoSchema`,
|
||||
`MoveValidationResultSchema`, `GameResultSchema`, `SessionTypeSchema`)
|
||||
and throwing validators (`parseGameConfig`, `parseSessionMetadata`,
|
||||
`parsePlayerInfo`) plus non-throwing probes
|
||||
(`tryParseMoveValidationResult`, `tryParseGameResult`) that turn
|
||||
untrusted input into the corresponding typed value or raise a typed
|
||||
`GameServerError` with the matching `GAME_SERVER.INVALID_*` code.
|
||||
|
||||
#### Scenario: GameConfig enforces maxPlayers >= minPlayers
|
||||
|
||||
- **WHEN** `parseGameConfig` is given `{ gameType: "x", maxPlayers: 1, minPlayers: 4, rules: {} }`
|
||||
- **THEN** it throws `GameServerError` with code
|
||||
`GAME_SERVER.INVALID_GAME_CONFIG` and a `cause` set to the underlying
|
||||
ZodError.
|
||||
|
||||
#### Scenario: SessionMetadata accepts passthrough extras with non-negative timestamps
|
||||
|
||||
- **WHEN** `parseSessionMetadata` is given a record with `createdAt`/`updatedAt`
|
||||
non-negative integers plus arbitrary extra keys
|
||||
- **THEN** the result preserves the extras and validates the timestamps.
|
||||
|
||||
#### Scenario: MoveValidationResult and GameResult reject extra keys
|
||||
|
||||
- **WHEN** an off-spec object includes additional keys not declared in
|
||||
the strict schema
|
||||
- **THEN** the non-throwing probe returns `null`.
|
||||
|
||||
**Verified by:**
|
||||
`src/types/common.test.ts::"rejects maxPlayers < minPlayers"`
|
||||
AND
|
||||
`src/types/common.test.ts::"throws GameServerError with INVALID_GAME_CONFIG"`
|
||||
AND
|
||||
`src/types/common.test.ts::"accepts a valid timestamp pair with passthrough extras"`
|
||||
AND
|
||||
`src/types/common.test.ts::"throws GameServerError with INVALID_SESSION_METADATA"`
|
||||
AND
|
||||
`src/types/common.test.ts::"throws GameServerError with INVALID_PLAYER_INFO on bad input"`
|
||||
AND
|
||||
`src/types/common.test.ts::"returns null on off-spec input"` (twice).
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Dependency-injection container
|
||||
|
||||
The package SHALL expose a `DIContainer` class with `register`,
|
||||
`resolve`, `has`, `unregister`, `clear`, and `getRegisteredServices`
|
||||
methods. `register` SHALL support `"singleton"` (default) and
|
||||
`"transient"` lifecycles, return `this` for chaining, and discard any
|
||||
cached singleton instance when an identifier is re-registered. `resolve`
|
||||
SHALL throw `GameServerError` with code
|
||||
`GAME_SERVER.SERVICE_NOT_REGISTERED` for unknown identifiers.
|
||||
|
||||
#### Scenario: singleton factory invoked exactly once
|
||||
|
||||
- **WHEN** an identifier registered as `"singleton"` is resolved repeatedly
|
||||
- **THEN** the factory runs once and every resolution returns the same instance.
|
||||
|
||||
#### Scenario: transient factory invoked on every resolve
|
||||
|
||||
- **WHEN** an identifier registered as `"transient"` is resolved repeatedly
|
||||
- **THEN** the factory runs once per resolution and instances are not cached.
|
||||
|
||||
#### Scenario: unknown identifier throws typed error
|
||||
|
||||
- **WHEN** `resolve` is called with an identifier that was never registered
|
||||
- **THEN** a `GameServerError` is thrown with code
|
||||
`GAME_SERVER.SERVICE_NOT_REGISTERED`, the message contains the
|
||||
stringified identifier, and `.context.identifier` matches.
|
||||
|
||||
#### Scenario: re-register invalidates cached singleton
|
||||
|
||||
- **WHEN** an identifier is registered twice with different factories
|
||||
- **THEN** the next resolve returns an instance produced by the second
|
||||
factory, not the cached first instance.
|
||||
|
||||
**Verified by:**
|
||||
`src/engine/di-container/di-container.test.ts::"registers and resolves a singleton (default lifecycle)"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"registers and resolves a transient service"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"throws GameServerError(SERVICE_NOT_REGISTERED) for unknown identifier (string)"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"throws with the symbol stringified for unknown symbol identifier"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"re-register invalidates the cached singleton"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"unregister returns true for known, false for unknown, and clears cache"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"clear() removes registrations and singleton cache"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"getRegisteredServices() returns the registered identifiers"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"supports symbol identifiers with typed generics"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"supports fluent chained registration"`
|
||||
AND
|
||||
`src/engine/di-container/di-container.test.ts::"has() reflects registration state"`.
|
||||
|
||||
> Deferred (2026-05-23): the broader `core/{AbstractGameLogic,
|
||||
> interfaces/*, events/*}`, `engine/{GameEngine, GameLoop,
|
||||
> PlayerManager, PluginManager, SessionLifecycle, SessionNode,
|
||||
> SessionStore, SessionTree}`, `state/*`, `matchmaking/*`,
|
||||
> `performance/*` → `telemetry/*`, `network/*`, `plugins/*`,
|
||||
> `enhanced/*`, and `integrations/*` surfaces from the reference branch
|
||||
> `claude/review-game-engine-arch-Kaykr` are tracked as follow-on
|
||||
> changes per the deferral table in `proposal.md`. Six of those
|
||||
> dependencies (`controller`, `keys`, `network-webrtc`,
|
||||
> `network-websockets`, `storage-browser`, `workflow`) are not yet
|
||||
> present on genesis and block the corresponding migration slices.
|
||||
113
openspec/changes/enterprise-game-server-core/tasks.md
Normal file
113
openspec/changes/enterprise-game-server-core/tasks.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Tasks — enterprise-game-server-core
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm clean working tree at slice open (verified via `git status` and `git worktree list`: worktree
|
||||
branched from `25dbec9c8b`, no in-flight uncommitted work).
|
||||
- [x] 0.2 Account for `.claude/worktrees/`: this slice runs in
|
||||
`agent-a0756387a9185f112` and is the only active worktree.
|
||||
- [x] 0.3 `MANIFEST.md` will be regenerated as part of archive (task 7.3).
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read reference branch source for the migrated scope.
|
||||
- [x] 1.2 No existing `openspec/specs/game-server/spec.md` — fresh capability.
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` §13/§14 hard rules.
|
||||
- [x] 1.4 Draft `proposal.md`.
|
||||
- [x] 1.5 `## Source mapping` table populated with line ranges /
|
||||
no-source justifications.
|
||||
- [x] 1.6 Draft `specs/game-server/spec.md` with ADDED requirements.
|
||||
- [x] 1.7 Every requirement carries a `**Verified by:**` line naming
|
||||
concrete `it(...)` titles.
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Package scaffold: `package.json`, `tsconfig.lib.json`,
|
||||
`vite.config.ts`, `vitest.config.ts`, `tests/setup/polyfills.ts`,
|
||||
`README.md`.
|
||||
- [x] 2.2 `src/errors/game-server-error.ts` + barrel — `GameServerError`,
|
||||
`GAME_SERVER_CODES`, `GameServerCode`.
|
||||
- [x] 2.3 `src/types/common.types.ts` (interfaces) + `src/types/common.ts`
|
||||
(Zod schemas + validators) + barrel.
|
||||
- [x] 2.4 `src/engine/di-container/{di-container.ts, .types.ts, index.ts}`.
|
||||
- [x] 2.5 Root `src/index.ts` re-exports the full Priority-1 surface.
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 `src/errors/game-server-error.test.ts` — registry shape, error
|
||||
shape, code-union discrimination.
|
||||
- [x] 3.2 `src/types/common.test.ts` — every schema accept + reject + the
|
||||
thrower path with the matching `GAME_SERVER.*` code.
|
||||
- [x] 3.3 `src/engine/di-container/di-container.test.ts` — 11 `it()`
|
||||
cases covering singleton, transient, symbol identifier, fluent
|
||||
chaining, miss-throws-typed-error (string + symbol),
|
||||
has/unregister/clear/getRegisteredServices, re-register cache
|
||||
invalidation.
|
||||
- [x] 3.4 Every `it(...)` carries `{ timeout: 5000 }`.
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` ≥200 words for each non-exempt `.ts`:
|
||||
`game-server-error.md`, `common.md`, `di-container.md`.
|
||||
- [x] 4.2 `README.md` lists each `.md` under `## Modules` and tracks
|
||||
deferred surfaces.
|
||||
- [x] 4.3 No CLAUDE.md package-listing edit required — the rewrite of
|
||||
`enterprise-gaming-game-server` into this slice plus deferred
|
||||
follow-ups will be captured when the listing is next refreshed.
|
||||
|
||||
## 5. Pre-archive verification (the 5-gate triple)
|
||||
|
||||
- [ ] 5.1 `bun run check` — Biome 0/0/0 across the package. **NOT RUN**
|
||||
in this session: scope-management constraint per migration brief.
|
||||
- [ ] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/game-server`
|
||||
— green. **NOT RUN** in this session.
|
||||
- [ ] 5.3 `bunx turbo test:coverage --filter=@suaveplan/game-server` —
|
||||
100/100/100/100. **NOT RUN** in this session.
|
||||
- [ ] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/game-server` —
|
||||
exit 0. **NOT RUN** in this session.
|
||||
- [ ] 5.5 `bunx turbo build --filter=@suaveplan/game-server` — green.
|
||||
**NOT RUN** in this session.
|
||||
|
||||
> The 5 boxes above remain unchecked because this slice was authored
|
||||
> under a single-session migration brief and the pre-archive gates
|
||||
> require a live Bun/Turbo invocation against the working tree. They
|
||||
> MUST be run and made green before this change is archived (task 7).
|
||||
|
||||
## 6. Pre-archive 7-row checklist (AGENTS.md §13)
|
||||
|
||||
- [x] 6.1 Generics threaded — `ServiceIdentifier<T>` carries through
|
||||
`register`/`resolve`; schema validator return types preserve
|
||||
readonly modifiers via explicit cast (canonical Zod↔TS bridge).
|
||||
- [x] 6.2 Zod schemas wired at every declared boundary
|
||||
(`GameConfig`/`SessionMetadata`/`PlayerInfo`/`MoveValidationResult`/`GameResult`).
|
||||
- [x] 6.3 Every code in `error-codes.ts` thrown by at least one
|
||||
production path: `SERVICE_NOT_REGISTERED` (di-container.ts:39),
|
||||
`INVALID_GAME_CONFIG` (common.ts:88), `INVALID_SESSION_METADATA`
|
||||
(common.ts:103), `INVALID_PLAYER_INFO` (common.ts:118).
|
||||
- [x] 6.4 Strict-mode behaviors tested both ways — `MoveValidationResult`
|
||||
strict-extra rejection, `GameResult` strict-extra rejection,
|
||||
`SessionMetadata` passthrough acceptance, `GameConfig` passthrough
|
||||
acceptance.
|
||||
- [ ] 6.5 OTel instruments not introduced in this slice — Priority-1
|
||||
DI container has no metric-emitting paths. Deferred to the engine
|
||||
slice (`enterprise-game-server-engine`) which lands `Session`
|
||||
tick-duration histograms and `SessionManager` lifecycle counters.
|
||||
- [ ] 6.6 Lifecycle dedup — not applicable to the four artifacts in this
|
||||
slice. Deferred to the engine slice where `SessionStore` /
|
||||
`SessionLifecycle` introduce idempotent lifecycle ops.
|
||||
- [x] 6.7 No retroactive ticks — every box above reflects work done in
|
||||
this slice; un-runnable gates and not-applicable rows are
|
||||
unchecked with an explicit reason.
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [ ] 7.1 Fold `specs/game-server/spec.md` into
|
||||
`openspec/specs/game-server/spec.md` (currently absent — first
|
||||
authoring of the capability).
|
||||
- [ ] 7.2 Move `openspec/changes/enterprise-game-server-core/` →
|
||||
`openspec/archive/2026-05-23-enterprise-game-server-core/`.
|
||||
- [ ] 7.3 `bun scripts/folder-reorg/generate-manifest.ts` to refresh
|
||||
`MANIFEST.md`.
|
||||
|
||||
> Tasks 5.x and 7.x will run as the next session's first action once the
|
||||
> 5-gate triple goes green.
|
||||
Reference in New Issue
Block a user