Files
Flight-Ops/ORCHESTRATOR.md

10 KiB
Raw Permalink Blame History

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.

Credentials are deliberately NOT stored in this file — see §1 for where they live. Never paste token values into any tracked file.

1. Project identity

  • Repo: Gitea at https://git.kkhost.pl → org OpenCode, repo Flight-Ops (full name OpenCode/Flight-Ops). 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 --build deploys from here).
  • Prod URL: https://flight.dev.kkhost.pl (Traefik → docker-compose stack on this host). GET /api/version reports the running version.
  • Gitea API access for git.kkhost.pl: prefer the gitea MCP tools — they are already configured with a token. For raw REST/curl, read the current token from the local .env file at repo root (variable GITEA_TOKEN; .env is gitignored and must never be committed): source .env && curl -s -H "Authorization: token $GITEA_TOKEN" https://git.kkhost.pl/api/v1/user. (Incident note, Sept 2026: an early draft of this doc committed a live token to the repo; that commit was purged from all refs and the token has since been rotated — never repeat this.)
  • Current user: kamilkrzywda (id 1).

2. The loop (per issue)

  1. Small incremental change on a feature branch off master; connected issues may share one branch/PR with a single version bump.
  2. Bump package.json version + add CHANGELOG.md entry at top: ## v<version> — YYYY-MM-DD HH:MM then bullet points of what changed (docs-only changes get patch bumps too — precedent v0.19.1/v0.19.2).
  3. Local verification ONLY (there are NO local e2e runs): npx tsc --noEmit, npx vitest run (the whole unit suite must pass — the test count grows over time, so compare against the previous green run, never a hardcoded number), npx playwright test --list (all specs compile and list).
  4. 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). Note: if a PR was created via API rather than create-on-push, double-check auto-merge actually fires.
  5. Tag the version on master: git tag v<version> origin/master && git push origin v<version> (after git fetch origin master).
  6. 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 verify curl https://flight.dev.kkhost.pl/api/version returns the new version and the root page is HTTP 200.
  7. Close each issue with a comment: what changed, version, PR number, merge commit SHA, deployed URL.
  8. 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 a pull_request key.

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}/merge with body {"do":"merge","merge_style":"merge"}.
  • Gitea REST shapes (NOT GitHub's): list runs → {workflow_runs:[...]}; run jobs → /actions/runs/{id}/jobs returns {"jobs":[...]}; single job log via MCP get_job_log_preview (param tail_lines) or download_job_log to 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 polls GET /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-run docker 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.__flightMap exposes the live MapLibre map instance in dev builds (also __flightLastSnapshot).
  • queryRenderedFeatures is 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

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, across three consecutive failed CI runs in Sept 2026): strict length guard → timeout; direct .geometry.coordinates access → 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 .geometry do 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 → queryRenderedFeatures returns 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 sized for this (the hover tests define one in expectHoverRoute in e2e/hover.spec.ts — ~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 (dedicated hover-routes GeoJSON source + top-most dashed hover-route LineLayer, plus an airports-target-highlight ring layer added last so it sits above the line at its endpoint — current colors/widths live in Map.tsx, don't trust this doc for exact paint values); applyHoverRef re-pushes data on every hover; window.__flightMap dev hook.
  • lib/mapData.ts — pure GeoJSON builders (hoverRouteGeoJson, routesGeoJson, greatCircleArc slerp) + 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. Finding current state (this file keeps no snapshots)

This manual is meant to stay valid long-term, so it deliberately contains NO version or issue snapshots — they rot. To orient yourself when you take over: git log --oneline -10 + CHANGELOG.md for what has shipped; GET /issues?state=open&type=issues (filter out entries with a pull_request key) for what is left to do; PLAN.md's "Later" section for the roadmap backlog.

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 --build after tagging).
  • Never commit credentials. Tokens/secrets live in the MCP server config or with the user, never in tracked files (see incident note in §1).