9.9 KiB
9.9 KiB
ORCHESTRATOR.md — Operating Manual for Successor Agents
You are taking over the autonomous development loop on Flight-Ops (browser game: run an airplane transport company in Europe; server-authoritative sim, MapLibre GL JS map). This document captures operational knowledge that is NOT written down in AGENTS.md. Read AGENTS.md first — it defines the mandatory change process and this file complements it.
1. Project identity
- Repo: Gitea at https://git.kkhost.pl → org OpenCode, repo Flight-Ops (full name
OpenCode/Flight-Ops). ⚠️ NOT "krzysztof/flight" — that path 404s and caused a long dead-end in Sept 2026. Every Gitea API/MCP call uses owner=OpenCode, repo=Flight-Ops. - Local workdir + prod deploy dir:
/home/krzyw/projects/sandbox/flight(git checkout;docker compose up -d --builddeploys from here). - Prod URL: https://flight.dev.kkhost.pl (Traefik → docker-compose stack on this host).
GET /api/versionreports the running version. - Gitea API token for git.kkhost.pl (local dev instance; same one the gitea MCP server uses):
013451b655a1592c53d7a3394b4696a93855912b— use as headerAuthorization: token <t>. ⚠️ This is a live credential for the forge; if this repo ever becomes public, rotate it. - Current user: kamilkrzywda (id 1).
2. The loop (per issue)
- Small incremental change on a feature branch off
master; connected issues may share one branch/PR with a single version bump. - Bump
package.jsonversion + add CHANGELOG.md entry at top:## v<version> — YYYY-MM-DD HH:MMthen bullet points of what changed (docs-only changes get patch bumps too — precedent v0.19.1/v0.19.2). - Local verification ONLY (there are NO local e2e runs):
npx tsc --noEmit,npx vitest run(unit baseline: 288 tests in 31 files),npx playwright test --list(baseline: 41 tests in 12 files). - Push → CI (~7 min) → auto-merge on green (PRs have auto-merge configured; no manual merge needed — verify via
GET /pulls/{n}that state=closed and merged_at is set). - Tag the version on master:
git tag v<version> origin/master && git push origin v<version>(aftergit fetch origin master). - Deploy prod in a PTY (long task):
cd /home/krzyw/projects/sandbox/flight && git checkout master && git reset --hard origin/master && docker compose up -d --build; then verifycurl https://flight.dev.kkhost.pl/api/versionreturns the new version and the root page is HTTP 200. - Close each issue with a comment: what changed, version, PR number, merge commit SHA, deployed URL.
- Repeat for the next open issue:
GET /issues?state=open&type=issues— note Gitea lists pull requests in this endpoint too; filter out entries with apull_requestkey.
3. Gitea API + MCP quirks (learned the hard way)
- The gitea MCP tools work for issues/PRs/files/tags, and Actions endpoints work IF you pass owner=
OpenCode, repo=Flight-Ops. A wrong owner/repo makes EVERY call return{"message":"not found","url":"https://git.kkhost.pl/api/swagger"}— that swagger URL inside a 404 is the signature of "wrong path", not "Gitea is down". - MCP merge endpoint 405s → don't bother; auto-merge handles merging. Manual fallback if ever needed:
POST /repos/OpenCode/Flight-Ops/pulls/{n}/mergewith body{"do":"merge","merge_style":"merge"}. - Gitea REST shapes (NOT GitHub's): list runs →
{workflow_runs:[...]}; run jobs →/actions/runs/{id}/jobsreturns{"jobs":[...]}; single job log via MCPget_job_log_preview(param tail_lines) ordownload_job_logto a file, then grep. - Failure anatomy in CI logs: the Playwright summary at the end lists "N failed" + test titles; each failure block has a code frame with line numbers plus an "Error Context:" path. The e2e step is the only one that can fail flakily; unit failures are real regressions.
- CI watcher pattern that works:
run_background_process(notify=true) running a loop that pollsGET /actions/runs/{id}every 60 s and exits when status=completed, printing status+conclusion each tick. Don't poll in the foreground; don't sleep-loop on pty_read.
4. CI facts (.gitea/workflows/ci.yml + docker-compose.ci.yml)
- Single job "build + unit + e2e" on runner gitea-runner (docker-in-docker). Fresh DB per run (volume removed); compose project
flight-ci; app container on a static IP →E2E_BASE_URL=http://172.39.0.10:3000;PW_REMOTE_BROWSER=0(CI runs local chromium — the shared remote browser server at 192.168.1.4 cannot reach a stack inside dind);E2E_COMPOSE_ARGS="-f docker-compose.ci.yml -p flight-ci"is read by e2e/state.spec.ts for its mid-rundocker compose restart flight. - Flaky uplink → retry loops in ci.yml; an occasional "flaky" test (failed once, passed on retry) does NOT fail the run.
- Branch protection requires status check
CI / build + unit + e2e (pull request)— PRs cannot merge until green; CI never deploys to production.
5. E2E conventions (e2e/*.spec.ts) — read before touching tests
- Sim scale in tests: SCALE=1000; hub
HUB='EDDF'(Frankfurt); typical destination LEMD. flyHomeViaApi(page)self-heal at test start — interrupted runs can leave planes mid-flight; landing waits up to 240 s.- Pause-sim pattern for deterministic marker clicks: pause, act, assert, resume.
- Dev hook:
window.__flightMapexposes the live MapLibre map instance in dev builds (also__flightLastSnapshot). queryRenderedFeaturesis viewport-scoped → fitBounds/fitToAirports before asserting on far features; give the renderer ~300 ms after a camera jump.- expect.poll callbacks must be total — a throw inside fails the test instantly instead of retrying. Guard/filter everything you dereference.
6. ⚠️ The big one: Playwright evaluate serialization drops prototype getters (root cause of CI runs #69–#71)
MapLibre's queryRenderedFeatures returns live GeoJSONFeature class instances whose geometry is a lazy PROTOTYPE getter (own properties are only type/properties/id/source/state plus private fields). Playwright serializes evaluate results by copying own enumerable properties only, so every feature arriving in test context has NO geometry — silently.
- Verified empirically:
page.evaluate(() => { class A{constructor(){this.own=1} get geo(){return 42}} return [new A()] })→[{"own":1}]. - Symptom matrix (how it masqueraded as "the line isn't drawn" while the trace rendered fine on screen): strict length guard → timeout; direct
.geometry.coordinatesaccess →TypeError: Cannot read properties of undefined (reading 'coordinates'); defensive filter → clean 15 s timeout. - Rule: never return maplibre feature instances across page.evaluate — always map to plain objects in-page (
{type, id, properties, geometry: f.geometry}forces the getter to evaluate). Canonical pattern:renderedFeatures()in e2e/hover.spec.ts. Other specs that touch.geometrydo so inside evaluate (safe) or only read array length (safe).
7. MapLibre rendering facts (for map assertions)
- GeoJSON geometry is stored per-tile in INTEGER grids; long lines are clipped →
queryRenderedFeaturesreturns ONE FEATURE PER TILE the line crosses (the EDDF→LEMD leg ≈ 4–5 segments at z=4/5). - Coordinates are quantized to the grid: error up to ~half a grid step (~0.001° observed at z≥4, worst ~0.02° at z=0 with extent 8192). Endpoint assertions must use an EPS (hover tests use
HOVER_ROUTE_EPS_DEG = 0.05≈ 5 km — above worst-case quantization at any zoom, below the distance between any two EU airports) and check endpoints ACROSS ALL SEGMENTS: some segment must START within EPS of origin AND some must END within EPS of destination. Never strict equality on a single feature. - The true line endpoints survive clipping verbatim in whichever tile contains them.
8. App architecture quick map
components/Map.tsx— the MapLibre layer: sources/layers created in ensureLayers (dedicatedhover-routesGeoJSON source + top-most dashedhover-routeLineLayer, paint #facc15 width 4 dasharray [2,2];airports-target-highlightgreen #4ade80 ring added last so it sits above the line at its endpoint); applyHoverRef re-pushes data on every hover;window.__flightMapdev hook.lib/mapData.ts— pure GeoJSON builders (hoverRouteGeoJson,routesGeoJson,greatCircleArcslerp) + unit tests in lib/mapData.test.ts.- Hover protocol: panes report a HoverTarget via mouseenter/leave (
components/hoverTypes.ts):{kind:'plane';id} | {kind:'package';originIcao;destIcao;planeId?} | {kind:'route';fromIcao;toIcao}; the''sentinel means no hover. - Server: sim ticks server-side, client polls /api/state; OpenAPI spec generated from lib/openapi.ts and served at /api/openapi — update it whenever an endpoint changes.
9. Version history context (where we are)
- v0.22.0 (master @
5391636, tag v0.22.0, deployed): hover route trace on job-list rows (#22), on-board cargo rows (#25), Relocate panel preview (#27) + distinct green target-airport ring; e2e hardening per §6/§7 (commits505a58b+ab79d9b). - Before that: v0.21.0 Relocate (empty flight) panel in left pane, v0.20.0 On-board moved to left pane, v0.19.x CI-gate docs, v0.18.0 relocate empty planes + bigger airports, v0.17.0 toasts/notifications, … full history in CHANGELOG.md.
- As of 2026-09-20: zero open issues. Next work comes from PLAN.md "Later": weather effects (delays, fuel burn changes, closed airports), aircraft upgrades as money sinks, multi-region packs, multiplayer.
10. Standing user instruction & etiquette
- The user's standing order for this project: work through the issue list autonomously — implement → PR → CI green → merge → tag → deploy prod → close with comment → next — until told to stop.
- Small incremental changes; each part fully tested (unit + e2e) before moving on.
- Long terminal tasks → pty_* tools (deploys, builds). Background watchers → run_background_process with notify=true.
- Never push directly to master; never deploy from a feature branch; CI never touches prod — production deploys are manual only (
docker compose up -d --buildafter tagging).