Skip to content

You are the build orchestrator for climate-lama. Deliver every agentically-buildable item below to landed-on-trunk, in worktree mode, spawning one agent per issue. Do not ask me for input — everything here is scoped to need none. Stop only on a genuine blocker.

Read docs/plan/triage-2026-08-01-orchestrator.md first. It is the plan; this prompt is the execution order. Where they disagree, re-verify against live gh/git state and trust that.

Execution log — run started 2026-08-01 06:00 local

What the re-derivation below actually found when this prompt was first executed. Kept because it is the clearest evidence for why the re-derivation is mandatory: four of the prompt's own premises were wrong within ten minutes of being written.

Premise as written What was true at execution
#454 and #477 are buildable Both were being built by other live sessions. Two locked worktrees held uncommitted work; file mtimes were 7 and 13 minutes old. Removed from scope.
S2 (#267 item 4) is a build item Already fixed. 1a2b105 (2026-04-26) added DateTime(timezone=True); the writer was never naive. Verified and ticked on #267 — no issue created.
Trunk 1e4d753 is ungated Correct. Dispatched ci.yml; came back green, so the gate was cleared before any build.
Alembic head 0062, next 0063 Correct — no migrations in any open PR or live worktree. Slot free.

Split issues created: S1 → #485, S3 → #486, S4 → #487. S2 was not created.

Net scope: 11 items, not 14 — #454, #477 and S2 all came out.

Outcome

All 11 dispatched. Ten merged; #481 correctly stopped rather than fabricating coefficients, and its documentation half landed as #509.

Wave Issues Result
1 #103, #276, #483, #486 all merged
2 #279, #487, #356 all merged
3 #485, #482, #480, #481 first three merged; #481 stopped by design

Two red gates, both raw SQL in a migration — and this is the run's main lesson:

  • 0063 (#485): doubled percent signs in a RAISE format string, which plpgsql reads as an escaped literal → zero specifiers against two arguments. Fixed in #502.
  • 0065 (#480): - and jsonb_typeof() used against a json column. Fixed in #507, and the underlying model-vs-schema drift filed as #508.

0064 (#482), which used typed op.add_column, passed first time. Nothing on this machine runs PostgreSQL, so raw SQL in a migration is the only artifact that reaches trunk entirely unverified — lint, type check and the memory gate all pass it. Gating after every migration merge is what kept each break to a one-commit revert instead of a batch.

Follow-ups filed: #497 (prod-host deploy secrets — #356 is inert without them), #508 (json/jsonb drift), climate-lama-ui#96 (drop removed settings controls).

Two agents materially corrected the issues this plan was built on: #480 found the data_source_allowlist "delegate" option would have been a privilege escalation (the column is platform-admin-only, the settings endpoint is org-admin), and #481 found that no FWI-calibrated curve can exist in principle — the defect is the ingestor's declared unit, not the seeded curve.

Ground truth to re-derive before you start (do not trust these numbers)

They were true at 2026-08-01 and move whenever another session merges:

  • Trunk was 1e4d753, Alembic head 0062_ingest_jobs_bbox, so the next revision was 0063.
  • Trunk was ungated — no ci.yml run for that SHA.
  • 453, #455, #475 are closed. #455 was already split into #477 (build) and #478 (deferred).

git worktree list is not a formality — it is the check that caught two live sessions. A worktree that is locked with recent file mtimes and no commits is an active build, not an abandoned one. Check mtimes against the clock before assuming anything is stale, and never delete or resume another session's worktree.

Run this before anything else:

git fetch origin main && git log --oneline -1 origin/main
git worktree list
gh pr list --state open --json number,headRefName,files \
  --jq '.[] | select([.files[].path] | any(startswith("migrations/")))'
gh issue list --state open --limit 100 --json number,title,labels
gh workflow run ci.yml --ref main        # gate trunk BEFORE building; require green

If trunk is red, fix or revert that first — do not build on a red trunk, or the first gate failure will implicate your work for a break that predates it.

Five hard constraints (verified — violating any of these silently corrupts the run)

  1. One migration in flight globally. tests/conftest.py:103 calls command.upgrade(cfg, "head") — singular. Two branches each adding a migration both set the same down_revision, producing Multiple head revisions are present, which fails every DB-backed test in every concurrent branch, not just the two that collided. The queue is global, not per-lane. Re-derive the next revision number across trunk plus open PRs plus live worktrees, immediately before creating the file.
  2. PYTHONPATH in worktrees. The editable install (__editable__.climate_lama-*.pth) is a plain path append resolving to the main checkout's src/. A worktree build that runs pytest without PYTHONPATH=<worktree>/src validates trunk's code, not the branch's — and reports green either way. This is the most dangerous silent failure in worktree mode.
  3. Pass --fast to every /build. This machine has no Docker/PostGIS. Without --fast, /build runs pytest, hits ConnectionRefusedError from the DB fixtures, burns its 3-attempt fix cap on a failure unrelated to the code, and returns failed. Tests are still written and committed — only local execution is skipped.
  4. NO-PR-CI. PRs carry no status checks by design; merge on mergeable, never wait for checks. Exception: sdk-regenerate.yml does fire automatically on PRs touching core/errors.py or the SDK error files. If it flags drift: run uv run python sdk/python/scripts/regenerate_errors.py, then ruff format that file, then commit.
  5. Other sessions share this repo. Re-check origin/main and git worktree list at each wave boundary and before each push.

Local signal that does exist: pytest tests/ -m "not slow" via .venv/Scripts/python.exe, ~10 min, stable baseline ~1264 passed / ~38 skipped / ~627 errors (all DB connection refusals). Compare failure sets, not counts. A skip is not a pass.

Wave 0 — create four split issues, then validate the plan with --rr

Four buildable slices live inside multi-item umbrella issues; /build cannot target "item 5 of #291". Create each with an ## Execution block (Deps: / Parallel: / Files: / Size:):

ID From Title Migration
S1 #291 item 5 feat(db): enforce the impact_functions.version bump invariant with a trigger Yes
S2 #267 item 4 fix(api): offset-naive datetime writes break user_preferences/org_settings Maybe
S3 #267 item 2 chore: make seed-demo resolve the interpreter via uv No
S4 #287 item 3 ci(release): sign release images with cosign keyless OIDC No

S4 is item 3 only — leave item 4 (SBOM) alone; its compliance trigger has not fired. S4 must also answer: cosign signing has no consumer unless something verifies. Either add cosign verify to #356's deploy path or document it as a manual step in docs/deployment/upgrading.md, and say which.

Then: /analyze-chain 103 276 279 356 454 477 480 481 482 483 <S1> <S2> <S3> <S4>

Then run --rr over your wave schedule before dispatching a single build. Re-verify the file-overlap map and the migration queue against live state, and correct the waves below if they have drifted. The waves are a starting point, not a contract.

Waves

Each agent: /build <N> --worktree --fast. Gate trunk at each wave boundary.

Wave 1 — 5 parallel, disjoint files, no migrations

Issue Model · effort Why this tier
#103 docs runbook Sonnet 5 · high Bounded synthesis from committed compose/config
#276 SDK async client Opus 4.8 · high Two real design calls: .aio namespace vs unified, and shared base vs duplication
#483 upload creates no ingest_jobs row Opus 4.8 · high Must substitute a failing unit test for the live-stack repro
S3 (#486) Makefile uv run python Sonnet 5 · high One line

~~#477 staged-source grid metadata~~ — removed at execution: owned by a live session.

#483 carries a coupling the issue does not mention. The in-flight #454 work adds JobRepository.get_id_by_ingest_job_id, which resolves the legacy Job via params['ingest_job_id'] — a back-reference its docstring says every producer of an ingest job stamps. #483 makes upload_hazard a third producer, so it must stamp that key or

454's lookup silently returns None for every upload-initiated ingest. Hand this to the

483 agent explicitly; it cannot infer it from the issue body.

#103's failure mode: the agent cannot reproduce any failure it documents. Every command must be derived from committed docker-compose.yml, docker-compose.prod.yml, migrations/alembic.ini and the Celery config, with the source file cited inline. Invented commands are worse than no runbook — they get trusted during an incident.

#483: if the failing test you write does not fail, stop and report. Do not fix a bug you could not first make fail.

Wave 2 — 4 parallel (each depends on a Wave-1 sibling or is independent)

Issue Model · effort Depends on
#279 SDK integration tests Opus 4.8 · xhigh #276 (same test tree)
S4 (#487) cosign signing Opus 4.8 · high
#356 prod deploy workflow Opus 4.8 · xhigh

~~#454 plan_chunks rolling-deploy safety~~ — removed at execution: owned by a live session. Its xhigh rationale still stands if it ever comes back: the defect is semantic, not arity — bbox=None changed meaning from "irrelevant" to "plan against the raw file", and reasoning about Celery chain serialisation across a rolling deploy is the hard part, not the patch.

S4's gating condition was checked and has fired: #287 times item 3 as "after v0.1.0 ships"; the repo is at v0.4.0. docs/ops/release-pipeline-followup.md §"Item 3" (~line 164) is a complete cold-start runbook including the exact cosign verify invocation.

#279 must be rescoped before it is built — its six sections are a wishlist, not acceptance criteria; built literally it is unbounded. Cut it to what the configured SDK_SMOKE_* secrets can exercise in sdk-smoke.yml, and fold in the three assertions sitting as a manual checklist in #278 (login payload depth, result_id at the top level of /v1/jobs/{id}, ead/aai on /v1/results/{id}). Run --rr on the rescope before building it.

#356 has the highest blast radius in the set — it writes .env.prod on a live host and runs migrations. Require: dispatch-only trigger, a concurrency: group, GHCR tag-existence preflight before any .env.prod edit, health gate after. Do not let it merge with a push/release trigger however convenient. It cannot be run — no SSH/host secrets exist at repo level or in the climate-lama (Production) environment. Deliver the workflow plus a named list of secrets the owner must add.

Wave 3 — STRICTLY SERIAL. One at a time. Gate after EVERY merge.

The global migration queue. Everything here lands lint-verified only; the trunk gate is the first real test. Re-derive the revision number before each one.

Order Issue Model · effort Note
1 S1 (#485) version-bump trigger Sonnet 5 · xhigh Mechanical migration + docs
2 #482 persist ingest resolution Opus 4.8 · high Mirrors #453 (1e4d753) exactly — copy that pattern
3 #480 org-settings dead keys Opus 4.8 · high Per-key wire-or-remove calls; resolve the data_source_allowlist dual-home
4 #481 wildfire FWI curve Opus 4.8 · xhigh Needs a cited FWI curve

~~5. S2 preferences tz bug~~ — removed at execution: already fixed, no issue created. The prompt said it "may correctly end in a close"; it did, and direct verification was cheaper than a build cycle. Evidence: the writer (db/repositories/preferences_repository.py:40,68) has used datetime.now(UTC) since its original commit 4251755, utcnow() appears nowhere in src/, and the column — the actually-naive half — was made DateTime(timezone=True) by 1a2b105 (fix(api): fix three CI failures in preferences/rbac tests, 2026-04-26), 135 commits back on trunk. Recorded on #267 and ticked.

#482 must not start before #454 lands. #454's in-flight diff edits db/repositories/job_repository.py, and #482 touches the same ingest-job persistence area. Re-check git worktree list and gh pr list at the Wave-3 boundary, not just at the start.

Gate after every merge here, not every three — a broken migration fails every test in every lane, so ~1 billed minute per merge is cheap insurance.

#481 must not invent coefficients. If no published FWI-calibrated curve is usable, it reports that and stops. Check first whether core/impact_function_seeder.py or migrations/versions/0005_seed_wf_impact_functions.py is the authoritative seeding path — if the seeder is live and 0005 is legacy, the change needs no migration and leaves this wave.

S2 may correctly end in a close, not a fix. #267 diagnosed offset-naive datetimes hitting asyncpg, but the model already declares DateTime(timezone=True) (models/preferences.py:26,40), so the defect is on the write path. It was last observed in April 2026, ~25 migrations ago. Reproduce first; if it no longer reproduces in CI, close it. Per constraint 2 this class of bug cannot reproduce on this machine, so the CI gate is the only evidence — do not guess.

Gate protocol

Before wave 1, after every Wave-3 merge, at each wave boundary, and once at the end:

git fetch origin main && HEAD=$(git rev-parse origin/main)
gh run list --workflow ci.yml --branch main -L 12 \
  --json databaseId,headSha,status,conclusion \
  --jq "[.[] | select(.headSha==\"$HEAD\")] | .[0]"    # dedupe — duplicates bill in full
gh workflow run ci.yml --ref main                       # only if empty or failed
gh workflow run sdk-smoke.yml --ref main                # after Wave 2 (SDK) and Wave 3 (migrations)

scripts/verify_ci_green.sh --sha <sha> is the same check locally at zero minutes.

On a red gate: freeze every lane before reverting. With parallel waves, continuing to merge onto a red trunk grows the implicated set faster than bisecting shrinks it. Freeze, git revert newest-first (squash merges revert one-to-one with PRs), re-gate, resume.

Do not build these

Item Why
#478 ranged/COG reads Deferred by its own body — gated on ADR-045 (#446) being implemented; needs real MinIO + a continent-scale raster
#447 pan-EU expansion Epic tracker; its own Execution block says "split before building"
#6 license choice A decision, not code
#278, #285, #287 items 2/4/5, #291 items 2/3/4, #267 items 1/3/5 External service clicks, parked phases, real prod UUIDs, or multi-week observation
#366, #367, #412, #273 Blocked epics, owner-gated data access, or out-of-MVP cross-repo work

Report

Per wave: issues attempted, PRs merged, gate SHA + result. At the end: what landed, what stopped and why, and the exact command to resume anything incomplete. Report trunks verified only by a green gate as verified; everything else is UNVERIFIED — do not call a lint-only merge "done".