Phase 15 — Live Risk¶
Defined 2026-09-08 by owner instruction, from the Live Event Mode charter (exploration/07-live-risk.md) whose rungs R0+R1 shipped as Phase 13 — Event Replay. Opened 2026-09-10 when Phase 14 — Solidify closed (its master sequence step 15; mid-review-phase-14.md); the backbone W0 had been pulled forward on 2026-09-09 by owner instruction. Serves Goal B (the backbone gains the feed-and-cadence half of live mode) and Goal C (a new showcase surface, the Live tab, and a fourth showcase moment). The engine is not touched.
What it is. The platform today can score a hazard that happened — one
observed footprint at a time, ingested by a person or a script, computed by a
button (Phase 13). Phase 15 closes the loop the owner described: an external
source — a satellite detection feed, a ShakeMap product, a drone orthomosaic
pushed by a partner, or an archived series replayed on a clock — lands as
time-stamped hazard observations through the same ingest pipeline every other
dataset uses, each observation is scored automatically on the
deterministic-event lane against the watched book, and a new Live surface in
climate-lama-ui shows the map, the ranked assets and what changed since the
last observation refreshing in front of a decision-maker, with sidebars for the
watch list and the selected event. The basis is built for real feeds; the phase
is walked on a replay feed over real archived data because that is what a
presentation can rely on (D10).
What it is not. No spread model, no weather, no forecast ensemble
(charter rung R3); no sensor tables or point-observation interpolation (R4
proper — only R4's first row, a pushed raster, ships as the ingress seam); no
push notifications, subscriptions or e-mail (guardrail 9 stands); no
auto-opened watches from a detection stream (D11); no fourth engine; nothing
copied from risk-assessment-tool. The visual revamp of the existing surfaces
and adaptation measures move to Phase 16 (D1) — renumbered again to
Phase 17 on 2026-09-11, when the owner defined
Phase 16 — Consistency to run beside this phase;
every "Phase 16" below that means the revamp stub now reads Phase 17.
The frame — one user question, one new surface¶
| Surface | User | The question it must answer | State at open |
|---|---|---|---|
Live (/live, new — the ninth nav entry) |
decision-maker, responder lens of the P3 persona; insurer reading their own book | "What is happening right now, what of mine is at risk, and what should we look at first?" | no such surface; the event screen is a nav-less nested route under a portfolio (/portfolios/:id/events/:eventId, ui src/App.tsx:154), reached only by clicking an Events row; the watch and every snapshot are API-only; compute is a button (ui src/pages/LiveEventPage.tsx:393-399); the UI re-polls every 30 s but nothing upstream ever produces a new snapshot |
The other eight surfaces are unchanged except where named below (Home gains one card; the portfolio page's Events section links into the new surface).
Ground truth at open — archived to archive/phase-15-log.md (consumed 2026-09-08).
Design record (D1–D18 — one line each; full prose in archive/phase-15-log.md)¶
One line per decision: the decision, then the one constraint that binds a builder. Nothing is dropped or renumbered — the prose each line compresses is in the archive, and the dated status sections below amend several of them.
- D1 — Phase 15 is Live Risk; the revamp stub becomes the next charter phase (renumbered 17) — Live is built as the revamp's pattern pilot, not a one-off.
- D2 — The phase opens at Phase 14's close; backbone W0 could be pulled forward only after B11 — W0's
api/v1/hazards.pyanddocker/hotspots land after #1237. - D3 — Ingress is a
LiveFeedcontract (replay,firms_nrt,usgs_shakemap) plus a push route — every outbound URL is code-built from a fixed per-adapter host;[]is never a snapshot. - D4 — A beat tick polls every enabled feed and dispatches
ingest_hazard— one org-scoped transaction per org; per-feed isolation; no-op when the flag or schema is behind. - D5 — A committed snapshot auto-computes on the deterministic-event lane —
origin="feed",notify=false,metered=false; feed jobs stay out of the human job lists. - D6 — Migration 0092 adds the watch and feed model, including
live_feeds— typed Alembic ops, FORCE RLS, lands alone, with amigration_windowdowngrade test. - D7 — The read model gains latest/snapshot fields, exposed count and value, and a delta route — one query, no N+1;
E_RESULT_STALE/E_RESULT_VECTOR_ABSENT, never a 500. - D8 —
GET /v1/infogainsfeatures: {live_mode, geocoding}and the UI mounts Live on it — the exact-key-set assertions stay exact, never relaxed to a subset. - D9 —
/liveis rail · map + scrubber · inspector · drawer, and nav becomes nine —/portfolios/:id/events/:eventIdredirects to/live/:id, keeping Phase 13 URLs alive. - D10 — Presentation mode is the replay feed over the real Evia 2021 series —
valid_atis the real observation instant, never re-stamped; the watchkindisreplay. - D11 — Feeds belong to one watch; nothing opens a watch by itself — a detection stream that creates watches is the R3 rung and stays parked.
- D12 — The lane is peril-agnostic, but no flood feed adapter ships this phase — a depth footprint must come from a primary provider, never from
risk-assessment-tool. - D13 — An un-profiled
live-workerservice in all three compose files — afteralembic upgrade head, restartlive-worker+beatand assert the head before the first tick. - D14 — No dataset retention is added; a feed carries
max_snapshots— reaching the cap pauses the feed with alast_error; nothing is ever deleted. - D15 — No new
NotificationType; auto-compute writes no usage row — feed snapshots keep storage accounting but are never charged against the upload quota. - D16 — Science posture is the charter's: two numbers, screening grade, no MMI — copy says near-real-time and states each feed's latency budget; no unmeasured figure ships.
- D17 — Verification seams are an injected
now, an injectedhttpx.Clientand the replay fixture — the smoke gains a series leg; acontract-map.tsentry per new client method. - D18 — A fourth showcase moment, Moment 4 (decision-maker) — Goal C's enumeration in
plan.mdandCLAUDE.mdgains it.
Guardrails — every Phase 15 issue¶
Inherited from Phase 13 (6–11: no sentinel frequency; _SCREENING_GRADE_PERILS
byte-identical; snapshots invisible to probabilistic readers; no push, no
"alert", the life-safety line everywhere; real data through the real API with a
volume-budget row before download; migration PRs gated) and Phase 11 (1–5), plus:
- The tick calls only service functions the public API also calls. The
worker task is a sync entry point over
core/live_feeds.py; nothing inworker/live_feeds.pyreaches a repository directly. - Every feed adapter is a
LiveFeedunderingest/feeds/, constructed with an injectable transport, unit-tested offline, and importing nothing fromscripts/. - Feeds never cross a watch or an org. A feed reads its own watch's AOI and
writes snapshots under its own
live_event_id; no adapter opens a watch. LIVE_MODE_ENABLEDgates route, queue and writer. With it off the tick no-ops, the feeds API refuses withE_LIVE_MODE_DISABLED, and/v1/info.features.live_modeisfalse.- Freshness is truthful. Every surface shows the snapshot's observed-at, its age at render and its source version; the feed strip shows the provider's latency budget; copy says near-real-time, never "live" for a 1–3 h feed.
- A replay is real data on a real clock. A replay feed releases archived
provider observations with their original
valid_at; re-stamping an observation to "now" is refused in code and in review. - Every job the tick creates is attributable, unmetered and bounded.
created_by_user_idon the watch,origin="feed"on both jobs,notify=falseandmetered=falseon auto-compute,max_snapshotson the feed, a Redis lock per feed with expiry,apply_async(expires=cadence). - No live-mode worker code opens an unscoped session. Every tick runs per
org under
set_org_guc_sync; a DB test proves two orgs' feeds each tick exactly once and never see each other. - Feed URLs are code-constructed. An adapter's base host is a constant in
its module; feed
paramsnever carry a host or a URL; a test enforces it. Adapters are trusted in-process code (ADR-027) and the SSRF allowlist does not apply to them — the plan says so rather than implying otherwise. - Nothing is removed without its walk (Phase 14 guardrail 2): the portfolio
event route redirects rather than disappears, and Moment 2 steps 8–9 are
re-walked on
/live/:eventIdin the same PR that adds the redirect. - PR typing per Phase 14 D15: planned work is
feat:/refactor:;fix:is reserved for rework of Phase 15 code; the circuit-breaker (fix ratio > 25 %, measured withscripts/rework_rate.py --follow-ups) stops dispatch.
Scope — waves and items¶
Dependency line: W0 → W1 → {W2, W3} → W4. Inside W0: W0.1 alone first, then
the rest concurrently where Files: are disjoint. Inside W1: W1.1 → W1.2 → W1.7;
W1.3 ∥ W1.4 after W1.1; W1.5 and W1.6 after W1.1; W1.8 last. W2 depends on
W1.7's contracts (written fully qualified as CortoMaltese3/climate-lama#N).
Sizes are drafts; /phase-split grounds and re-sizes each item against the code.
W0 — Foundations (backbone)¶
| Item | Delta (grounding) | Acceptance |
|---|---|---|
| W0.1 Migration 0092 (lands alone) | models/live_event.py (5 columns today); no live_feeds; event_impact_results (0088); models/job.py has no origin marker |
columns and table per D6 including jobs.origin; FORCE RLS via the 0082 predicate; _ORPHANED_ORG_SCOPED_TABLES (tests/db/test_app_role_rls_isolation.py:67-87) updated; typed ops only; downgrade() tested through tests.conftest.migration_window; alembic upgrade head from scratch under migrations-gate.yml |
W0.2 Settings + /v1/info.features |
config.py:736 one flag; main.py:324 three keys; tests/test_api/test_info.py:26 exact key set |
live_feed_tick_seconds (300), live_feed_max_snapshots (500), live_aoi_buffer_km (10), live_feed_http_timeout_seconds (30), firms_map_key (secret, from env), usgs_min_magnitude (4.0); features.live_mode on /v1/info; test_info.py asserts set(data) == {"version", "environment", "git_sha", "features"} and data["features"] == {"live_mode": settings.live_mode_enabled} for both flag values; .env.example + .env.example.prod (the prod value documented inline) + tests/test_env_template_parity.py green; docs/CLIMATE_LAMA.md settings table |
W0.3 live queue + live-worker service |
celery_app.py:64-93 routes everything to compute; three compose files have worker + beat only; deploy-prod.yml:518 "all seven services", :584-585 two container ids |
task_routes entry for the tick; live-worker in all three compose files per D13; LIVE_MODE_ENABLED wired on beat and live-worker too; REDIS_EXPORTER_CHECK_SINGLE_KEYS (docker-compose.yml:496) gains live; deploy-prod.yml comment count updated and before_live_worker added to the UI-deploy container-id proof; docs/DEPLOYMENT.md + RUNBOOK.md (rows: live-worker triage; "live-worker up, live_feeds missing" = the pre-migration window); acceptance box for the real-stack proof stays on the owner's WSL walk (builders have no Docker) — the PR lands Refs. Seam paths (compose, deploy workflow, deploy docs) → Opus |
W0.4 LiveFeed contract + registry + replay adapter |
no ingest/feeds/; ingest/sources/base.py is the sibling shape |
ingest/feeds/base.py (LiveFeed, Observation, FeedState; poll() returns [] for "nothing observed" — an empty raster is never an Observation), ingest/feeds/registry.py (FEED_BY_KIND), ingest/feeds/replay.py (reads manifest.json + rasters from a directory under the worker volume, releases the next unreleased observation per poll, cursor = index); a test that no adapter reads a host or URL from feed params (guardrail 22); injected now; unit tests offline |
W0.5 Rasteriser + grid parser into core/ingest/ |
scripts/packs/firms_bt_event.py:939-1190 + _silence_transport_url_logging (:1369), shakemap_event.py:755-1125; docker/worker.Dockerfile never COPYs scripts/ and [tool.mypy] files = ["src/climate_lama"] |
core/ingest/firms_points.py (CSV rows → 1 km max-bright_ti4 GeoTIFF, cos-lat grid, per-day split, and the URL-path-secret logging suppression the FIRMS adapter reuses) and core/ingest/shakemap_grid.py (grid.xml → PGA g GeoTIFF, clip window); the two packs become thin callers with byte-identical output on the vendored fixtures (test); mypy + ruff cover the moved code |
| ~~W0.6 #1155 tile proxy out-of-bounds~~ | EXISTS — PR #1160 shipped the empty-PNG answer on 2026-09-03 (api/v1/tiles.py:90, 172); two of #1155's three boxes are ticked |
Not a build item. The remaining manual box ("panning the Evia replay map on prod produces no 502s") is W3.3's checklist; #1155 closes on the walk |
| W0.7 #1240 sidecar drains the body before an error | inherited from Phase 14 residual triage | per the issue's own acceptance boxes |
W1 — Feeds, the tick, and the lane's missing numbers (backbone)¶
| Item | Delta (grounding) | Acceptance |
|---|---|---|
W1.1 core/live_feeds.py + worker/live_feeds.py + beat (split into W1.1a service + repository and W1.1b worker task + org loop + lock + lifted dispatch — see the split cards) |
precedents: worker/job_watchdog.py::reap_stale_jobs (:221, org loop), dataset_ingest.py::_create_ingest_jobs (:470) + its apply_async (:752); core/job_dispatch.py:75-192 is async-only; the worker has no Redis client; #1271 must land first |
service: create_feed, list_feeds, update_feed, delete_feed, poll_feed_now, tick_due_feeds(now), on_snapshot_committed; task tick_live_feeds on live (beat every live_feed_tick_seconds, expires= set) as a list_org_ids_sync() loop with set_org_guc_sync per org; a worker-side Redis client + SET NX EX per feed; per-feed isolation (one bad feed never aborts the rest); staging under dataset_source_download_dir/live/{feed_id}/; jobs (origin="feed") + ingest_jobs rows and ingest_hazard dispatch on compute with the single-event params; create_and_dispatch_job_sync added beside the async one; flag off → no-op with a log line; missing relation (ProgrammingError) → no-op with a log line, never a crash loop; unit tests with an injected clock and the replay adapter; DB test: two orgs' feeds each tick exactly once and never see each other; max_snapshots pause |
| W1.2 Auto-compute hook | aggregate_and_commit.py::_schedule_cog_build (:1904, called at :2314); worker/tasks.py::_record_compute_usage (:89, called at :1775), the unconditional JOB_COMPLETE emit at :1795 |
on_snapshot_committed called from the commit callback; dispatches compute_event_impact with the watch's selector, user_id, notify=false, metered=false, origin="feed"; the task honours all three (no job_complete, no usage_events row); a watch with auto_compute=false gets no job; DB tests on a fixture book: ingest one snapshot → exactly one event_impact_results row appears; N ticks add 0 to usage_by_period.run_count for the org |
W1.3 firms_nrt adapter |
pack AreaRequest/plan_requests (firms_bt_event.py:472-645); core/ingest/coverage.py:32 (floor default 0) |
area CSV over the watch AOI, VIIRS_SNPP_NRT, day_range 1, cursor = newest acq_date+acq_time seen, dedupe on (latitude, longitude, acq_time), cadence floor 300 s (5000 tx / 10 min cap), the key never logged (suppression from W0.5's firms_points), per-overpass raster in K; a poll with zero rows over the AOI returns [] — no job, no snapshot, last_error null, last_polled_at stamped (unit test); MockTransport tests on the vendored CSV; volume-budget row for the NRT product before the first prod poll |
W1.4 usgs_shakemap adapter |
core/ingest/shakemap_grid.py (W0.5, landed in #1283) for the grid half; the request-planning helpers scripts/packs/shakemap_event.py::resolve_event_id / ::fetch_event_products / ::select_shakemap_product (:537-660) stayed in scripts/ and are a reference only, never imported — the adapter re-derives that shape against UsgsShakeMapFeedParams (guardrail 13: scripts/ is not in the worker image; #1334 item 3); charter §6 marks the FDSN bbox query and the artifact set for foreign events [UNVERIFIED] |
first task, time-boxed: re-derive the FDSN query (query?format=geojson&minmagnitude=&minlatitude=…&producttype=shakemap over the AOI bbox) against a real Greek event and pin its exact form by a recorded-response test — fallback if producttype filtering 404s: query by bbox + magnitude and resolve products per event the way select_shakemap_product does in the pack (mirrored, not imported); cursor = per-event updateTime; a new event or a newer updateTime → grid.xml → PGA g GeoTIFF clipped to the AOI; source names us/atlas + version; the adapter records the origin-time → updateTime delta per event (D16); zero events → [] |
W1.5 POST /v1/live-events/{id}/observations |
hazards.py:779-843 multipart upload (streaming, cap, quota; single_event HDF5-only per core/hazard_upload.py:166-196; no valid_at/intensity_unit/live_event_id fields); hazards.py:501-560 the mode-3 seam |
a new route: multipart GeoTIFF + valid_at (required — never defaulted to now, guardrail 17) + intensity_unit (required, checked against seeded_units_for(haz_type)) + label; reuses the upload route's staging, cap and quota helpers, then builds the mode-3 params (file_path, event{label, valid_at}, live_event_id, intensity_unit) and dispatches exactly as POST /v1/hazards/ingest does; live_event_id org-checked via assert_live_event_visible; refused when the watch is closed or live mode is off; triggers auto-compute like any snapshot; jobs.origin="user"; SDK method; contract test. Sized as a route, not "a field" (reassess correction A1) |
| W1.6 Exposed count/value + delta | worker/tasks.py:1604-1620 has the intensity matrix and the per-point loss vector at hand; api/v1/live_events.py:371-470; loss_exp_path nullable (models/event_impact_result.py:98-104) |
exposed_count/exposed_value = assets whose cell intensity is finite and above the curve's first non-zero x (per peril, documented); stored on the result and read on GET …/results/{job_id}; GET …/results/{job_id}/delta?against= per D7, paged, E_RESULT_STALE on drift, E_RESULT_VECTOR_ABSENT when either side has no stored vector (test on a hand-inserted null-vector row); both keys absent from every annual payload (test) |
| W1.7 Watch + feeds API, jobs filter, list summary, codes, SDK | api/v1/live_events.py (7 routes); api/v1/jobs.py::list_jobs (status/job_type filters only); core/errors.py:53-58; main.py::_CODE_TO_HTTP_STATUS (:345) |
PATCH /v1/live-events/{id} (name, auto_compute, aoi, kind); POST/GET/PATCH/DELETE …/feeds, POST …/feeds/{feed_id}/poll (ANALYST+; rate-limited like the other write routes); POST /v1/live-events accepts aoi, exposure_dataset_id, auto_compute, and derives the AOI from the book when omitted; list rows per D7 in one query; GET /v1/jobs excludes origin="feed" by default with ?include_automated=true as the opt-in — acceptance: after ten simulated ticks the default page is unchanged; codes E_LIVE_FEED_NOT_FOUND, E_LIVE_FEED_KIND_UNKNOWN, E_LIVE_FEED_PAUSED, E_LIVE_WATCH_SELECTOR_CONFLICT, E_RESULT_VECTOR_ABSENT mirrored into the SDK (sdk-regenerate.yml real check); LiveEventsAPI gains create/patch/close/feeds/poll/observations; docs/CLIMATE_LAMA.md API table |
| W1.8 Smoke series leg + fixtures staging | scripts/smoke_e2e.py:2290-2440 single-snapshot leg; sdk-smoke.yml stages one .tif |
the series leg per D17 on the vendored Evia series (W3.1); sdk-smoke.yml stages the series directory; the existing event leg unchanged; E2E_READY_SERIES_GRC line with snapshot and result counts |
W2 — The Live surface (climate-lama-ui)¶
| Item | Delta (grounding) | Acceptance |
|---|---|---|
| W2.0 Client foundation | src/api/client.ts::listLiveEvents…getEventResult (:1644-1710), src/types/index.ts::LiveEvent…EventResult (:2092-2190), src/auth/AuthContext.tsx (no features), tests/e2e/smoke/contract-map.ts::Resources (:558, liveEventId only) |
types + methods for info.features, list summary fields, watch create/patch with AOI and selector, feeds CRUD + poll-now, observations upload, exposed_*, delta; every method in tests/e2e/smoke/contract-map.ts; no callers changed; Deps written fully qualified |
W2.1 /live shell + rail + nav + redirect |
App.tsx:38-47 eight entries; :154 nested event route |
nav.live entry mounted when features.live_mode; /live and /live/:eventId routes; /portfolios/:id/events/:eventId → redirect with ?portfolio=; watch-list rail per D9 (freshness dot, kind chip from kind, sparkline via recharts, sort by latest_valid_at); off-state copy; liveEventChip.ts deleted with its test; the portfolio Events section links to the new route |
| W2.2 Event view composition | LiveEventPage.tsx (rail + map + scrubber) |
/live/:eventId = rail (kept) · map with AOI outline + admin boundaries layer cards · scrubber with Follow latest (auto-advances on a new snapshot while on; a manual scrub turns it off) · collapsible inspector · bottom drawer table (sortable, grouped by layer); useIntervalFetch keeps the 30 s cadence while open; resize + stacked-below-sm test |
| W2.3 Create / close / feed controls | none exists | Create-watch dialog per D9 (book picker over /v1/portfolios, dataset picker over /v1/exposure-datasets, AOI = book extent or current map view, feed kind + params, cadence, auto-compute); Close; feed status strip (kind, cadence, last/next poll, latency budget, last error, Poll now); ANALYST+ gating mirrors the API |
| W2.4 Two numbers + delta panel | EventLossHeadline shows one figure |
headline shows exposed count/value beside the € loss with the screening badge; delta panel ("since the previous observation": Δ loss, Δ exposed, top movers, newly exposed) from the delta endpoint; aria-live mirror; no "alert" string (test) |
| W2.5 Home "Live now" card | HomePage.tsx placeholder → Phase 14 U6 cards |
one card listing open watches with latest loss and freshness, linking into /live/:eventId; empty state; hidden when features.live_mode is false |
| W2.6 Specs, i18n, docs | events.spec.ts; live.json |
live.spec.ts on the smoke stack: create a watch with a replay feed through the dialog, Poll now ×2, map and scrubber update, delta panel renders; Moment 2 steps 8–9 re-walked on /live/:eventId; CLIMATE_LAMA_UI.md routes and nav of nine |
W3 — Operations (trackers; routed by Blocker:, not by label)¶
| Item | Blocker | Acceptance |
|---|---|---|
| W3.1b second replay series (#1297) | Blocker: credential:FIRMS_MAP_KEY |
a second real series (Rhodes 2023-07 or Attica/Varnavas 2024-08) fetched by the pack after its volume-budget and manifest rows land; W3.1a's vendored Evia fixture (one acquisition day, 2021-08-06) already shipped as a build issue |
| W3.2 flood footprint sourcing spike (#1284) | Blocker: agent — read-only research, no prod and no credential |
one comment on the issue naming a primary-provider depth footprint with licence and resolution, or the finding that none exists — then close; no code |
| W3.3 prod ops (#1300) | Blocker: agent — prod SSH plus a 24 h wall-clock gap; no human step in it |
live-worker up after the release; three watches opened (the Evia replay at an accelerated cadence, a firms_nrt watch over a Greek book's AOI, a usgs_shakemap watch at M ≥ 4.5); 24 h of last_polled_at advancing with last_error null recorded on the tracker |
| #1155 manual box (rides W3.3) | Blocker: agent — ./dc logs api \| grep UPSTREAM_TILE_ERROR |
panning the Evia replay map on prod produces no 502s; PR #1160 already ships the empty-PNG answer, so this box is the issue's last |
W4 — Closeout¶
| Item | Delta | Acceptance |
|---|---|---|
| W4.1 Docs | plan.md, showcase-script.md, CLAUDE.md (nav of nine, fourth lens), docs/CLIMATE_LAMA.md, CLIMATE_LAMA_UI.md |
Moment 4 steps flipped on the phase-end walk; status log; route and API docs current |
| W4.2 Release + mid-review | migration 0092 rides the release; deploy-prod.yml migrates after up -d (D13) |
pg_dump first; coordinated R for both repos; after alembic upgrade head: docker compose … restart live-worker beat, assert SELECT version_num FROM alembic_version = the release head (0093 since #1388; 0094 if #1365's migration lands first), then confirm the first tick's log line — before any watch is opened. Then the one phase-end walk (exit criterion 2), once, after the release. docs/plan/mid-review-phase-15.md carries the verdict, the tagged findings and the two rework counters from rework_rate.py --follow-ups, plus ~/.claude/scripts/usage_scan.py --since <phase start> --by-build on both repos — a sizing-tier boundary moves only on a same-window pair of them (the tier-2a Sonnet trial of 2026-09-10 is decided here) |
Exit criteria¶
State at closeout — 2026-09-17 (/build 1298). Six of seven met; the phase
closes on criterion 4 alone. Evidence is linked per criterion below; the walk
record for 2, 6 and 7 is mid-review-phase-15.md.
| # | Criterion | State | Evidence |
|---|---|---|---|
| 1 | Build issues at 0 open on both milestones | ✅ met | backbone Phase 15 -- Live Risk and the UI milestone both hold 0 open build issues once this closeout merges; the three remaining backbone rows are trackers — #1300 (Blocker: agent), #1284 (Blocker: agent) and this issue — and #1297 left the milestone for the standing ops list on its credential:FIRMS_MAP_KEY blocker |
| 2 | One phase-end walk on prod after the release | ✅ met | owner walk 2026-09-17, UI v0.15.1 / backbone v0.25.0 → v0.26.0: mid-review-phase-15.md § "Check — the walk, with ids"; markers flipped in showcase-script.md Moment 4 steps 1–7 and Moment 2 steps 8–9 |
| 3 | Series smoke green on trunk with the series leg | ✅ met | sdk-smoke.yml run 34566448950 at 5f8d163, N = 1 per decision 7 |
| 4 | A real wire proven once — 24 h of poll health | ⏳ pending — the only item left | FIRMS watch d33c553d / feed 1f4de1a9 first polled 2026-09-17T19:10:00Z, USGS watch aafa30f7 / feed 6d0fba09 at 19:15:00Z, last_error null on both; the read is due 2026-09-18T19:15Z on #1300 |
| 5 | Guardrails 6–20 hold on trunk and prod | ✅ met | FIRMS_MAP_KEY absent from every prod log (0 hits); the life-safety line rendered on both surfaces; no annual number on an event figure — mid-review-phase-15.md § "Exit criteria tracking" |
| 6 | Surfaces — nav = 9, Live gated, redirect, no provider chip | ✅ met, with one reporting defect | all four hold on prod; the rail's chip is correct in the database and on the detail page but is rendered from a stale pre-feed object until a reload — filed ui#482, found-in:phase-15 |
| 7 | This record: mid-review + the two rework counters | ✅ met | mid-review-phase-15.md with the verdict, seven tagged findings and both counters (backbone escape 0.64 / partial 0.24, UI 0.41 / 0.65 — over the breaker in both repos) and the tier-2a Sonnet trial adopted ($13 vs $79 per build at equal rework); plan.md banner, status row and status log flipped by this issue |
Three walk findings carry found-in:phase-15 and are drained by the next
/phase-split, not by this phase: ui#481 (success toasts render as
errors), ui#482 (a new replay watch chipped Near-real-time) and
#1444 (a
cadence below live_feed_tick_seconds accepted and misreported). The one
finding folded into this closeout instead of filed — F-5, no documented way to
stage a replay series on prod — is answered by
DEPLOYMENT.md § "Staging a replay series on prod".
The criteria in full:
- Build issues at 0 open on the
Phase 15 -- Live Riskmilestone in both repos. Trackers do not count — #1297 (Blocker: credential:FIRMS_MAP_KEY) leaves the milestone for the standingopslist rather than holding the phase open; a decision is settled in-run by the framework's decision protocol (2026-09-17) and is never a tracker. - One phase-end walk, by the owner on prod, run once after the release,
scripted from the showcase moments this phase touched: Moment 4 — open
/live, watch the Evia replay feed advance on the map without touching anything, read the two numbers and the delta panel, open the drawer, create and close a watch from the UI — and Moment 2 steps 8–9, re-walked on the new/live/:eventIdroute. Findings are tagged and either fixed in one closing wave or filedfound-in:phase-15and drained by the next/phase-split. - Series smoke:
sdk-smoke.ymlgreen on trunk with the series leg — three replayed snapshots, three auto-computed results,exposed_count > 0, a delta between consecutive results, noead/aai, and the last snapshot's ranked list reproduced by a second identical run (the charter §11 criterion). - A real wire, proven once: a
firms_nrtwatch and ausgs_shakemapwatch on prod poll for 24 h withlast_polled_atadvancing andlast_errornull (a detection is not required — poll health is the criterion; if either feed ingests a real observation in the window, that is recorded as evidence). - Guardrails hold: 6–20 verified on trunk and prod as Phase 13's were.
- Surfaces: nav = 9 with Live mounted only when
features.live_mode;/portfolios/:id/events/:eventIdredirects; no chip is derived from a provider string. - Record:
docs/plan/mid-review-phase-15.mdwith a verdict, tagged findings and the two rework counters (escape rate,partial rate) fromscripts/rework_rate.py --follow-ups;plan.mdbanner and status log flipped.
Conflict hotspots (files touched by >1 item)¶
Backbone: src/climate_lama/api/v1/live_events.py (W1.5, W1.6, W1.7a, W1.7b —
serial), schemas/live_event.py (same four), core/upload_streaming.py (W1.5,
read-only reuse), api/v1/jobs.py + db/repositories/job_repository.py (W1.7a),
models/live_event.py + models/event_impact_result.py + models/job.py +
models/enums.py + migrations/ (W0.1 only, lands first), core/errors.py +
core/exceptions.py + main.py + sdk/python/climate_lama_client/errors.py
(W1.6 then W1.7b — serial), sdk/python/climate_lama_client/{client,aio}.py
(W1.5, W1.7b), config.py (W0.2, W1.7b), worker/celery_app.py (W0.3 routes,
W1.1b include + beat — serial), worker/tasks.py (W1.2, W1.6 — serial),
worker/ingest/aggregate_and_commit.py (W1.2), worker/dataset_ingest.py +
core/ingest_dispatch.py + core/job_dispatch.py (W1.1b), core/live_feeds.py
(W1.1a, W1.1b, W1.2, W1.7b — serial), ingest/feeds/registry.py (W0.4, W1.3,
W1.4), scripts/packs/* (W0.5), tests/test_worker/test_rls_lanes.py (W1.1b),
tests/fixtures/reference_datasets/README.md (W3.1a), scripts/smoke_e2e.py +
sdk-smoke.yml (W1.8), docker-compose*.yml + .github/workflows/deploy-prod.yml
(W0.3), tests/test_api/test_info.py (W0.2), docs/DECISIONS.md (W0.4),
docs/data/volume-budget.md (W1.3, W3.1b), docs/CLIMATE_LAMA.md (W0.2, W1.5,
W1.7b — one docs-editing issue per wave, first), docs/RUNBOOK.md (W0.3, W1.8).
UI: src/api/client.ts + src/types/index.ts (W2.0 only), src/App.tsx (W2.1),
src/pages/LiveEventPage.tsx (W2.2, W2.4 — serial), src/components/live/*
(W2.2, W2.3, W2.4 — serial inside W2), src/i18n/locales/en/live.json (every W2
item — serial), tests/e2e/smoke/contract-map.ts (W2.0).
Orchestration sketch — archived to archive/phase-15-log.md (consumed 2026-09-10).
Orchestration — status 2026-09-10 and the build roadmap (sessions, deps, gates)¶
History (2026-09-10, first revision — PRs #1337 #1339 #1340). Phase 14 closed at PR #1338
(e391d6a); W0.4 #1282 and W0.5 #1283 had landed through the pulled-forward session
lama-phase-build-P15-W0 (PRs #1331 #1332, rework #1335, residue #1334, gate 34447716218 green
at be05f81, convergence 1 / 2). The two facts that gated the first dispatch are both spent: the
W0 convergence trip was answered by the owner starting P15-B1, and the "0092 must not ride a Phase
14 hotfix" guard became moot once F-12 closed the phase. The two W1.4 rows that pointed the
usgs_shakemap adapter at scripts/ were corrected in that revision (#1334 item 3). The second revision
(2026-09-11, PR #1391) recorded P15-B1 … B6 and P15-U0 as landed and re-cut the remaining rows;
this third revision (2026-09-13) records P15-R1 and P15-U1; the fourth (2026-09-14) records
P15-U1′ and the hygiene row P15-R-0′, with owner decisions 3–6 and 9 resolved.
Status 2026-09-13 — P15-R1 and P15-U1 landed; what remains¶
State (backbone trunk 4b57307, UI trunk b7df549; prod unchanged at v0.24.1 / UI v0.14.1,
Alembic 0091). Backbone: every W0–W1 build issue and the residue lane A are merged and
closed — 28 closed on the milestone; Alembic head 0093; trunk gate ci.yml run 34689176625
green at 4b57307 (six R1 merges proven ancestors); sdk-smoke.yml 34566448950 green at
5f8d163 (exit criterion 1 at N = 1; nothing on the series path has moved since). UI: ui#378 and
ui#380 closed; ui#379 and ui#424 merged but open on Refs — their last box is a smoke-executed
assertion, and the UI smoke gate is red for an environment reason: e2e-smoke.yml run
34683624847 at 1da8a94 died in compose pull before a single spec ran (minio/mc anonymous pull
denied; the two private ghcr.io images answer unauthorized on a :latest HEAD) — filed as
ui#449 [CRITICAL]; the wave touched no compose or workflow file, and the last green smoke run
is 34562279358 at b0052fc. UI ci.yml 34684673205 is green at b7df549. No open PRs and no
held worktrees in either repo. Open on the milestones: backbone 9 (build: #1267 #1314 #1315
1298; trackers: #1284 #1297 #1299 #1300 #1155), UI 10 (build: ui#379 ui#381–#384 ui#424; on¶
Refs until the release tag: ui#396 ui#397; on an owner decision: ui#421 item 6; undisposed:
ui#377). Off-milestone found-in:phase-15 residue: backbone #1357 #1365 #1398 (unsized), UI
ui#449 (the smoke stack) and ui#450 (test-log noise).
Update 2026-09-14 (backbone trunk e8f1be9, UI trunk 54f64a4). P15-U1′ landed: ui#449 → UI PR
453 (MinIO pulled from quay.io by digest, api/worker built in the smoke stack) and ui#450 closed;¶
e2e-smoke.yml run 34793955800 green on main at 54f64a4 and it executed ui#379's step-9
redirect (events.spec.ts:314) and ui#424's geocode 503 contract (contract.spec.ts:62), so both
were closed on that proof. P15-R-0′ is done (below). Phase 16 is building on the same trunk:
P16-B1 merged five PRs (#1400–#1404, incl. deploy-prod.yml's geocoding profile) and P16-B2 is
running beside P15-U2 — the release row re-derives its state at bump time, and the two lanes share
the three-builder cap. One consequence already visible: #1369 is OPEN with its code merged (PR #1403)
— the Phase 16 orchestrator's to close, never a dependency for a Phase 15 issue. Off-milestone
residue after R-0′: backbone #1357 #1365 #1398 (now scoped and sized) #1405 (MinIO images off
Docker Hub — the backbone half of ui#449, found after the v3 revision) #1410 (the structural
split of #1398) #1408 (ui#377 item 3); Phase 16 gained #1406 #1407 ui#454 ui#455 from the
ui#421 / ui#377 dispositions.
Update 2026-09-15 (backbone trunk 7ad780c, UI trunk 6564e5d). P15-R2, P15-U2 and P15-U3
landed, and P15-R is half shipped. Backbone v0.25.0 is live and verified: tag at 7ad780c,
ci.yml 34965255088 and sdk-smoke.yml 34965258562 green at that SHA, release.yml 34966002412
green, /v1/info returns version 0.25.0 with the tagged git_sha and features.live_mode: true,
and prod Alembic is at 0094 (head) with a pre-migration dump at backups/20260915_120251. The
first gate at the bump SHA a8b850d was red: #1422's validation envelope turned the SDK smoke's
malformed-uuid case from E_INTERNAL into E_VALIDATION. PR #1423 fixed the assertion and added
a fifth distinct code, so the gate did its job. The UI half did not ship. v0.15.0 was tagged
on e9d0c21, the v0.14.1 bump commit, taken from a local checkout 21 commits behind origin/main.
publish.yml 34966461242 went green on it, since verify-ci only proves the tagged SHA is green
and never that the SHA is the release. deploy-prod.yml 34966801293 then failed closed at #1315's
new bundle-identity step: still serving the same UI bundle (assets/index-Qxd715a3.js). Prod
serves backbone 0.25.0 with UI 0.14.1 and nothing regressed, but there is no Live surface yet. The
release session's first guess, a GHA layer-cache bug, is refuted: the tag object points at
e9d0c21 and package.json at the tag reads 0.14.1. The guard is ui#461, a tag-vs-
package.json check in verify-ci. Residue after these waves: backbone #1421 (R2, four
findings), #1267 (one box: owner closes on local proof or pays for a CI red run), #1315
(closes on the UI re-deploy, whose failed-closed run already proves the "same bundle → fail"
half), and the older off-milestone #1408 and #1410. The UI Phase 15 milestone holds ui#396 and
ui#397 (they close on the corrected publish run) and ui#461. The Evia series still has N = 1, so
live.spec covers one snapshot; the wider series is #1297's (U3's deferred note, not filed). The
bump→tag backbone freeze is lifted (tag v0.25.0 pushed). Phase 16 backbone lanes may merge.
Landed sessions — the ledger (merge SHAs are squash commits on main; a "gate" is a ci.yml
run whose SHA is a descendant of every merge in the row):
| Session | Issues → PRs | Gate(s) | Residue filed | Filed / completed |
|---|---|---|---|---|
| P15-W0 | #1282 → #1331 bae28b2 · #1283 → #1332 a8c2cdb · rework #1335 be05f81 |
34447716218 green be05f81 |
#1334 (off-milestone) | 1 / 2 |
| P15-B1 | #1279 → #1341 f6f3592 (migrations-gate 34464897193) · #1281 → #1342 08ea4a6 · #1290 → #1343 04f9d57 · #1280 → #1344 c9d76c9 · #1240 → #1345 f50d324 · #1336 → #1346 a5c69f5 · fix #1349 603604b · #1347 resolved in-run by #1352 27939aa + #1354 451b705; WSL walk green for #1281 (--clean-slate, WALK_EXIT=0) |
34491744200 at 451b705 (1 red: a #1348 × #1351 composed test, fixed by B3's #1356); UI e2e-smoke 34474099201 green |
#1347 (closed) | 1 / 6 → 0 after resolution |
| P15-B2 | #1285 → #1348 3c4f3e7 · #1286 → #1350 44781b1 · #1287 → #1355 b4f420d · guard fix #1353 00da839 |
34493606730 green 38d37dc |
#1357 (off-milestone) | 1 / 3 |
| P15-B3 | #1288 → #1351 2eea62f · #1289 → #1359 28d1ad3 · fixes #1356 38d37dc, #1361 f22df2e, #1362 98e9533 |
34504632227 green 98e9533 |
#1358 #1360 (off-milestone) | 2 / 2 |
| P15-B4 | #1291 → #1363 ffce6e1 · #1292 → #1364 a50b1a5 |
34520122409 green a50b1a5 |
#1365 (off-milestone), #1367 (milestone, [CRITICAL]) |
2 / 2 |
| P15-B5 | #1367 → #1381 1b71b4e · #1293 → #1382 300359e (sdk-regenerate rollup green) · #1295 → #1383 ce3eaa6 · #1287 closed on this gate |
34553902991 green ce3eaa6; UI e2e-smoke 34553905903 green b0052fc |
#1384 (milestone) | 1 / 3 |
| P15-B6 | #1384 → #1385 000fc03 · #1296 → #1386 9358b9a · #1294 → #1387 cf5535f · [CRITICAL] fix #1388 → #1389 5f8d163 (migration 0093) |
34562274905 green cf5535f, 34567231555 green 5f8d163; sdk-smoke 34562277270 red at cf5535f → 34566448950 green at 5f8d163 (criterion 1) |
#1390 (milestone) | 2 / 4 |
| P15-U0 | ui#358 → #426 f965e30 (closed) · ui#350 → #440 f5603fb (closed on red 34543602487 + green 34546060738) · ui#396 → #425 b8ed987 (Refs; its red half proven by throwaway tag v0.14.2-rc.396, run 34537625753 verify-ci=failure · publish=skipped, tag deleted) · ui#397 → #438 a41a4ee (Refs; premise corrected — a stable tag already moved :latest, the drift was the manual main dispatch) · ui#421 → #439 8c3aad4 (Refs; items 1–5, 7, 8 done — the stable-canvas maplibre mock is on trunk, so the W2 precondition is met) · in-run chore/test PRs #441 #442 #443 #444 |
UI ci.yml 34546056085 + e2e-smoke.yml 34546060738 green b0052fc (39 pass, 2 flaky-on-retry, 4 skipped) |
none — R-0's UI half done (blocks on ui#350 #396 #397 #421) | 0 / 5 |
| P15-R1 | #1329 → #1392 041eb8d · #1358 → #1393 90c20e7 · #1303 → #1394 78633a6 (refuted its own hypothesis: no test interaction — _enqueue_catalog_regeneration blocks ~110 s per test against an unreachable broker) · #1390 → #1395 254bc94 · #1334 → #1396 3503405 (items 1–2; item 2's naive fix was a security regression caught at premise check) · #1360 → #1397 4b57307 (/build self-merged despite --review; verified post-hoc, accepted). All six closed; #1358 / #1360 bodies had their rider box ticked by the orchestrator on gate proof (the one body edit of the wave). Two builders killed by transport errors and resumed via SendMessage, no work lost |
34689176625 green 4b57307 |
#1398 (off-milestone, UNSIZED, four findings in the body + two in a comment) | 1 / 6 = 0.17 — converging |
| P15-U1 | ui#378 → #445 e7fa4a3 (closed) · ui#379 → #446 17a0beb (Refs; step-9 redirect assertion in events.spec.ts never smoke-executed) · ui#424 → #447 8cd5539 (Refs; the geocode 503 contract assertion never smoke-executed) · ui#380 → #448 1da8a94 (closed) · in-wave fix #451 b7df549 (featuresStatus tri-state: an unread /v1/info is not "flags off") |
UI ci.yml 34683622637 1da8a94, 34684673205 b7df549; e2e-smoke.yml 34683624847 red at compose pull (ui#449) |
ui#449 [CRITICAL], ui#450 (both off-milestone, sized at filing, not by /analyze) |
2 / 4 = 0.50 — tripped |
| P15-U2 | ui#381 → #456 18d0730 (Refs) + #459 8402499 · ui#382 → #457 a1b98b8 (closed) · ui#383 → #458 a3a0ce1 (closed; #459 finishes the newest-run-wins re-list) · all three closed |
UI e2e-smoke.yml 34801583007 a3a0ce1, 34803204203 green 8402499 |
none | 0 / 3 |
| P15-R2 | #1267 → #1415 bbf816e (Refs, one box open) · #1315 → #1416 e4cfadb (Refs, closes on the release deploy) · #1405 → #1417 5a280e1 · #1314 → #1418 a2ba3fa (docs.yml 34948868047 OK https://climate-lama.pages.dev (200), so decision 8's red half was never observable) · #1365 → #1419 f19065d (migration 0094, migrations-gate green) · #1398 → #1420 d2d04de · #1357 → #1422 4b0457b · 5 closed, 2 open |
34957151886 green 4b0457b; UI e2e-smoke 34957218866 green against backbone 4b0457b |
#1421 (off-milestone, [ENHANCE-NEXT]) |
1 / 7 = 0.14 |
| P15-U3 | ui#384 → #460 6564e5d (Refs, closed on proof); premise corrected: series_dir is format-checked only, so a missing directory fails at the first poll |
UI e2e-smoke.yml 34948576274 green 6564e5d (42 pass · 4 skip, none in live.spec/events.spec); ci.yml 34948572716 green on attempt 2 (RiskOverTimeChart observer flake, not reproduced) |
none (N = 1 note → #1297) | 0 / 1 |
| P15-R (backbone) | bump #1414 a8b850d · gate fix #1423 7ad780c (SDK smoke pinned the pre-#1422 E_INTERNAL) · tag v0.25.0 → release.yml 34966002412 · deploy 34966801293 (backbone leg ok; run red at the UI identity step) |
ci.yml 34965255088 + sdk-smoke.yml 34965258562 green 7ad780c (red sdk-smoke 34963911484 at a8b850d) |
ui#461 (UI stale-tag guard) | 1 / 1 |
Owner decisions pending (each blocks one row below; a recommendation is recorded, not a decision):
- The wave breaker has tripped in B4, B5, B6 and U1 (2 / 2, 1 / 3, 2 / 4, 2 / 4); R1 converged
(1 / 6). Phase-wide on 2026-09-13,
scripts/rework_rate.py --since 2026-09-10 --follow-upsreads 33 builds, 8fix:PRs (0.24 / build) and 12 follow-up issues (0.36 / build; 0.30 once #1374 and #1378, Phase 16 planning issues theRefsheuristic mis-attributes, are excluded). Still over the 25 % line of guardrail 20, trending down. Recommendation: proceed — every follow-up sits on the milestone or thefound-in:phase-15label and the lanes below consume them; the mid-review at P15-O2 quotes both tables (rework_rate.pyandusage_scan.py). The U1 orchestrator asked "mid-review now vs continue"; the answer recorded here is continue. - #1288's
cursor["seen"]deviation from the pinned contract (P15-B3): the split card pinned "this window's keys"; the build shipped "keys actually released, aged out at 48 h on the acquisition clock" because its refuter proved the pinned form re-releases already-ingested detections and loses out-of-AOI ones permanently. Ratify or revert. Recommendation: ratify and let #1298 re-pin the contract text. - ui#421 item 6 —
GET /v1/hazardshas nosearch/limit: a backbone S issue now, or Phase - Recommendation: Phase 16, beside #1369 (the catalog country filter), then close ui#421 on the pointer.
- ui#377's remainder (items 2
JobListItem.portfolio_id, 3 per-point weight, 5formatNumber; items 1 and 4 resolved): Recommendation: item 2 → a backbone S issue on Phase 16 (near #1371's job-payload work), items 3 and 5 → one Phase 16 UI issue; close ui#377 once the pointers exist. - #1365 finding 1 — the event lane books zero usage because
usage_eventscarriesCHECK (job_type IN ('impact','cost_benefit'))(migration 0038) while the lane writesevent_impact; fixing it is migration 0094. Land it in P15-R2 before the bump (it then rides the release under D13), or defer to Phase 16. Its## Executionblock carries twoParallel:lines and must be repaired in P15-R-0′ either way. - #1267's three questions (pin vs strict step in
docs.yml; the lint scope forscripts/; where #226's perf guard runs) — answered in a comment before P15-R2, or the builder stalls. - Exit criterion 1's letter says "three replayed snapshots"; D10, W1.8 and the ready-line contract already say N ≥ 1, and the vendored series is one acquisition day. Recommendation: amend the criterion to N ≥ 1 in #1298 and keep #1297 (a second real series) as the road to three.
- #1314 step 1 is a dashboard action only the owner can take — set the
climate-lamaCloudflare Pages project's production branch tomain(npx wrangler pages deployment listmust then show the newest deployment as Production). The repo half (a post-deploycurlofsite_urlindocs.yml) builds in P15-R2 either way; without step 1 its check stays red. - The smoke stack's registry access (ui#449): if pinning an anonymously pullable
minio/mctag (or replacingmcwith theminioimage's own client) does not restore the pull, the fix needs a Docker Hub read token as a repository secret — an owner step, not a builder's. Recommendation: try the pin first; theghcr.iounauthorizedlines are HEAD requests on a:latestmanifest and were warnings, not the failure.
Outcomes 2026-09-14 (owner instruction: "go with the recommended option after a fast /reassess";
3 agents — two refuters, one completeness critic — corrected five designs before they were applied):
3 ui#421 item 6 → backbone #1406 (?search= only — limit/offset already existed, ?country=
landed in PR #1403) + UI #454; ui#421 closed, its two surviving alertdialog sites pointed at ui#429.
4 ui#377 → #1407 (JobListItem.portfolio_id, Phase 16), #1408 (the mirror-attachment weight, no
milestone), ui#455 (formatNumber cache, Phase 16); ui#377 closed. 5 #1365 finding 1 builds in
P15-R2: user-initiated event computes meter like impact (D5/D15 and the ADR scope "unmetered" to
feed-driven work only); the migration takes the next free number at branch time (Phase 16 is
landing migrations too — never pre-assign 0094), the model's copy of the CHECK moves in the same PR,
record_sync catches IntegrityError specifically. 6 #1267: docs gate via the existing docs
extra (--extra docs on ci.yml's uv export + one mkdocs build --strict step; docs.yml installs
'.[docs]') — one version source, no == lists; scripts/ joins both ruff gates; the bbox guard
keeps its 1M fixture and slow marker, asserts the GiST index in EXPLAIN (FORMAT JSON), prints
the timing, and the slow job's path widens to collect it. 9 resolved without a credential: PR
453 pins quay.io/minio/* by digest. Still open: 1 (recorded "continue"; re-measure at O2),¶
2 (#1288's cursor deviation — ratify in #1298), 7 (criterion 1's N — #1298), 8 (the Cloudflare production-branch dashboard step — owner; #1314's new check warns on cron and fails only on a manual dispatch until it is done).
Decisions opened 2026-09-15. 10: #1267's last box (a CI run of mkdocs build --strict going red on a dead anchor). Either close it on the builder's local proof at the uv.lock versions (R2's recommendation, 0 billed min), or push a temp branch with a dead anchor and dispatch ci.yml (~13 billed min). 11: the bad UI tag. Either delete v0.15.0 and its GHCR version and re-cut v0.15.0 (recommended), or leave it and cut v0.15.1. Rider 22: every tag, in both repos, names the SHA read from git rev-parse origin/main right after a fetch. A green verify-ci proves only that the tagged commit is green, not that it is the release. ui#461 makes that check mechanical for the UI.
Session table — every row is one orchestrator session or one owner step¶
Conventions: sessions are named lama-phase-build-P15-<row> (UI: lama-ui-phase-build-P15-<row>);
inside a cell → is serial (a Files hotspot or a Deps edge) and ∥ concurrent; every
/phase-build carries --no-pr-ci and an explicit issue list (never the milestone); budgets
assume the measured ≈100 min per serial Opus·xhigh issue (B4 measured 76 and 134 for two Ms —
the figure is a floor); the combined live-builder cap is 3 across every session running at the
same time — the split is stated where two rows overlap. Trackers **#1284 #1297 #1299 #1300
1155** are never passed to /phase-build (ops/spike/model: none). Builders take the model¶
and effort from each issue's ## Execution block; orchestrators are Opus·xhigh. Rows are in
dispatch order; the first two can start now and run beside each other (different repos).
| # | Step | Issues, partition, command | Merged before it starts | Who | Passes when |
|---|---|---|---|---|---|
| P15-U1′ | ~~Repair the UI merge gate, then prove the two open U1 boxes~~ — done 2026-09-14 (session lama-ui-phase-build-P15-U1'): ui#449 → PR #453, ui#450 closed; e2e-smoke.yml 34793955800 green at 54f64a4; ui#379 and ui#424 closed on that run by R-0′ |
— | — | orchestrator (UI) | met — the gate runs specs again; 0 U1 issues open |
| P15-R-0′ | ~~Hygiene remainder — make lane B dispatchable~~ — done 2026-09-14 (session lama-plan-phase-15): /analyze 1314 1315 1357 1398 (all tier 2b from the sizer; #1314 and #1315 written as Sonnet · xhigh · 2a-trial because their 2b came from the retired seam floor); parseable blocks appended to #1314 #1315 #1357, #1398 re-scoped to findings 1 + 7 (2–6 → #1410), #1365's double Parallel: repaired, #1405's cross-repo Deps: and sub-floor effort repaired; decisions 5 and 6 posted on #1365 and #1267; ui#377 and ui#421 disposed (see Outcomes) |
— | — | orchestrator | met — every issue in P15-R2's list carries one parseable block; both UI residue issues closed |
| P15-R2 | ~~Backbone residue, lane B + release tooling~~ — done 2026-09-15 (7 merged, see ledger) | — | — | orchestrator | met except #1267's CI-red box (decision 10) and #1315 (→ P15-R′) |
| P15-U2 | ~~Event view, create/close/feed controls, two numbers + delta~~ — done 2026-09-14 (ui#381 ui#382 ui#383 closed) | — | — | orchestrator (UI) | met — exit criterion 5 second half on the smoke stack |
| P15-U3 | ~~live.spec.ts on the smoke stack~~ — done 2026-09-15 (ui#384 closed; live.spec.ts:275 executed green in 34948576274) |
— | — | orchestrator (UI) | met |
| P15-R | ~~Release — backbone half~~ — done 2026-09-15: prod backbone 0.25.0 at 7ad780c, Alembic 0094, live_mode: true. UI half failed closed (stale tag, see Update 2026-09-15) |
— | — | owner + orchestrator | backbone met; UI → P15-R′ |
| P15-R′ | UI re-release, the only thing between prod and a Live surface (session lama-release-P15-R, which holds the context) |
(1) UI bump PR on origin/main 6564e5d (package.json + package-lock.json → the release version; main still says 0.14.1); (2) UI ci.yml + e2e-smoke.yml green at the bump SHA; (3) the bad tag: the owner chooses between deleting v0.15.0 and its GHCR version, then re-cutting v0.15.0 (recommended: nothing consumed it, since the deploy failed closed and a stable tag's :latest move only rewinds to the same v0.14.1 image) or leaving it and cutting v0.15.1; (4) git tag -a vX <bump-sha> naming the SHA from git rev-parse origin/main after a fetch, never local HEAD; (5) publish.yml push run green, whose verify-ci proof closes ui#396 ui#397; (6) deploy-prod.yml with ui_tag only (backbone already live), where the bundle must change and the log proves #1315's pass half, closing #1315; (7) D13 tail: live-worker + beat up, first tick logged before any watch opens, then verify-deploy.sh |
P15-R backbone half | owner + orchestrator | prod serves UI ≥ 0.15.0 with the Live nav; deploy-prod green; ui#396 ui#397 #1315 closed |
| P15-U4 | Stale-tag guard | /build 461 (Sonnet · xhigh · 2a-trial; seam publish.yml, refuter mandatory). Prove-it-fails on a throwaway -rc.guard tag, the ui#396 recipe |
P15-R′ (its publish run closes ui#396/ui#397 on the same file) | orchestrator (UI) | merged; the red run is in the PR body; UI Phase 15 milestone 0 open |
| P15-B8 | Post-release backbone residue (beside Phase 16's lanes, shared cap) | /phase-build 1421 1410 --no-pr-ci --max-runtime 300. #1421 first (Sonnet · xhigh; seam docs.yml; item 1 bounds _schedule_cog_build at aggregate_and_commit.py:1940 the way #1420 bounded the catalog enqueue, and classifies each worker/live_feeds.py apply_async as best-effort or must-fail), then #1410 (Opus · xhigh, structural). #1408 (Phase 14 residue, no milestone) is not in this lane: it is a behaviour change to mirror attachments, better taken with Phase 16's portfolio work. Re-verify every line number at dispatch (bodies grounded at 4b0457b/e8f1be9) |
P15-R (freeze window closed) | orchestrator | 2 merged; gate green by ancestry; neither is an exit criterion, so B8 can trail O1/O2 |
| P15-O1 | #1300 prod ops: three watches (Evia replay at an accelerated cadence, a firms_nrt watch over a Greek book, a usgs_shakemap watch at M ≥ 4.5), 24 h poll health, #1155's manual pan box, RAM |
owner-run tracker. The health read is last_polled_at advancing and last_error null and the feed's cursor and dataset counts consistent with its polls. #1390 is now deployed (0.25.0), so a null last_error is evidence. Watches run through the API, so the backbone half can start before P15-R′; the Moment 4 read waits for the UI |
P15-R backbone half (API side) · P15-R′ (UI read) | owner | exit criterion 3; #1155 closed on the pan check; the D13 RAM assumption replaced by a measurement |
| P15-O2 | #1299 walk + mid-review | the owner walks Moment 4 and Moment 2 steps 8–9 on /live; docs/plan/mid-review-phase-15.md records the verdict, tagged findings, both rework tables (rework_rate.py --follow-ups and usage_scan.py --by-build, same window), the tier-2a Sonnet trial verdict (the trial window closes 2026-09-24: U0's ui#396/#397, R2's #1314/#1315/#1405, B8's #1421 on Sonnet · xhigh) and the stale-tag incident |
P15-R′; P15-O1 running ≥ 24 h | owner + orchestrator | exit criteria 2 and 6; decision 1 re-measured |
| P15-B7 | #1298 closeout docs | /build 1298 (Sonnet · xhigh) once #1299's walk record exists — plan.md banner + status log, showcase Moment 4 → live, CLAUDE.md nav of nine, docs/CLIMATE_LAMA.md; re-pin the #1288 cursor contract (decision 2) and exit criterion 1's N (decision 7); CLIMATE_LAMA_UI.md is ui#384's |
every build issue; #1299's record | orchestrator | Phase 15 CLOSED — both milestones 0 open and closed |
| any time | #1284 flood-footprint spike (read-only research → one comment naming a primary-provider depth product with licence and resolution, or the finding that none exists → close) · #1297 second FIRMS series (owner-run pack CLI; unblocked — #1290 landed, the vendored series is one acquisition day; a volume-budget row docs PR first, then the fetch on the WSL box with FIRMS_MAP_KEY from the host env) |
no build | — | orchestrator / owner | a comment on #1284; a second vendored series turns criterion 1's N = 1 into N ≥ 2 |
Order and calendar. R2, U2, U3 and the backbone half of R are done (2026-09-15). Next R′ (UI re-release) → U4 ∥ B8 ∥ O1 → O2 → B7. R′ is one release session, about an hour plus two UI gates. O1's 24 h window can open on the API now, and decision 8 is moot because #1314's check went green on first dispatch. With Phase 16 sharing the builder cap, allow two days to B7.
Standing riders for every dispatch in this table (in addition to the split's rider on each
issue): (1) explicit issue lists, --no-pr-ci, the row's --max-runtime; (2) the two real PR
gates — migrations-gate.yml (any migrations/** or models/** path — #1365 if it migrates)
and sdk-regenerate.yml (errors.py paths — #1357 if it registers a code) — are read from the
rollup and must be green; the ever-present Cloudflare Pages check is non-gating; (3) the trunk
gate is debounced and its coverage is proven by ancestry (git merge-base --is-ancestor <merge>
<run sha>), never by freshness — a green run at an ancestor SHA covers none of your merges,
and the last-wave override applies once non-coverage is proven; (4) if the builder agent's
isolation: worktree is refused, use the documented fallback (an orchestrator-made worktree + a
general-purpose agent carrying builder.md, pinned to that path); a refused spawn works on an
identical retry; after any fixer check the shared checkout is back on main; before any
worktree sweep, filter by branch name ∈ this run's issue set — two sibling sessions lost
worktrees to another session's sweep in B1/B2 (recover by re-adding at the byte-identical path,
never rebuild); (5) DB tests skip on a box with no PostgreSQL on 5433 — start the WSL PG16, give
each concurrent builder its own TEST_DATABASE_URL (climate_lama_test_2 … _6 exist), and say
"DB tests skipped locally" in any PR body where that was so; per-agent DBs partition Postgres but
not MinIO/Redis, so worker-suite failures under three concurrent suites are contention noise
unless they reproduce on an idle box; never run pytest tests/test_worker/ as a package until
1303 lands; jq is not on PATH — use gh --jq; (6) guardrail 20 — fix: only for rework of¶
Phase 15 code (P15-U0 typed its in-run fixes of pre-Phase-15 code chore/test); the breaker
is re-measured at each session close with scripts/rework_rate.py --since 2026-09-10
--follow-ups; (7) builders never gh issue edit an issue body — corrections are comments; a
partial-scope PR uses Refs, never Resolves, in every branch commit — GitHub composes the
squash message from the branch commits, so one Resolves in a commit auto-closes the issue even
after the PR body is corrected (P15-B5); and a Refs-footered PR never populates
closingIssuesReferences, so a watcher keyed on that field is dead — poll headRefName; an
issue whose state is OPEN with merged code is the normal partial-build case, never proof of an
unmet dep — search merged PRs and grep the symbol on trunk; (8) sibling gates — the UI
e2e-smoke.yml after every UI session, sdk-smoke.yml again only at the release, never per
wave; SDK unit tests run only inside sdk-smoke.yml after the walk step, so a red walk
leaves them unproven; (9) any test that pins a digest over float-derived raster data renders
floats at fixed precision (the recipe in tests/test_core/conftest.py), never repr(), and a
GeoTIFF's bytes are platform-owned — compare content digests; (10) never shorten a verbatim
manifest record to fit a column (ADR-059) — widen the column, as #1388 did; (11) a test that
asserts a fact about the build backlog ("kind X is not registered") is dated the moment the
backlog moves — construct the precondition (monkeypatch.delitem + a positive control),
never observe it (the #1348 × #1351 break); (12) before dispatching a successor, grep the
predecessor's new symbols for their call sites — the B4 composed seam (a refusal called on one
path, a hook firing on all) was caught by one grep; (13) redirect a long suite run to a file and
grep it — never conclude from a tail-truncated failure list; (14) a wsl bash -c wrapper
pre-expands $(…) and $? — stage runner scripts via a file on /mnt/c; (15) never
Set-Location into a builder worktree from the orchestrator — it moves the session's primary
working directory; (16) an issue body's premises rot faster than its line numbers — before
dispatch, re-verify the claim each acceptance box rests on against current trunk, not just the paths
(R1 found one inverted premise whose naive fix was a security regression, and one item already done);
(17) a builder killed by a transport-level API error (No response from API, ENOTFOUND) is
resumed with SendMessage after testing connectivity, and the resume message orders an immediate
WIP commit — it does not consume the one-retry budget; (18) when a deferred finding's files are
owned by a later step of the same chain, fold the fix into that step instead of a fixer PR (R1: zero
extra PRs for three stale comments); (19) /build can self-merge despite --review — verify
post-hoc exactly what the gate would have checked, then accept or revert on that evidence, never
re-land an identical diff for form's sake; (20) a Deps: entry is always a bare same-repo #N or none
— a cross-repo reference resolves to the wrong issue in the parsing repo (#1405 carried
climate-lama-ui#449); cross-repo preconditions go in prose after the block; (21) never pre-assign an
Alembic revision number in an issue while another phase is merging migrations onto the same trunk —
"next free at branch time, down_revision = alembic heads" is the contract.
Exit-criteria map. 1 ← met at N = 1 (P15-B6, sdk-smoke 34566448950 at 5f8d163; the
"three" clause is decision 7) · 2 ← P15-O2 · 3 ← P15-O1 (#1390 is merged; real once deployed) ·
4 ← every session's repo-wide guard tests + the mid-review · 5 ← first half met (P15-U1: nav = 9
with Live gated on live_mode, App.test.tsx proves the 9-vs-8 count) + P15-U2 for the redirect
and the chip · 6 ← P15-B7 + P15-O2.
Mid-review inputs handed over at Phase 14's close (docs/plan/mid-review-phase-14.md):
eight gates reported success without doing their work, so a Phase 15 gate counts only once it
has been shown red by inverting its claim — the prove-it-fails box on every issue, and the
orchestrator records the red run id beside the green (so far: ui#350's 34543602487 / 34546060738,
ui#396's 34537625753, the series leg's 34562277270 / 34566448950, the #1286 guard's 27-blind →
1-caught mutation); the number mid-review-phase-15.md quotes first is follow-ups per build
(Phase 14 close: backbone 0.33, UI 0.44; Phase 15 at 2026-09-13: 0.30 backbone after excluding the
two mis-attributed Phase 16 issues, UI 2 / 9 = 0.22 with ui#449 an environment failure). F-13, the
app-wide sweep for the walk's defect classes, was not run — it stays on the owner's prompt and is
not a Phase 15 row.
Phase state — 2026-09-17 (by /phase-analyze, origin/main 5fb5603)¶
State — P15 · Live Risk · read 2026-09-17 · origin/main 5fb5603
| Wave | Status | Owner | Issues | Deps | Command | Budget | Notes |
|---|---|---|---|---|---|---|---|
| P15-R2 | ✅ done — gate 34957151886 @ 4b0457be |
agent | ✓1267 ✓1314 ✓1315 ✓1357 ✓1365 +2 | — | /phase-build 1267 1314 1315 1357 1365 1405 1398 … |
480/178 | #1267 closed on decision 10, 2026-09-17 |
| P15-U2 | ✅ done — e2e-smoke 34803204203 | agent (UI) | ✓ui381 ✓ui382 ✓ui383 | — | /phase-build 381 382 383 |
420/175 | — |
| P15-U3 | ✅ done — e2e-smoke 34948576274 | agent (UI) | ✓ui384 | — | /phase-build 384 |
180/66 | — |
| P15-R | ✅ done — backbone half | owner + agent | — | — | /release |
— | v0.25.0 @ 7ad780c, Alembic 0094 |
| P15-R′ | ✅ done — deploy-prod 35091278837 | owner + agent | ✓ui396 ✓ui397 ✓1315 | P15-R ✓ | UI re-tag → deploy-prod.yml ui_tag |
— | UI v0.15.1 on prod |
| P15-B8 | ✅ done — gate 35198886403 @ 1efb351 |
agent | ✓1421 ✓1410 | P15-R ✓ | /phase-build 1421 1410 --no-pr-ci --max-runtime 300 |
300/— | PRs #1431 #1433, ancestry proven |
| P15-O2 | ✅ done — walk 2026-09-17; record 5fb5603 |
owner + agent | ✓1299 | P15-R′ ✓ | walk → mid-review-phase-15.md |
— | exit criteria 2 and 7; PR #1445 |
| P15-U4 | 🔵 merged + verified — ui#461 open | agent (UI) | ◐ui461 | P15-R′ ✓ | cd ../climate-lama-ui && /build 461 |
—/— | last box needs the next UI release tag |
| P15-O1 | 🟠 agent — 24 h read due 2026-09-18T19:15Z | agent | ⊘1300 ✓1155 | the walk ✓ | psql -c "select kind,last_polled_at,last_error from live_feeds" |
— | both NRT feeds polling, last_error null |
| P15-B7 | 🟢 ready | agent | ○1298 | #1299 record ✓ | /build 1298 |
60/— | Phase 15 closeout docs; +F-5 per the #1298 comment |
Next — P15
| # | Wave | Command | Session | After |
|---|---|---|---|---|
| 1 | P15-B7 | /build 1298 |
/rename lama-phase-build-P15-B7 |
— |
| 2 | P15-O1 | the live_feeds read, pasted on #1300 |
new session | 2026-09-18T19:15Z |
- drift — prod moved to backbone v0.26.0 (
3eaaf043, deploy 35244603330) at 16:09Z, mid-walk; Alembic head0094at both tags, so no migration rode it and nothing walked was invalidated - drift — #1299's checklist asked for
alembic_version = 0092and a coordinated release; both were satisfied by P15-R / P15-R′ at0094, recorded in its closing comment - decision 10 settled 2026-09-17 — #1267 closed on PR #1415's two inversions; the CI-observability last mile is #1447 (the
slowjob cannot end green whileINGEST_MEMORY_FIXTURE_URLis unset). Exit criterion 1 now waits only on #1298 and ui#461 - unassigned — none on the milestone · superseded — none · any-time — ⊘1284 spike (agent, read-only) · ⊘1297 second FIRMS series (off the milestone, credential)
- walk findings — ui#481 ui#482 #1444 (+#1447 from decision 10) filed
found-in:phase-15, deliberately off the milestone; F-5 folded into #1298, F-4 and F-6 recorded only - breaker — backbone escape 0.64 (48/75) · partial 0.24 · UI escape 0.41 (21/51) · partial 0.65 — over 25 % in both repos; tier-2a Sonnet trial adopted ($13 vs $79 per build)
- release — climate-lama v0.26.0 @
3eaaf043(1 behind, ancestor ✓, pyproject 0.26.0 ✓, Alembic 0094, deploy 35244603330 ✓) · climate-lama-ui v0.15.1 @6ceff68(17 behind, ancestor ✓, package.json 0.15.1 ✓) - holds — none · lanes — 0 live builders / cap 3 · stale-worktree
.claude/worktrees/agent-a5590799dff585a33(foreign, Phase 16 B2 leftover)
Landscape refresh (2026-09-08) — archived to archive/phase-15-log.md (consumed 2026-09-08).
Inputs from Phase 14 and their disposition — archived to archive/phase-15-log.md (consumed 2026-09-08).
Decisions taken autonomously — archived to archive/phase-15-log.md (consumed 2026-09-08).
Reassess record (2026-09-08) — archived to archive/phase-15-log.md (consumed 2026-09-08).
Split readiness (with Contracts pinned and Split cards) — archived to archive/phase-15-log.md (consumed 2026-09-08).
Reassess record — split-readiness pass — archived to archive/phase-15-log.md (consumed 2026-09-08).
Out of scope (deliberate — the charter's ladder and Parked ledger)¶
Spread and forward risk (ELMFIRE, WindNinja, ECMWF, fuel-model mapping — R3); sensors, drones as devices, point-observation tables and interpolation (R4); auto-opened watches from a detection stream; trigger thresholds and any push; uncertainty ranges on event loss; a flood feed (pending W3.2); positioning and customers; everything in the Phase 17 charter stub.
Process amendments recorded here¶
- Milestone
Phase 15 -- Live Riskin both repos, labelmilestone:phase-15;/phase-splitruns once per repo, backbone first; W2.0 is the mandatory first UI issue and every other UI issue is consumer-only. - Release cadence: one release at close (migration 0092 →
pg_dumpfirst), coordinated across both repos; a mid-phasepublish.ymlimage for walking the UI on the fullstack compose. - The weekly metric is unchanged: the series smoke passes, and Moment 4 visibly improves.