diff --git a/active.md b/active.md
index fb25a1653..85e4e11c7 100644
--- a/active.md
+++ b/active.md
@@ -1,5 +1,20 @@
# Active Tasks
+## point-solver-magnification-plane-redshift
+- issue: https://github.com/PyAutoLabs/PyAutoLens/issues/480 (filed 2026-04-28, pre-existing)
+- issued: 2026-08-27
+- prompt: active/point_solver_magnification_filter_ignores_plane_redshift.md
+- status: library-dev
+- branch: claude/point-source-json-regime-tmk7la (@PyAutoLens)
+- repos:
+ - PyAutoLens
+- worktree-none: remote web session — no task worktree; the container checkout at
+ /home/user/pyautolens is the working tree and is discarded with the session.
+- note: no new issue was opened — PyAutoLens#480 already describes this bug, so /create_issue
+ was deliberately skipped. Unblocks the two multiple_sources scripts in @autolens_workspace
+ config/build/no_run.yaml and the parked
+ draft/bug/pyautolens/point_source_json_datasets_record_no_regime.md.
+
## transformed-message-factor-gradient-unpack
- issue: https://github.com/PyAutoLabs/PyAutoFit/issues/1501 (issued 2026-08-19)
- issued: 2026-08-19
diff --git a/active/point_solver_magnification_filter_ignores_plane_redshift.md b/active/point_solver_magnification_filter_ignores_plane_redshift.md
new file mode 100644
index 000000000..5171192eb
--- /dev/null
+++ b/active/point_solver_magnification_filter_ignores_plane_redshift.md
@@ -0,0 +1,175 @@
+# PointSolver magnification filter ignores plane_redshift
+
+Type: bug
+Target: pyautolens
+Repos:
+- @PyAutoLens
+Difficulty: small
+Autonomy: supervised
+Priority: high
+Status: formalised
+Filed: 2026-08-27
+Issue: https://github.com/PyAutoLabs/PyAutoLens/issues/480
+
+GitHub issue **PyAutoLens#480** already exists (filed 2026-04-28 by Jammy2211,
+untouched since). This prompt is the Mind-side record for fixing it; no new issue
+should be opened.
+
+## Why now
+
+#480 is the gate on two other pieces of work, which is what surfaced it:
+
+- `point_source/features/multiple_sources/{simulator,modeling}` are excluded from
+ harness execution in @autolens_workspace `config/build/no_run.yaml:41-42`,
+ named as blocked by #480.
+- `draft/bug/pyautolens/point_source_json_datasets_record_no_regime.md` is parked
+ behind exactly that exclusion — its re-check log names #480 closing as the
+ single trigger for doing the work.
+
+So this is one fix that clears a workspace exclusion, two skipped scripts and a
+parked Mind task.
+
+## The bug
+
+`PointSolver.solve(..., plane_redshift=z)` returns 0 positions whenever `z` is an
+**intermediate** plane of a multi-plane tracer. The triangle search is fine —
+`magnification_threshold=0.0` recovers the candidates — only the magnification
+post-filter rejects them, because it measures magnification at the wrong plane.
+
+Two methods on the same class disagree about which plane is being solved for:
+
+- `autolens/point/solver/shape_solver.py:224` — `_plane_grid` **honours**
+ `plane_redshift`: `plane_index_via_redshift_from(redshift=...)`, then
+ `tracer.deflections_between_planes_from(grid=..., plane_i=0, plane_j=plane_index)`.
+- `autolens/point/solver/shape_solver.py:322` — `_filter_low_magnification` takes
+ no `plane_redshift` at all. It builds `ag.LensCalc.from_mass_obj(tracer)`, whose
+ deflection callable is the whole tracer, i.e. the **last** plane.
+
+The search therefore solves to plane *j* and the filter judges those candidates
+as if they were at plane *-1*. In the issue's repro (z = 0.5 / 1.0 / 2.0, solving
+for z=1.0) the candidates come back at magnification ~1e-3–1e-5 — correct for a
+z=2.0 source, meaningless for the z=1.0 one — and the 0.1 threshold discards
+every one of them.
+
+`point_solver.py:242` is the call site that drops the argument on the floor: it
+has `plane_redshift` in scope (it passed it to `solve_triangles` seven lines
+earlier) and does not forward it.
+
+## The fix already exists as API — do not write new physics
+
+@PyAutoGalaxy `autogalaxy/operate/lens_calc.py:165` already provides:
+
+```python
+LensCalc.from_tracer(tracer, use_multi_plane=True, plane_i=0, plane_j=-1)
+```
+
+which binds `tracer.deflections_between_planes_from` with those plane indices via
+`functools.partial` — precisely the callable `_filter_low_magnification` should
+be measuring against. The solver is simply calling the wrong constructor.
+
+**It is a provable no-op for the default case**, which is what #480 asks for.
+`autolens/lens/tracer.py:879-881`:
+
+```python
+if self.total_planes > 1:
+ return self.deflections_between_planes_from(grid=grid, xp=xp)
+return self.deflections_of_planes_summed_from(grid=grid, xp=xp)
+```
+
+`deflections_between_planes_from` defaults to `plane_i=0, plane_j=-1`, so for any
+multi-plane tracer `from_mass_obj(tracer)` and `from_tracer(tracer, plane_j=-1)`
+wrap the *same* callable. Single-plane tracers have one plane, so the summed and
+between-planes forms agree there too. Passing `plane_j=-1` when `plane_redshift is
+None` changes nothing — this is not an argument that needs a tolerance, it is the
+same function.
+
+## The fit side already does this — the solver is the half that was never wired
+
+Worth stating because it changes what this task is: not "choose how to measure magnification at a
+plane", but "finish a feature the rest of the point-source code already has".
+
+@PyAutoLens `autolens/point/fit/abstract.py:125-134`, `magnifications_at_positions`:
+
+```python
+use_multi_plane = len(self.tracer.planes) > 2
+plane_j = (
+ self.tracer.extract_plane_index_of_profile(profile_name=self.name)
+ if use_multi_plane else -1
+)
+od = ag.LensCalc.from_tracer(
+ tracer=self.tracer, use_multi_plane=use_multi_plane, plane_j=plane_j
+)
+```
+
+`_lens_calc_for` in `autolens/point/fit/solved.py:65-82` mirrors it for the Jacobian weighting. So
+`LensCalc.from_tracer(..., plane_j=...)` is the established pattern for exactly this measurement,
+and `from_tracer` exists in @PyAutoGalaxy to serve it. The solver is the single place it never
+reached. The two resolve `plane_j` by different keys, each correct for its own API: the fit knows
+the profile *name* (`extract_plane_index_of_profile`), the solver is handed a *redshift*
+(`plane_index_via_redshift_from`).
+
+**The two halves of one likelihood evaluation therefore disagreed.**
+`autolens/point/fit/positions/image/abstract.py:124-130` already passes the redshift down:
+
+```python
+return self.solver.solve(
+ tracer=self.tracer,
+ source_plane_coordinate=self.source_plane_coordinate,
+ xp=self._xp,
+ plane_redshift=self.plane_redshift,
+ remove_infinities=False,
+)
+```
+
+and `plane_redshift`'s own docstring (`abstract.py:187-199`) says it exists "to ensure that if
+multi-plane tracing is used when solving the model image-plane positions, the correct source-plane
+is used". So for an intermediate-plane source, `model_data` measured magnification at the last
+plane and returned nothing, while `magnifications_at_positions` measured at the source's own plane
+and was right. That is the mechanism behind #480's "AnalysisPoint log-likelihood evaluation fails"
+impact line.
+
+`autolens/analysis/result.py:115` and `:184` pass `plane_redshift` too. The argument was plumbed
+end-to-end through the callers; only the last consumer ignored it.
+
+## Scope
+
+1. Give `_filter_low_magnification` a `plane_redshift: Optional[float] = None`
+ parameter and switch its constructor to `ag.LensCalc.from_tracer(...)` with the
+ resolved `plane_j`.
+2. Forward `plane_redshift` from `point_solver.py:242`.
+3. Extract the `plane_redshift -> plane_index` resolution (currently inline in
+ `_plane_grid`) into one helper both methods call. The bug was two methods
+ disagreeing about the plane; leaving two copies of the resolution invites the
+ same drift back.
+4. Handle the unmatched-redshift case while in there. `plane_index_via_redshift_from`
+ returns `None` when no plane is within its 1e-8 tolerance, and `_plane_grid`
+ then evaluates `traced_grids_list[None]` and raises a bare `TypeError`. A
+ mistyped redshift should say which redshifts the tracer actually has. This is a
+ latent bug on the same lines, not scope creep.
+
+## Testing
+
+**The reason this shipped is that nothing tests it.** No test under
+`test_autolens/point/triangles/` passes `plane_redshift` to the solver at all —
+the multi-plane solve path has zero coverage. Adding that coverage is the larger
+half of this task.
+
+- The #480 repro becomes the regression test: a z=0.5/1.0/2.0 tracer, solve for
+ the z=1.0 source, assert 4 positions rather than 0.
+- Assert the no-op directly: for a multi-plane tracer, `plane_redshift=None` and
+ `plane_redshift=` return identical positions to the current
+ implementation.
+- Cover the unmatched-redshift error from scope item 4.
+- The JAX path shares `_filter_low_magnification`, so check the padded/static-shape
+ path is unaffected (`test_use_jax.py`, `test_implicit_diff.py`).
+
+## Downstream, out of scope here
+
+#480's own text notes that @autolens_workspace simplified its multi-source example
+to work around this (dropping the intermediate source's mass profile), and wants
+that reverted once fixed. That is a separate workspace task — this prompt is the
+library fix. Removing the `no_run.yaml` entries should follow the fix, not ride
+along with it.
+
+
diff --git a/dashboard.html b/dashboard.html
index d4d78270b..1b7ef25ed 100644
--- a/dashboard.html
+++ b/dashboard.html
@@ -204,7 +204,7 @@
PyAutoMindDashboard
Intent. Priority. Flow.
Every task the Mind is holding. Tap a task's 📋 and its /start_dev command is on your clipboard — paste it into a Claude Code chat to route Claude straight to that task. Recent is the same work by date — what has been happening rather than what to do next.
-
1In flight
3Parked
5Planned
132Backlog
+
2In flight
3Parked
5Planned
135Backlog
Last updated 2026-08-27. This page is generated from active/, draft/ and the registry files, so it is only as current as they are. dashboard_refresh.yml re-renders it on every push to main — that heals a stale page, but not a stale prompt: a task that shipped without its prompt advancing to complete/ keeps rendering here as pickable backlog. Reconciling those is the refresh below.
diff --git a/dashboard.md b/dashboard.md
index 6cb67ff8b..9414f9cf2 100644
--- a/dashboard.md
+++ b/dashboard.md
@@ -42,14 +42,14 @@ anything you could not verify.
| Where | Count |
|-------|------:|
-| [In flight](#in-flight) (`active/`) | 1 |
+| [In flight](#in-flight) (`active/`) | 2 |
| [Parked](#parked) (`parked.md`) | 3 |
| [Planned](#planned) (`planned.md`) | 5 |
-| [Backlog](#backlog) (`draft/`) | 132 |
+| [Backlog](#backlog) (`draft/`) | 135 |
## Start here
-**Highest priority** (filed as `high`) — showing 12 of 15
+**Highest priority** (filed as `high`) — showing 12 of 16
📋 TRIAGE: needs manual review before routing — medium · safe · high
@@ -91,6 +91,14 @@ anything you could not verify.
+📋 LensCalc NumPy Hessian step is too coarse for multi-plane tracers — autogalaxy · large · supervised · high
+
+```
+/start_dev draft/bug/autogalaxy/lenscalc_numpy_hessian_step_is_too_coarse.md
+```
+
+
+
📋 multi_galaxy package: new regime package in autolens_workspace — autolens · large · supervised · high
```
@@ -139,14 +147,6 @@ anything you could not verify.
-📋 Fix JIT quick-update visualization output regressions — health_fixes · too-large · supervised · high
-
-```
-/start_dev draft/bug/health_fixes/jit_visualization_outputs.md
-```
-
-
-
**Quick wins** (small enough, and safe enough to run unattended)
- _(none right now)_
@@ -163,6 +163,14 @@ Issued — each has an open GitHub issue and usually a branch. The full record f
+📋 PointSolver magnification filter ignores plane_redshift — issue #480 — issued 2026-08-27 — library-dev
+
+```
+/start_dev active/point_solver_magnification_filter_ignores_plane_redshift.md
+```
+
+
+
## Parked
Started or scoped, not currently in flight — resume by moving the row back to `active.md`. Full detail in [`parked.md`](parked.md).
@@ -247,10 +255,10 @@ Scoped but not started; some are not yet prompt files. Full detail in [`planned.
## Backlog
-**132** filed prompts, not started. Each section is sorted most-pickable first (priority, then size). **25** of them belong to an epic and are listed only under [Epics](#epics) below.
+**135** filed prompts, not started. Each section is sorted most-pickable first (priority, then size). **25** of them belong to an epic and are listed only under [Epics](#epics) below.
-feature — 27
+feature — 28📋 Numba CPU likelihood phase 1: batched MGE convolution + operated-matrix caching — autoarray · medium · supervised · high
@@ -364,6 +372,14 @@ Scoped but not started; some are not yet prompt files. Full detail in [`planned.
+📋 Multi-plane time delays — autolens · large · supervised · normal
+
+```
+/start_dev draft/feature/autolens/multi_plane_time_delays.md
+```
+
+
+
📋 Search settings-estimation + profiling infrastructure (n_starts / batch_size / n_batch) — autolens_profiling · large · supervised · normal
```
@@ -769,7 +785,15 @@ Scoped but not started; some are not yet prompt files. Full detail in [`planned.
-bug — 16
+bug — 17
+
+📋 LensCalc NumPy Hessian step is too coarse for multi-plane tracers — autogalaxy · large · supervised · high
+
+```
+/start_dev draft/bug/autogalaxy/lenscalc_numpy_hessian_step_is_too_coarse.md
+```
+
+📋 Fix release JAX runtime compatibility and likelihood parity — health_fixes · too-large · supervised · high
@@ -1003,7 +1027,7 @@ Scoped but not started; some are not yet prompt files. Full detail in [`planned.
-refactor — 5
+refactor — 6📋 einstein_radius_jit_from: replace static init_guess with a JAX-native seed finder — autogalaxy · too-large · supervised · high
@@ -1029,6 +1053,14 @@ Scoped but not started; some are not yet prompt files. Full detail in [`planned.
+📋 One construction path for plane-bound lensing quantities — autolens · medium · supervised · normal
+
+```
+/start_dev draft/refactor/autolens/one_construction_path_for_plane_bound_lensing.md
+```
+
+
+
📋 Remove the dead EDEN packaging tooling from PyAutoFit — pyautofit · medium · supervised · normal
```
@@ -1156,76 +1188,76 @@ The 50 newest things to happen to the work in hand, newest first — issued, par
| Date | Event | Task |
|------|-------|------|
+| 2026-08-27 | issued | PointSolver magnification filter ignores plane_redshift |
+| 2026-08-27 | filed | One construction path for plane-bound lensing quantities |
+| 2026-08-27 | filed | Multi-plane time delays |
+| 2026-08-27 | filed | LensCalc NumPy Hessian step is too coarse for multi-plane tracers |
| 2026-08-26 | filed | jax_profiling/gradient/imaging/pixelization.py: 3.2% of its pin move… |
| 2026-08-26 | filed | The Brain board should work in a session that has no gh |
| 2026-08-24 | filed | wiki-currency's --check-version gate rots on every library main merge |
| 2026-08-24 | filed | interferometer/jax_grad/gradient.py: eager and jitted likelihoods… |
| 2026-08-24 | filed | autocti_workspace has no Navigator Check, so its CI can never roll… |
| 2026-08-24 | filed | Un-park multi_galaxy/features/scaling_relation/slam once a capped run… |
-| 2026-08-24 | filed | Induct PyAutoReduce into the PyAutoHands release machinery (date… |
-| 2026-08-24 | filed | Heart's local smoke runner cannot run any CTI workspace — no autocti… |
-| 2026-08-23 | filed | pynufft removal: unswept downstream residue (1 hard break + stale… |
-| 2026-08-23 | filed | Phase 3: stop installing pynufft in Hands/Heart CI and PyAutoCTI… |
… 10 more (40 left)
| Date | Event | Task |
|------|-------|------|
+| 2026-08-24 | filed | Induct PyAutoReduce into the PyAutoHands release machinery (date… |
+| 2026-08-24 | filed | Heart's local smoke runner cannot run any CTI workspace — no autocti… |
+| 2026-08-23 | filed | pynufft removal: unswept downstream residue (1 hard break + stale… |
+| 2026-08-23 | filed | Phase 3: stop installing pynufft in Hands/Heart CI and PyAutoCTI… |
| 2026-08-23 | filed | Brain board follow-ups: what real mornings surface |
| 2026-08-22 | filed | smoke_install.sh's stale jax<0.7 pin — CI is on the right jax… |
| 2026-08-22 | filed | Untrack the generated FITS test artifacts in autoarray |
| 2026-08-22 | filed | The reconstruction noise map describes a different estimator than the… |
| 2026-08-22 | filed | Point-source JSON datasets record no resolution regime |
| 2026-08-22 | filed | Is Intel macOS a supported platform, and what is the numpy-only… |
-| 2026-08-21 | filed | Rectangular mesh split: Bilinear (fast CPU default) vs RTU… |
-| 2026-08-20 | filed | Numba CPU likelihood phase 2: kernel-CDF numba fast path (the 49-88%… |
-| 2026-08-20 | filed | Numba CPU likelihood phase 1: batched MGE convolution +… |
-| 2026-08-19 | filed | autofit_profiling: bootstrap the repo + general PyAutoFit profiling… |
… 10 more (30 left)
| Date | Event | Task |
|------|-------|------|
+| 2026-08-21 | filed | Rectangular mesh split: Bilinear (fast CPU default) vs RTU… |
+| 2026-08-20 | filed | Numba CPU likelihood phase 2: kernel-CDF numba fast path (the 49-88%… |
+| 2026-08-20 | filed | Numba CPU likelihood phase 1: batched MGE convolution +… |
+| 2026-08-19 | filed | autofit_profiling: bootstrap the repo + general PyAutoFit profiling… |
| 2026-08-19 | issued | @PyAutoFitTransformedMessage.factor_gradient crashes on first… |
| 2026-08-19 | filed | Release board: local run_logs enrichment |
| 2026-08-19 | filed | RTD organism docs currency: Nerves page, organ-count drift, hands.md… |
| 2026-08-19 | filed | Deduplicate repos_sync.py's check/write pairs |
| 2026-08-19 | filed | Bug in autocti_workspace: the dataset_1d results/database example… |
| 2026-08-18 | parked | single-source-density-design |
-| 2026-08-18 | parked | prior-message-collapse-design |
-| 2026-08-18 | filed | @PyAutoFitTransformedMessage.logpdf/pdf omit the transform… |
-| 2026-08-17 | filed | Which other searches need prior-support handling — coverage audit… |
-| 2026-08-17 | filed | Teach repos_sync --write to stamp organ config surfaces |
… 10 more (20 left)
| Date | Event | Task |
|------|-------|------|
+| 2026-08-18 | parked | prior-message-collapse-design |
+| 2026-08-18 | filed | @PyAutoFitTransformedMessage.logpdf/pdf omit the transform… |
+| 2026-08-17 | filed | Which other searches need prior-support handling — coverage audit… |
+| 2026-08-17 | filed | Teach repos_sync --write to stamp organ config surfaces |
| 2026-08-16 | filed | LogGaussianPrior misreports its own support as (-inf, inf) |
| 2026-08-14 | filed | Three jax_likelihood pins are stale by ~1.24e-4 and fail the smoke… |
| 2026-08-09 | found | isothermal-ell-sph-oversampling-at-the-cusp |
| 2026-08-08 | parked | pyautoreduce-slacs1430-acs-comparison |
| 2026-08-08 | filed | Regenerate autolens_workspace markdown/ so the MGE pages show… |
| 2026-08-07 | filed | Regenerate setup_notebook-drifted notebooks in… |
-| 2026-08-06 | filed | Triage: Convolver "No blurring_image provided" warning in canonical… |
-| 2026-08-06 | filed | Rewrite PyAutoCTI docs/api — 55 of 89 autosummary entries are dead |
-| 2026-08-06 | filed | Dependency-cap refresh 2026-08: safe bumps, astropy 8 decision, two… |
-| 2026-08-05 | filed | Give PyAutoFit searches a seed — today no search can be made… |
… 10 more (10 left)
| Date | Event | Task |
|------|-------|------|
+| 2026-08-06 | filed | Triage: Convolver "No blurring_image provided" warning in canonical… |
+| 2026-08-06 | filed | Rewrite PyAutoCTI docs/api — 55 of 89 autosummary entries are dead |
+| 2026-08-06 | filed | Dependency-cap refresh 2026-08: safe bumps, astropy 8 decision, two… |
+| 2026-08-05 | filed | Give PyAutoFit searches a seed — today no search can be made… |
| 2026-08-04 | filed | dataset/imaging/jwst_lw is untracked because the gitignore was never… |
| 2026-08-04 | filed | cosmos_web_ring stores boolean masks as float64, wasting ~3.4 MB of… |
| 2026-08-04 | filed | autolens_workspace_developer: broad stale-API rot (56 symbols, no CI) |
| 2026-08-04 | filed | Nightly release has been blocked 8 nights running — triage the streak |
| 2026-08-04 | filed | HowToLens ch4 tutorial 3: mask overlay is never actually drawn |
| 2026-08-03 | filed | Un-park imaging/features/scaling_relation/slam — the PyAutoArray#431… |
-| 2026-07-31 | filed | interferometer/start_here.py OOM in nightly release-validation… |
-| 2026-07-31 | filed | Quick-update plotting cost — minutes per update, and it is not JAX… |
-| 2026-07-31 | filed | Cluster-scale gradient-search benchmark (Prodigy vs Nautilus… |
-| 2026-07-30 | filed | plot coverage — follow-ups deferred from plot-coverage-gaps |
diff --git a/draft/bug/autogalaxy/lenscalc_numpy_hessian_step_is_too_coarse.md b/draft/bug/autogalaxy/lenscalc_numpy_hessian_step_is_too_coarse.md
new file mode 100644
index 000000000..1bbdc52cf
--- /dev/null
+++ b/draft/bug/autogalaxy/lenscalc_numpy_hessian_step_is_too_coarse.md
@@ -0,0 +1,55 @@
+# LensCalc NumPy Hessian step is too coarse for multi-plane tracers
+
+Type: bug
+Target: PyAutoGalaxy
+Repos:
+- PyAutoGalaxy
+- PyAutoLens
+Difficulty: large
+Autonomy: supervised
+Priority: high
+Status: formalised
+
+Filed: 2026-08-27
+
+LensCalc's NumPy Hessian uses a hardcoded finite-difference step that is too coarse for
+multi-plane configurations, returning magnifications that are wrong by >100% with flipped signs.
+
+Found on 2026-08-27 while cross-checking the fix for PyAutoLens#480. It is a separate, pre-existing
+bug: #480's fix is accurate, and this was found by the control arm of that check.
+
+`LensCalc._hessian_via_richardson` (autogalaxy/operate/lens_calc.py) evaluates the Hessian by
+central finite differences at a hardcoded `buffer=0.01` arcsec, Richardson-extrapolated at h and
+h/2. That step is fixed regardless of the scale the deflection field actually varies on.
+
+Measured, on the tracer from PyAutoLens#480 (lens z=0.5 Isothermal R_E=1.6; source z=1.0 with its
+own Isothermal R_E=0.2; source z=2.0), magnification at the four image positions of the z=1.0
+source, computed three ways:
+
+ last plane (z=2.0)
+ numpy Richardson FD -0.00694 -0.00221 0.00139 0.00246
+ jax exact autodiff 0.04508 0.01099 -0.08602 -0.01118
+ ray-traced Jacobian 0.04508 0.01099 -0.08602 -0.01101
+
+JAX autodiff (float64) and a Jacobian derived independently from `traced_grid_2d_list_from` agree
+with each other; the NumPy path disagrees by 122% and has the WRONG SIGN on three of four points.
+The ray-traced values are stable across step sizes h=1e-4 to 1e-7, so this is not noise in the
+cross-check.
+
+The same three-way comparison at the intermediate plane (z=1.0) agrees to 1.7e-08. So the failure
+is configuration-dependent, not general: the map to z=1.0 involves only the smooth main lens, while
+the map to z=2.0 additionally passes the compact z=1.0 deflector (R_E=0.2), whose deflection field
+varies on scales where a 0.01 arcsec step is far too coarse.
+
+Why it matters: the NumPy path is the default. `AbstractFitPoint.magnifications_at_positions` uses
+it, so point-source flux fits and source-plane chi-squareds on multi-plane models with a compact
+intermediate deflector are exposed, as is `PointSolver`'s magnification threshold. A sign flip on a
+magnification is not a small error.
+
+Scope to consider: scale the step to the local deflection scale rather than hardcoding it; or
+error-estimate from the Richardson pair (the h vs h/2 difference already bounds the truncation
+error and is currently discarded) and warn or refine when it is large; or make the JAX path
+reachable from NumPy callers. A regression test should pin the multi-plane configuration above
+against the ray-traced Jacobian, which is the independent oracle used to find this.
+
+
diff --git a/draft/bug/pyautolens/point_source_json_datasets_record_no_regime.md b/draft/bug/pyautolens/point_source_json_datasets_record_no_regime.md
index 35031b57d..f6e1067ac 100644
--- a/draft/bug/pyautolens/point_source_json_datasets_record_no_regime.md
+++ b/draft/bug/pyautolens/point_source_json_datasets_record_no_regime.md
@@ -110,6 +110,35 @@ so they are fixed here rather than left to bite again:
Likewise the weak simulator is `scripts/weak/simulator.py` in
@autolens_workspace.
+**2026-08-27 — both facts re-verified, STILL BLOCKED. Do not build.**
+Second gate check in four days, same verdict: no issue was opened, no branch was
+cut, no worktree claimed.
+
+1. **PyAutoLens#480 is still open.** `updated_at` is still byte-identical to
+ `created_at` (2026-04-28T21:01:57Z) — not one edit, comment, label or
+ assignment since it was filed; still no assignees, still
+ `closed_by_pull_requests: 0`. Four months untouched.
+2. **`weak/simple` is still regime-invariant.** `scripts/weak/simulator.py:122`
+ still calls `simulator.via_tracer_from(tracer=tracer, grid=positions,
+ name=dataset_name)`, and the file still contains no `os.environ` read of any
+ kind. Its last commit is 2026-07-27, i.e. it has not moved since the previous
+ re-check either.
+
+The `no_run.yaml` entries happen to sit at `config/build/no_run.yaml:41-42` in
+@autolens_workspace again, matching the original citation the 2026-08-23 note
+found drifted. That is coincidence, not a reason to trust line numbers — keep
+matching on the entry text.
+
+One standing risk the previous note did not record (it predates that note; the
+file has not changed since 2026-07-27, so this is an omission, not a change):
+`weak/simulator.py` *advertises* the helper that would break fact 2. Its prose
+blocks at `:94-95` and `:116-117` name
+`via_tracer_random_positions_from(tracer=..., n_galaxies=..., grid_extent=...)`
+as the "quick uniform-square catalogue" alternative to the explicit annulus this
+script passes. So the invariant is one plausible simplification away from being
+reversed, with nothing in the suite to catch it. Not a reason to build now — but
+if fact 2 ever expires, that is the likely route.
+
Next re-check: when PyAutoLens#480 closes. That is the only trigger — fact 2 is a
standing invariant, not a countdown, and only breaks if someone switches
`weak/simulator.py` to the random-positions helper.
diff --git a/draft/feature/autolens/multi_plane_time_delays.md b/draft/feature/autolens/multi_plane_time_delays.md
new file mode 100644
index 000000000..206406bb9
--- /dev/null
+++ b/draft/feature/autolens/multi_plane_time_delays.md
@@ -0,0 +1,60 @@
+# Multi-plane time delays
+
+Type: feature
+Target: PyAutoLens
+Repos:
+- PyAutoLens
+Difficulty: large
+Autonomy: supervised
+Priority: normal
+Status: formalised
+
+Filed: 2026-08-27
+
+Multi-plane time delays: the time-delay equivalent of the PyAutoLens#480 magnification fix.
+
+#480 fixed magnification being measured at the tracer's last plane rather than the plane being
+solved for. Time delays have the same shape of gap one level up, and it is now the blocking one:
+with #480 fixed, multi-plane point-source modeling works, so a PointDataset can carry
+`time_delays` for a system whose time delays cannot be computed.
+
+Two distinct pieces.
+
+1. `Tracer.time_delays_from` refuses more than two planes. `tracer_util.time_delays_from` raises
+ `RayTracingException` when `len(plane_redshifts) != 2`, and its docstring says so plainly: "It
+ requires a two-plane system (lens and source), and does not currently support multi-plane time
+ delay calculations involving more than two planes, but it could be extended to do so in the
+ future." This is an honest, enforced limitation rather than a silent wrong answer, which is why
+ it is a feature and not a bug. Extending it needs the multi-plane time-delay formalism: the
+ time-delay distance `D_dt = (1+z_l) D_d D_s / D_ds` is a two-plane construction, and the
+ multi-plane generalisation is a sum of per-plane terms, not a substitution into the same
+ expression.
+
+2. `LensCalc.fermat_potential_from` silently mixes planes, and this one IS a latent bug of exactly
+ the #480 kind. `LensCalc.from_tracer(tracer, use_multi_plane=True, plane_j=j)` binds the
+ deflection callable to plane j via `functools.partial`, but takes
+ `potential_2d_from = getattr(tracer, "potential_2d_from", None)` unchanged and passes it in both
+ branches. `Tracer.potential_2d_from` is the sum over ALL galaxies with no plane awareness
+ ("the summed 2D potential of all galaxies in the tracer"). So a plane-bound LensCalc computes
+ `fermat_potential_from` as a plane-j geometric term minus an all-planes potential -- including
+ the potential of deflectors at and beyond plane j, which should not contribute to a source at
+ plane j. `from_tracer` binds half of what the plane affects.
+
+ Not currently reached by `Tracer.time_delays_from`, which builds via `from_mass_obj(galaxies)`
+ on a guarded two-plane system, so this is latent rather than live. It becomes live the moment
+ anything constructs a plane-bound LensCalc and asks for a Fermat potential -- which is what
+ extending time delays to multi-plane will do. Fix it as part of this, or guard it: a plane-bound
+ LensCalc whose potential is not plane-bound should refuse `fermat_potential_from` rather than
+ answer wrongly.
+
+Also worth checking while in here: `time_delay_geometry_term_from` computes
+`src = grid - deflections` then `0.5 * ((grid - src)**2)`, which reduces algebraically to
+`0.5 * |alpha|^2`. Correct for two planes; confirm what it should be for multi-plane before reusing
+it there.
+
+Prior art to read first: PyAutoLens#480 and the branch that fixed it, which established the
+`from_tracer(..., plane_j=...)` pattern and the ray-traced-Jacobian cross-check used to validate
+plane-dependent quantities. Use the same cross-check here -- an independent oracle matters more for
+time delays, where there is no equivalent of "the solver returns 0 images" to make an error obvious.
+
+
diff --git a/draft/refactor/autolens/one_construction_path_for_plane_bound_lensing.md b/draft/refactor/autolens/one_construction_path_for_plane_bound_lensing.md
new file mode 100644
index 000000000..7bb32a4a1
--- /dev/null
+++ b/draft/refactor/autolens/one_construction_path_for_plane_bound_lensing.md
@@ -0,0 +1,57 @@
+# One construction path for plane-bound lensing quantities
+
+Type: refactor
+Target: PyAutoLens
+Repos:
+- PyAutoLens
+Difficulty: medium
+Autonomy: supervised
+Priority: normal
+Status: formalised
+
+Filed: 2026-08-27
+
+Give plane-bound lensing quantities one construction path instead of three hand-rolled ones.
+
+Motivated by PyAutoLens#480, where the solver measured magnification at the wrong plane for four
+months. The fix was small, but the reason the bug existed is structural: every caller that needs a
+lensing quantity "at plane j" builds its own LensCalc, resolves the plane index by its own key, and
+nothing checks the answers agree.
+
+The three sites today:
+
+- `AbstractFitPoint.magnifications_at_positions` (autolens/point/fit/abstract.py) --
+ `use_multi_plane = len(self.tracer.planes) > 2`, then
+ `plane_j = tracer.extract_plane_index_of_profile(profile_name=self.name)`, then
+ `ag.LensCalc.from_tracer(...)`.
+- `_lens_calc_for` (autolens/point/fit/solved.py) -- the same six lines again, duplicated
+ deliberately with a comment saying it "Mirrors AbstractFitPoint.magnifications_at_positions".
+- `ShapeSolver._filter_low_magnification` / `_plane_grid` (autolens/point/solver/shape_solver.py) --
+ resolves by redshift instead, via `plane_index_via_redshift_from`. This is the one that was
+ wrong; it now shares a `_plane_index` helper between the search and the filter, which is a local
+ fix of the same duplication one level down.
+
+So the plane is resolved by profile name in two places and by redshift in a third, and the fit
+carries its answer to the solver as `plane_redshift = planes[plane_index].redshift` -- a
+name -> index -> redshift -> index round trip across two modules. It happens to be consistent (there
+is now a test pinning it), but nothing structural makes it so.
+
+Two further smells worth folding in:
+
+- `use_multi_plane = len(planes) > 2` is a no-op distinction. `Tracer.deflections_yx_2d_from`
+ dispatches to `deflections_between_planes_from(0, -1)` whenever `total_planes > 1`, so the
+ `use_multi_plane=False` branch of `from_tracer` wraps the same callable as the True branch with
+ `plane_j=-1`. Two code paths, one behaviour. Verified numerically during #480: magnifications
+ compare bit-identical, max|diff| = 0.0, for multi-plane and single-plane tracers alike.
+- `from_tracer` binds the deflection callable to the plane but leaves `potential_2d_from` as the
+ tracer's all-planes sum, so a plane-bound LensCalc is only half plane-bound. Filed separately as
+ the multi-plane time-delay task; a refactor here should make that shape impossible to express
+ rather than leave it to be remembered.
+
+Likely direction, to be decided in the task: a single accessor on Tracer -- something like
+`tracer.lens_calc_at(plane_index=... | redshift=... | profile_name=...)` -- that resolves the plane
+once, binds every plane-dependent callable consistently, and is the only way the point-source code
+obtains one. Behaviour-preserving: the #480 tests, the fit/solver agreement test and the
+ray-traced-Jacobian cross-check already pin the numbers this must not change.
+
+