Skip to content

Orchestrator plan — triage of open issues #103–#412

Snapshot: 2026-08-01, verified against trunk 675a159. Scope: the 17 issues open in the range #103–#412, plus #480/#481 created during triage. Four were closed (#269, #270, #275, #360).

This document is the input an orchestrator picks up. It assumes worktree mode (/build --worktree, or /phase-build, which uses worktrees per issue).

Preconditions — do these before dispatching anything

  1. Gate trunk first. As of this writing 675a159 has no ci.yml run. If the run starts on an ungated trunk, the first red gate implicates the new work when the break may predate it. Dispatch gh workflow run ci.yml --ref main and require green before lane 1. (Actions is healthy — 81089a8 was dispatched and passed 2026-08-01 01:44Z.)
  2. Re-check origin/main AND what is in flight. Other sessions merge into this repo concurrently, and origin/main does not show their unmerged work. Before each lane and before each push, run all three:
git fetch origin main && git log --oneline -1 origin/main
git worktree list                                    # other sessions' live worktrees
gh pr list --state open --json number,headRefName,files \
  --jq '.[] | select([.files[].path] | any(startswith("migrations/")))'
  1. The migration slot is free — but the head moved. PR #484 merged as 1e4d753, so 0062_ingest_jobs_bbox.py is now on trunk and the head is 0062, not 0061. Next revision is 0063. This changed mid-triage, which is the point: never infer the next revision from a number written in this document. Re-derive it from trunk plus every open PR and live worktree, immediately before creating the file.
  2. The working tree carries an unrelated uv.lock modification. Sync before branching and don't let a worktree build absorb it into a PR.
  3. Stale worktrees may exist (e.g. agent-a1e3b1f0eb71908bd, detached, unlocked). Confirm abandoned before pruning — a locked worktree belongs to a live session.

Repo facts that shape the plan

Constraints, not preferences. Each is verified; violating any breaks the run.

  1. Single Alembic head — and the head is not what trunk says. Trunk's head is 0061_grid_membership_assignment (chain confirmed: 0059 ← 0060 ← 0061, no fork on trunk). But 0062 is already taken by open PR #484, so the effective next revision is 0063. Two branches that each add a migration both set the same down_revision; tests/conftest.py:103 calls command.upgrade(cfg, "head")singular — which then raises Multiple head revisions are present. That fails every DB-backed test in every concurrent branch, not just the two that collided.

Never read the next revision number off trunk. Compute it across trunk plus every open PR and live worktree (precondition 2), and re-check immediately before creating the file — the answer moves whenever another session merges. Migration-bearing work is strictly serial across sessions, not just within this plan. 2. --fast is mandatory on this machine. /build without it runs pytest, hits ConnectionRefusedError from the DB fixtures, burns its 3-attempt fix cap on a failure unrelated to the code, and returns failed. Dispatch every build with --fast — but the tests still get written and committed; only local execution is skipped. 3. Worktrees silently test the wrong tree. The editable install (__editable__.climate_lama-*.pth) is a plain path append that resolves to the main checkout's src/. Any worktree build that runs pytest must set PYTHONPATH=<worktree>/src or it validates trunk's code, not the branch's — and reports green either way. This is the single most dangerous silent failure in worktree mode. 4. Local signal exists but is partial. pytest tests/ -m "not slow" runs in ~10 min with a stable baseline of ~1264 passed / ~38 skipped / ~627 errors (the errors are all DB connection refusals). Compare failure sets before/after, not counts. Use .venv/Scripts/python.exe. Anything whose behaviour depends on migrations having run cannot reproduce locally at all and will look green here while failing in CI. 5. "PRs have no status checks" is not quite true. sdk-regenerate.yml fires automatically on any PR touching src/climate_lama/core/errors.py, sdk/python/scripts/regenerate_errors.py, or sdk/python/climate_lama_client/errors.py. If it flags drift: run uv run python sdk/python/scripts/regenerate_errors.py, then ruff format that file (the generator emits single blank lines; the committed file is ruff-formatted, so an unformatted diff shows dozens of phantom deletions), then commit. Cloudflare Pages also reports on PRs. Everything else is on-demand. 6. Trunk gate is the real verification. gh workflow run ci.yml --ref main. Dedupe by SHA first — dispatches on a branch ref do not cancel each other and duplicates bill in full. scripts/verify_ci_green.sh --sha <sha> runs the same check locally at zero minutes.

Wave 0 — create the split issues (prerequisite, no code)

Four agentic slices sit inside multi-item umbrella issues. /build cannot target "item 5 of

291", so each needs its own build-ready issue. Create these; leave the umbrellas open as

trackers.

New Carved 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 Possibly
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

Each body needs an ## Execution block in the house format:

## Execution
Deps: <issue numbers or none>
Parallel: <yes|no>
Files: <comma-separated paths>
Size: <S|M|L>

S4 is item 3 only — drop item 4 (SBOM) from this wave. #287 gates item 3 on "after v0.1.0 ships" (fired — v0.4.0 shipped 2026-06-12) and item 4 on "compliance wave / design-partner requirement" (has not fired). Building an SBOM pipeline for a requirement nobody has raised is the plumbing-with-no-consumer pattern worth avoiding.

Open question S4 should answer before it merges: cosign signing has no consumer unless something verifies. Signed images that are never verified are ceremony. Either add cosign verify to the deploy path (#356's workflow is the natural home) or document it as an explicit manual step in docs/deployment/upgrading.md — and say which in the issue.

Then size the set once — do not size issues individually:

/analyze-chain 103 276 480 481 <S1> <S2> <S3> <S4> 279 356

Lane structure

Four lanes run concurrently; ordering within a lane is strict. Peak concurrency is 6 worktrees (A:2 + B:1 + M:1 + C:2). Cap lower if the machine struggles — Lane M is the one that must never be widened.

Lane A  (docs + CI, no code deps)        #103 ──────────────┐
                                          S4  ──────────────┤
Lane B  (SDK, serial: shared test tree)  #276 ──▶ #279 ─────┤
                                                            ├──▶ final trunk gate
Lane M  (STRICTLY SERIAL — one Alembic    S1 ──▶ S2 ──▶ #480 │    + sdk-smoke dispatch
         head; gate after EVERY merge)           └──▶ #481 ──┤
Lane C  (standalone)                      S3  ──────────────┤
                                         #356 ──────────────┘

Lane A — docs and release CI (parallel, low risk)

Issue Scope Verification available
#103 troubleshooting runbook docs/, mkdocs.yml nav, README.md Doc exists, covers all five services, mkdocs builds
S4 cosign signing .github/workflows/release.yml, docs/deployment/upgrading.md Workflow lints; real proof deferred to the next release tag

Conflict watch: #103 adds an mkdocs.yml nav entry; S4 edits an already-nav'd page and must not touch nav. If S4 proposes a nav change, serialise it after #103.

#103's failure mode is the one to guard. The agent cannot reproduce any failure it is documenting (no Docker). 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. A runbook of plausible-looking invented commands is worse than none, because it will be trusted during an incident.

Lane B — SDK (serial)

Order Issue Why serial
1 #276 async client Owns sdk/python/climate_lama_client/, adds to sdk/python/tests/
2 #279 integration tests Same test tree; #279's body says to mirror the async suite once #276 lands

#276 is the strongest agentic candidate in the entire set, and this is measured, not assumed: pytest sdk/python/tests runs 26 passed, 1 skipped in 0.52s locally, no Docker. It is the one issue here fully verifiable before merge. Run the suite in the worktree with PYTHONPATH set per fact 3.

#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 currently 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}. Those belong in a test, not a checklist a human re-runs by hand. Verification is deferred to an sdk-smoke.yml dispatch.

Lane M — migrations and preferences (STRICTLY SERIAL)

Exists because of facts 1 and 4. Everything here lands lint-verified only; the trunk gate is where it is first genuinely tested.

Unblocked — #484 landed, so the slot is free and the next revision is 0063 (re-derive before use). Note #482 also needs a migration and therefore joins this chain even though it is functionally worker work — the migration constraint cuts across lanes, so there is exactly one global migration queue, not one per lane.

Order Issue Migration Shared files
1 S1 version-bump trigger Certain migrations/versions/0063_* (re-derive)
2 S2 preferences tz bug Possibly models/preferences.py, writers
3 #480 org-settings dead keys Likely (drops a JSONB key) schemas/preferences.py, models/organization.py
4 #481 wildfire FWI curve Likely core/ingest/wildfire.py, core/impact_function_seeder.py

S2 → #480 are adjacent deliberately: both touch the preferences model/schema pair.

#481 stays in this lane on evidence, not caution. The wildfire curve is seeded by migrations/versions/0005_seed_wf_impact_functions.py, and a landed migration is never edited — re-basing means adding one. The one thing that could move it out: if src/climate_lama/core/impact_function_seeder.py is the live seeding path and 0005 is legacy, the change belongs there and needs no migration. **Resolve that at the top of

481's build**; if it comes back "no migration", pull it into Lane C and run it parallel.

Gate cadence here is stricter than elsewhere: dispatch the trunk gate after every Lane-M merge, not every three. A broken migration fails every test in every lane, so ~1 billed minute per merge is cheap. On red, git revert newest-first — squash merges revert one-to-one with PRs.

S2 may be unfixable as specified, and that is an acceptable outcome. #267 diagnosed it as 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, not the column. It was last observed in April 2026, ~25 migrations ago. Reproduce first; if it no longer reproduces in CI, close S2 rather than "fixing" a symptom that is gone. This matters more than usual because per fact 4 no local run can tell the agent either way — S2 is exactly the class of bug that cannot reproduce on this machine.

Lane C — standalone

Issue Notes
S3 make seed-demo via uv run python One Makefile line. Unverifiable on Windows; correctness by inspection against how the rest of the project shells out
#356 prod deploy workflow Agent authors it; it cannot be run. No SSH/host secrets exist at repo level or in the climate-lama (Production) environment — only CLOUDFLARE_* and SDK_SMOKE_*. Deliver the workflow plus a named list of secrets an owner must add

#356 has the highest blast radius in the set — it writes .env.prod on the 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 — auto-deploy on tag is a different, much riskier product than what #356 asks for.

Lane W — worker / ingest (added 2026-08-01, outside the original #103–#412 triage)

These were opened after the triage range and are agentically buildable. All three sit on the ingest plan/dispatch path, so they overlap on files and must be ordered.

Order Issue Migration Files
1 #477 persist validate_source grid metadata on the staged-source descriptor No — deliberately uses the existing staged_source JSON column worker/ingest/plan_chunks.py, worker/ingest/validate_source.py
2 #454 plan_chunks signature unsafe across a rolling deploy No worker/ingest/plan_chunks.py, worker/ingest/pipeline.py
#483 POST /v1/hazards/upload never creates an ingest_jobs row No api/v1/hazards.pyparallel, no overlap
→ M #482 persist ingest resolution on the job row Yes joins the global migration queue in Lane M

477 before #454: both edit plan_chunks.py, and #477 is the "buildable now" half of the

closed #455 (its sibling #478 is deferred — see exclusions). #454's own proposed fix is "resolve bbox and job id from the ingest_jobs row inside the task (see #453)" — #453 landed as 1e4d753, so that pattern now exists to copy.

#483's first acceptance check says "reproduce against a live stack", which no agent here can do. Substitute a failing API test at the unit level — the code evidence is conclusive (upload_hazard's params literal has no ingest_job_id; ingest_hazard raises ValidationError when it is absent). If the test passes — i.e. the bug does not reproduce — stop and report. Do not "fix" a bug you could not first make fail.

Excluded from the agentic set (added 2026-08-01)

Item Why
#478 ranged/COG windowed reads Deferred by its own body: gated on ADR-045 (#446) being implemented, and its acceptance evidence needs a real MinIO and a continent-scale raster
#447 pan-EU coverage expansion Epic tracker; Deps: #446 + Phase 9 streams A+B, and its own Execution block says "split before building"
#6 license choice A decision, not code

Trunk-gate protocol

Run before the first lane, after every Lane-M merge, at each lane completion, 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: skip if queued/running/green
gh workflow run ci.yml --ref main                         # only if empty or failed
gh workflow run sdk-smoke.yml --ref main                  # after Lane B, and after Lane M

On a red gate: pause every lane before reverting. With four lanes merging concurrently, continuing to merge onto a red trunk grows the implicated set faster than bisecting shrinks it, and "revert newest-first" becomes ambiguous. Freeze, revert, re-gate, resume.

sdk-smoke.yml is required after Lane B (SDK changed) and after Lane M (migrations changed).

Not in the orchestrator's scope — human worklist

Item What blocks it
#278 — PyPI trusted publishers ×2 Clicks on test.pypi.org / pypi.org; nothing in-repo can perform or confirm them
#278 — pypi-prod required reviewers Environment exists; protection is branch_policy only, so a sdk-v* tag would publish with no approval. One setting, ~2 min
#267 item 1 — demo-seeder end-to-end smoke Needs a running stack on a fresh DB
#267 item 3 — remove CL_SEED_DEMO A decision gated on demo-deploy stability; also moves the demo entrypoint
#291 items 2/3/4 Needs real production UUIDs and sha256 digests, then a 2-week observation window
#287 items 2/5 — arm64, Helm + ArgoCD Gated on managed hosting, which is parked
#287 item 4 — SBOM Trigger (design-partner/compliance) has not fired
#285 — hosted secrets backend Same parked trigger; parked by design — the issue says so
#356 secrets — SSH deploy key, host details Owner provisions; least-privilege deploy key, not a personal key
#412 — wildfire/windstorm footprints CDS + EWDS access is an open owner action (also a Phase 9 entry condition)
#366, #367 — product epics Blocked behind Phase 9; #366 also needs insurer interviews — not code work
#273 — demo-tour fix Lives in climate-lama-ui; onboarding is out of MVP scope per CLAUDE.md

Suggested invocation

# 0. Gate trunk first — it is currently ungated at 675a159
gh workflow run ci.yml --ref main       # require green before proceeding

# 1. Create S1..S4 with Execution blocks, then size the whole set once
/analyze-chain 103 276 480 481 <S1> <S2> <S3> <S4> 279 356

# 2. Start the lanes that are NOT migration-bearing — these can run now
/phase-build 103 <S4> 276 279 <S3> 356 --fast

# 3. Lane M only after PR #484 merges and releases the 0062 slot.
#    Re-derive the next revision number at that point; do not assume 0063 still holds.
/phase-build <S1> <S2> 480 481 --fast

--fast per fact 2 — without it every build burns its fix cap on DB connection errors.

If the derived partition disagrees with Lane M, the Deps:/Parallel: blocks are wrong — fix the issue bodies, not the orchestrator. #480 and #481 already carry Parallel: no with the migration rationale inline; S1 and S2 must be created the same way.