# Modernization Task Tracker

Status: live
Last updated: 2026-06-17

This file is the active execution queue. It is written for a coordinator that
can assign bounded packets to smaller parallel workers. Completed and stale
history belongs in `docs/modernization/tasks-done.md`, not here.

## Operating Rules

- Prioritize working-app ownership transfer over planners, CLI commands,
  package-only cleanup, or test-only work.
- Every coding task must remove or narrow a real retained dependency, hotspot,
  unsafe ownership path, or thread/runtime ambiguity.
- Tests are validation and guardrails. A task that only adds tests is not a P0
  modernization slice unless it directly enables a blocked ownership move.
- Do not broaden worker scopes. If a task crosses file boundaries, split it
  into worker packets with disjoint write scopes and integrate centrally.
- No new `App::I`, `Canvas::I`, owning raw `Node*`, detached worker, direct GL
  resource dependency, or platform SDK dependency may be introduced in moved
  code.
- Raw pointers may remain only as documented non-owning implementation details
  backed by a checked owner, handle, scoped connection, or explicit lifetime
  contract.
- Preserve current app behavior first. UI appearance, file formats, brush
  behavior, platform behavior, and rendering output are not to be redesigned in
  modernization slices.
- Use CMake source ownership as the progress signal. Shrinking
  `PP_PANOPAINTER_*` and `PP_LEGACY_*` ownership matters more than adding new
  helpers around the same retained code.

## Current Audit Snapshot

Validation performed during the 2026-06-17 review:

- `python scripts/dev/check_component_boundaries.py`: passed.
- `python scripts/dev/check_renderer_api_contract.py`: passed.

Key facts:

- Pure component boundaries currently pass their static checks.
- Remaining architectural risk is concentrated in the working app, retained
  app/UI/canvas targets, singleton reach, raw node ownership, and direct GL
  resource usage.
- `PP_PANOPAINTER_APP_SOURCES`: 47 files, about 9620 lines.
- `PP_PANOPAINTER_UI_SOURCES`: 52 files, about 9051 lines.
- `PP_LEGACY_PAINT_DOCUMENT_SOURCES`: 22 files, about 6277 lines.
- `PP_LEGACY_APP_SOURCES`: 26 files, about 4711 lines.
- `PP_LEGACY_UI_CORE_SOURCES`: 32 files, about 4304 lines.
- `App::I` still appears hundreds of times in retained app/canvas/UI/resource
  code.
- `Canvas::I` still appears hundreds of times in retained canvas modes, panels,
  and workflow bridges.
- Raw `Node*` and callback captures remain a dominant UI lifetime risk.
- Retained stroke-preview/runtime draw paths still depend on legacy
  render/runtime helpers, but `RTT`, `Texture2D`, `Shape`, `Shader`,
  `TextMesh`, and `CanvasLayer` no longer call `App::I` directly for queueing.
- `AppRuntime` now owns synchronized running flags plus explicit post/reject,
  same-thread execution, and queue-drain behavior, but broader singleton reach
  and app-shell ownership remain.
- Retained cloud upload/download, brush-package import, and timelapse-export
  async paths now route through `AppRuntime::canvas_async_task`, but dialog and
  execution ownership still remains in retained app/document/cloud bridges.
- `App::dialog_browse()` no longer owns browse-dialog button wiring inline; the
  retained document-open bridge now owns that handoff in
  `src/legacy_document_open_services.*`.
- `App::init_toolbar_main()` now delegates retained main-toolbar button wiring
  through `src/legacy_main_toolbar_binding_services.*`, so
  `src/app_layout_main_toolbar.cpp` is down to a thin root lookup and adapter
  call while retained toolbar execution still lives in
  `src/legacy_app_shell_services.*`.
- `App::init_menu_file()` now delegates retained File-menu popup and export
  submenu wiring through `src/legacy_file_menu_binding_services.*`, so
  `src/app_layout_file_menu.cpp` is down to a thin trigger lookup and adapter
  call while retained file/export execution still lives in
  `src/legacy_app_shell_services.*`.
- `App::init_menu_about()` now delegates retained About-menu popup wiring
  through `src/legacy_about_menu_binding_services.*`, so
  `src/app_layout_about_layer_menu.cpp` no longer owns the About callback body
  inline while retained About execution still lives in
  `src/legacy_app_shell_services.*`.
- `App::init_menu_tools()` now delegates the retained Tools > Panels submenu
  wiring through `src/legacy_tools_menu_binding_services.*`, so
  `src/app_layout_tools_menu.cpp` no longer owns that floating-panel submenu
  body inline while retained Tools execution and options wiring remain.
- `App::update()` now delegates retained app-frame layout update and
  canvas-toolbar refresh execution through `src/legacy_app_frame_services.*`,
  so `src/legacy_app_runtime_shell_services.cpp` is thinner at the frame
  update seam while draw-time execution still remains there.
- `App::tick()` and `App::resize()` now delegate retained app-frame tick and
  surface-resize execution through `src/legacy_app_frame_services.*`, so
  `src/app_events.cpp` is thinner at the frame-execution seam while broader
  input/platform dispatch still remains there.
- `App::ui_save()` and `App::ui_restore()` now delegate retained floating and
  docked panel persistence through `src/legacy_app_ui_state_services.*`, so
  `src/app_layout_ui_state.cpp` is down to thin preference adapters while RTL
  direction execution stays local.
- `App::init_sidebar()` now delegates the retained color-popup open/close
  wiring through `src/legacy_sidebar_color_popup_services.*` with explicit
  `App&`, popup-root, trigger-button, and panel dependencies, so
  `src/app_layout_sidebar.cpp` is thinner while the retained stroke/grid/layer
  popup families still remain inline.
- `App::dialog_usermanual()`, `App::dialog_changelog()`, and
  `App::dialog_about()` now delegate the retained info-dialog construction and
  overlay close wiring through `src/legacy_info_dialog_services.*` with
  explicit `App&` plus overlay-anchor dependencies, so
  `src/app_dialogs_info_openers.cpp` is thinner while `open_whatsnew_dialog()`
  still owns the retained whats-new flow inline.
- `App::dialog_newdoc()` and `App::dialog_save()` now delegate retained dialog
  construction and button wiring through `src/legacy_document_session_services.*`,
  so `src/app_dialogs_workflow.cpp` is thinner at the document-session seam
  while browse, open, and resize retained workflows still remain there.
- `App::title_update()` now delegates retained document-title and DPI-label
  rendering through `src/legacy_app_status_services.*`, so
  `src/app_layout.cpp` no longer owns that app-status family inline.
- `App::init_toolbar_draw()` now delegates retained draw-toolbar button lookup,
  click wiring, and default-tool application through
  `src/legacy_draw_toolbar_binding_services.*`, so
  `src/app_layout_draw_toolbar.cpp` is down to a thin adapter while retained
  tool execution still flows through `src/legacy_canvas_tool_services.*`.
- `App::init_sidebar()` now delegates the retained stroke-popup open/anchor/tick
  wiring through `src/legacy_sidebar_stroke_popup_services.*` with explicit
  `App&`, popup-root, trigger-button, and panel dependencies, so
  `src/app_layout_sidebar.cpp` is thinner while the retained grid and layer
  popup families still remain inline.

## Parallel Assignment Rules

Coordinator packets for workers should include only:

- task id and one-paragraph goal
- exact write scope
- allowed read scope
- debt ids that matter
- required validation command
- specific `rg` or `clangd_nav.py` queries
- current behavior notes needed to avoid broad rediscovery

Safe parallel groups:

- One worker on canvas/render execution, one worker on generic UI controls, one
  worker on platform CMake cleanup, and one worker on runtime queue contracts
  can run in parallel if write scopes remain disjoint.
- Do not run two workers against `src/app_runtime.*`, `src/node.*`,
  `src/legacy_canvas_document_io_services.cpp`, or
  `src/legacy_node_stroke_preview_runtime_services.cpp` at the same time.
- Do not assign both a CMake source-list move and code edits touching the same
  source files to separate workers unless the coordinator serializes the CMake
  integration.

## P0 Queue

### ARC-RUN-010 - Harden `AppRuntime` Into An Explicit Runtime Service

Status: Ready

Why now:
Render/UI/background queues are central to memory and thread safety. The
current `AppRuntime` owns several `std::jthread` workers, but runtime state is
still mutable, partly unsynchronized, and app-specific. The working app still
uses `App::I` as the practical access path to render/UI queues.

Write scope:

- `src/app_runtime.h`
- `src/app_runtime.cpp`
- `src/app.h`
- `src/legacy_app_runtime_shell_services.cpp`
- `src/app_core/app_thread.h`
- `tests/app_core/app_thread_tests.cpp`

Read scope:

- `src/texture.cpp`
- `src/rtt.cpp`
- `src/shape.cpp`
- `src/shader.cpp`
- `src/platform_windows/windows_platform_services.cpp`

Required work:

- Make render/UI/prepared-file/canvas worker running state synchronized or
  atomic, with a single shutdown path per worker.
- Add explicit runtime service methods for thread-affinity checks and
  post/drain/shutdown semantics.
- Keep exceptions from escaping worker bodies.
- Stop exposing queue usage only through `App::I` wrappers for touched call
  sites.
- Keep behavior identical for same-thread immediate execution and blocking
  render/UI calls.

Done when:

- Touched queue state has no unsynchronized read/write ambiguity.
- Worker shutdown drains or rejects queued work according to documented
  behavior.
- Touched app code can call an explicit runtime service instead of reaching
  queues through singleton state.
- App-thread planner tests cover shutdown, stopped-worker enqueue, same-thread
  execution, and queue-drain behavior that the live runtime implements.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_app_core_app_thread_tests -TestRegex "pp_app_core_app_thread"
python scripts/dev/check_component_boundaries.py
```

Mini-model packet:
Start by auditing `render_running_`, `ui_running_`,
`prepared_file_running_`, `canvas_async_running_`, thread ids, and worker stop
methods. Do not rewrite GL resources in this task; expose the runtime contract
needed for the next task.

### ARC-RND-010 - Move GL Resource Queueing Behind Renderer Runtime Contracts

Status: Ready

Why now:
`RTT`, `Texture2D`, `Shape`, `Shader`, `Font`, and `CanvasLayer` still use
`App::I->render_task*` directly. That blocks renderer backends and hides thread
affinity behind a global app singleton.

Write scope:

- `src/texture.cpp`
- `src/texture.h`
- `src/rtt.cpp`
- `src/rtt.h`
- `src/shape.cpp`
- `src/shape.h`
- `src/shader.cpp`
- `src/shader.h`
- `src/font.cpp`
- `src/font.h`
- `src/canvas_layer.cpp`
- `src/canvas_layer.h`
- narrow adapter files if introduced under `src/renderer_gl/`

Read scope:

- `src/app_runtime.*`
- `src/renderer_api/*`
- `src/renderer_gl/*`
- `src/paint_renderer/*`

Required work:

- Introduce a narrow render-dispatch interface or adapter consumed by retained
  GL resource classes.
- Convert one coherent GL resource family per slice; do not edit every file in
  one worker pass unless the abstraction is already integrated.
- Preserve blocking versus async semantics exactly.
- Do not move app policy into `pp_renderer_gl`.
- Do not add future backend implementation work.

Done when:

- The touched GL resource family no longer calls `App::I` directly for queueing
  or thread checks.
- Render-thread assertions use an explicit runtime/render-dispatch contract.
- CMake ownership remains consistent and no pure renderer API target depends on
  app headers.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_renderer_api_tests,pp_renderer_gl_capabilities_tests -TestRegex "pp_renderer|pp_paint_renderer"
python scripts/dev/check_renderer_api_contract.py
```

Mini-model packet:
Start with either `Texture2D`/`RTT` or `Shape`/`Shader`, not both. Use `rg -n
"App::I->render_task|is_render_thread" src/texture.* src/rtt.*` for the first
slice.

### ARC-RND-011 - Split Canvas Document I/O From Render Execution

Status: Ready

Why now:
`src/legacy_canvas_document_io_services.cpp` is still the largest working-app
document/export hotspot and has the highest `App::I` concentration found in the
review. It mixes license checks, worker dispatch, render readback, progress UI,
platform publish/flush, and retained `Canvas` mutation.

Write scope:

- `src/legacy_canvas_document_io_services.cpp`
- `src/legacy_canvas_document_io_services.h`
- `src/legacy_document_export_services.*`
- `src/app_core/document_export.h`
- `src/paint_renderer/*`
- focused tests under `tests/app_core` or `tests/paint_renderer`

Read scope:

- `src/canvas.*`
- `src/canvas_layer.*`
- `src/legacy_canvas_render_shell_services.*`
- `src/platform_api/platform_services.h`

Required work:

- Pick one export/import family first: equirectangular export, cube-face export,
  layer/frame collection export, or project save/open async I/O.
- Move orchestration into an app-core or paint-renderer service request that
  accepts explicit document/render/platform dependencies.
- Leave retained `Canvas` as a final adapter only for data that has not moved.
- Remove direct `App::I` calls from the touched path.
- Preserve progress and platform publish behavior.

Done when:

- One live document/export path is executable through an explicit service
  request rather than by walking `App::I`/`Canvas::I` from the bridge.
- The retained bridge is visibly thinner and has fewer reasons to know about
  UI, worker, platform, and renderer details at the same time.
- The touched path has focused validation that exercises the new request
  contract and the retained adapter.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_export_tests,pp_paint_renderer_compositor_tests -TestRegex "document_export|paint_renderer"
```

Mini-model packet:
Do not broaden into all export types. Start with the path that already has the
strongest pure planning/readiness coverage, then remove only the corresponding
direct app/canvas singleton reach.

### ARC-RND-012 - Make Stroke Preview A Renderer-Owned Service

Status: Ready

Why now:
`NodeStrokePreview` has been thinned, but
`src/legacy_node_stroke_preview_runtime_services.cpp` still owns static worker
state, render-context handoff, preview texture lifetime, and direct app/canvas
access. This is a high-risk UI/render/thread boundary.

Write scope:

- `src/node_stroke_preview.*`
- `src/legacy_node_stroke_preview_runtime_services.*`
- `src/legacy_node_stroke_preview_draw_services.*`
- `src/legacy_node_stroke_preview_sample_services.*`
- `src/paint_renderer/*`
- `tests/paint_renderer/*`

Read scope:

- `src/app_runtime.*`
- `src/texture.*`
- `src/rtt.*`
- `src/canvas.*`
- `src/node_panel_stroke.*`

Required work:

- Move one preview execution phase behind a renderer-facing service contract.
- Replace static worker/resource state for the touched phase with owned service
  state or explicit runtime dependency.
- Remove direct `App::I`/`Canvas::I` from the touched phase.
- Preserve preview output and cancellation/shutdown behavior.

Done when:

- The touched preview phase can be reasoned about without reading the full node
  implementation.
- Preview worker lifetime is owned and cancellable for the touched phase.
- Renderer-facing tests cover the contract without linking app texture objects.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_paint_renderer_compositor_tests -TestRegex "paint_renderer|stroke_preview"
```

Mini-model packet:
Start with result copy, live pass request assembly, or worker lifecycle. Do not
combine all preview phases in one slice.

### ARC-UI-010 - Move Generic Controls Out Of `pp_legacy_ui_core`

Status: Ready

Why now:
Generic controls still live in `PP_LEGACY_UI_CORE_SOURCES`, keeping
`pp_panopainter_ui` tied to retained app/UI targets. This is working-app UI
architecture, not cosmetic cleanup.

Write scope:

- `src/node_button.*`
- `src/node_checkbox.*`
- `src/node_icon.*`
- `src/node_image.*`
- `src/node_scroll.*`
- `src/node_slider.*`
- `src/node_text.*`
- `src/node_text_input.*`
- `src/ui_core/*`
- `cmake/PanoPainterSources.cmake`
- `CMakeLists.txt`

Read scope:

- `src/node.*`
- `src/layout.*`
- app-specific `src/node_panel_*`
- app-specific `src/node_dialog_*`

Required work:

- Move one generic control family at a time to `pp_ui_core`.
- Split renderer-neutral state/event/layout logic from retained GL drawing
  when a control still depends on GL classes.
- Keep app-specific panels and dialogs out of `pp_ui_core`.
- Update CMake ownership so the source-list change is real.

Done when:

- At least one generic control family is owned by `pp_ui_core` or has its
  renderer-neutral core owned there with only a narrow retained draw adapter.
- `PP_LEGACY_UI_CORE_SOURCES` shrinks.
- Existing UI behavior is unchanged.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_ui_core_layout_xml_tests,pp_ui_core_node_lifetime_tests,pp_ui_core_overlay_lifetime_tests -TestRegex "pp_ui_core"
python scripts/dev/check_component_boundaries.py
```

Mini-model packet:
Start with the least app-specific control: checkbox, button, icon, image,
scroll, slider, text, or text input. Do not touch panels/dialogs in the same
slice.

### ARC-UI-011 - Convert UI Ownership To Checked Handles By Default

Status: Ready

Why now:
`pp_ui_core` has checked lifetime helpers, but base `Node` and app panels still
mix raw parent/manager pointers, shared child vectors, raw callback parameters,
and destroy-during-callback assumptions.

Write scope:

- `src/node.*`
- `src/layout.*`
- `src/legacy_ui_overlay_services.*`
- one dialog or panel family per slice under `src/node_dialog_*` or
  `src/node_panel_*`
- `src/ui_core/node_lifetime.*`
- `src/ui_core/overlay_lifetime.*`

Read scope:

- call sites found with `rg -n "add_child|remove_child|destroy\\(|on_.*=|Node\\*" src/node_dialog_* src/node_panel_* src/legacy_ui_* src/node.*`

Required work:

- Convert one popup/dialog/panel family to checked handles or scoped
  connections.
- Remove raw lifetime assumptions from callbacks in the touched family.
- Document any remaining raw `Node*` as non-owning views with owner proof.
- Keep visual behavior and event ordering unchanged.

Done when:

- The touched UI family can close during callback dispatch without relying on
  dangling raw pointers.
- Overlay/popup lifetime flows through `pp_ui_core` lifetime primitives by
  default.
- New touched callbacks are scoped or handle-checked.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_ui_core_node_lifetime_tests,pp_ui_core_overlay_lifetime_tests -TestRegex "ui_core_(node_lifetime|overlay_lifetime)"
```

Mini-model packet:
Pick one family only, such as open/browse dialogs, picker dialog, popup menu,
layer panel, or stroke panel. Avoid broad `Node` redesign unless the family
requires a small base helper.

### ARC-APP-010 - Reduce App Shells To Composition And Adapters

Status: Ready

Why now:
`PP_PANOPAINTER_APP_SOURCES` is still about 9620 lines. The app shell owns
workflow, dialogs, layout binding, runtime, VR, cloud, brush package, platform
hooks, and retained document/export adapters.

Write scope:

- one `src/app_*.cpp` family per slice
- matching `src/legacy_app_*` service files
- matching app-core planner/service headers only when needed
- `cmake/PanoPainterSources.cmake` if ownership moves

Read scope:

- `src/app.h`
- relevant app-core headers under `src/app_core`
- relevant UI node files for the touched workflow

Required work:

- Pick one shell family: layout menus, dialogs, startup/frame, cloud, brush
  package, recording, VR, or document session.
- Move retained implementation into a named service with explicit dependencies.
- Make the app method a thin adapter or composition call.
- Do not add planner-only coverage unless the app method actually shrinks.

Done when:

- One app shell file loses real workflow/runtime ownership.
- The new service accepts explicit `App&`, runtime, platform, UI, document, or
  renderer dependencies instead of reading global state internally.
- The touched path has focused validation or an app build gate.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli -TestRegex "pp_app_core|pano_cli_plan"
```

Mini-model packet:
Start with a single app shell family. Do not mix dialogs, layout, cloud, and VR
in one worker assignment.

### ARC-PLT-010 - Finish Platform Source Ownership In CMake

Status: Ready

Why now:
Platform implementation ownership improved, but CMake still leaks Web platform
sources into `PP_PANOPAINTER_APP_SOURCES`. Platform implementation files should
belong to concrete `pp_platform_*` targets, not the app source group.

Write scope:

- `cmake/PanoPainterSources.cmake`
- `CMakeLists.txt`
- `src/platform_web/*` only if build integration requires a narrow include or
  factory adjustment

Read scope:

- `webgl/CMakeLists.txt`
- `src/platform_android/*`
- `src/platform_linux/*`
- `src/platform_apple/*`
- `src/platform_api/platform_services.h`

Required work:

- Remove `${PP_PLATFORM_WEB_SOURCES}` from `PP_PANOPAINTER_APP_SOURCES`.
- Ensure root app/platform targets link `pp_platform_web` only when needed.
- Keep WebGL retained package entrypoint behavior unchanged.
- Do not reintroduce `platform_legacy`.

Done when:

- Concrete Web platform files are not compiled as app sources in the root app
  source group.
- The source ownership direction is visible in CMake.
- Platform API boundary checks still pass.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pp_platform_api_tests -TestRegex "pp_platform_api"
python scripts/dev/check_component_boundaries.py
```

Mini-model packet:
Keep this structural. Do not edit platform behavior unless CMake exposes a real
link or include problem.

## P1 Queue

### ARC-SAFE-010 - Remove Manual Allocation From Touched Ownership Paths

Status: Ready

Why now:
The review found manual `new`/`delete` pockets in node loading, canvas, layer
actions, Wacom/bootstrap helpers, and retained resource cleanup. Some are
non-owning or placement-new cases, but touched working-app ownership paths
should move to RAII containers and factories.

Write scope:

- one selected ownership path at a time, such as `src/legacy_ui_node_loader.*`,
  `src/node.*`, `src/canvas.cpp`, `src/platform_windows/windows_bootstrap_helpers.cpp`,
  or `src/wacom.cpp`

Read scope:

- immediate owner/caller files for the selected path

Required work:

- Replace owning raw allocation with `std::unique_ptr`, `std::shared_ptr`,
  `std::vector`, `std::string`, or an explicit RAII wrapper.
- Preserve non-owning views only where ownership is proven.
- Avoid mixing this with UI or renderer redesign.

Done when:

- The touched path has no owning raw `new`/`delete`.
- Failure paths cannot leak.
- Lifetime remains clear at call sites.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter -TestRegex "pp_ui_core|pp_app_core"
```

Mini-model packet:
Start with a single obvious allocation family. `legacy_ui_node_loader` is a
good first target because it is UI ownership, not rendering behavior.

### ARC-SAFE-011 - Replace Remaining Ad Hoc Workers With Runtime-Owned Services

Status: Ready

Why now:
Most recent worker conversions use `std::jthread`, but retained worker pockets
still sit in UI/dialog/cloud/grid/preview services and `std::async` remains in
`Asset`. The end state requires service-owned cancellation and shutdown.

Write scope:

- one worker family per slice:
  `src/node_dialog_cloud.*`, `src/legacy_cloud_services.*`,
  `src/legacy_grid_ui_services.*`, `src/asset.*`, or
  `src/legacy_node_stroke_preview_runtime_services.*`

Read scope:

- `src/app_runtime.*`
- corresponding app-core/cloud/grid/preview planner headers
- immediate UI caller files

Required work:

- Move worker ownership behind a runtime/service contract.
- Add cancellation or shutdown semantics for the touched worker.
- Avoid capturing raw nodes across worker completion without checked handles.
- Preserve progress and completion callbacks.

Done when:

- The touched worker cannot outlive its owner.
- Shutdown behavior is explicit and validated.
- UI completion handoff is handle-safe or owner-checked.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter -TestRegex "pp_app_core|pp_ui_core"
```

Mini-model packet:
Pick one worker family. Do not perform broad thread cleanup across unrelated
subsystems in one task.

### ARC-WKF-010 - Thin Document Session/Open/Save Bridges

Status: Ready

Why now:
The pure document/session planners are extensive, but live bridges still own
retained prompts, metadata mutation, title updates, history clearing, snapshot
handoff, and legacy `Canvas` execution.

Write scope:

- `src/legacy_document_open_services.*`
- `src/legacy_document_session_services.*`
- `src/legacy_history_services.*`
- focused app-core document/session headers only when needed

Read scope:

- `src/app_core/document_route.h`
- `src/app_core/document_session.h`
- `src/app_core/document_canvas.h`
- `src/app.h`
- `src/canvas.*`

Required work:

- Pick one bridge path: open-project confirmation, save-before-workflow,
  save-version, new-document overwrite, history clear, or title update.
- Move decisions/mutations behind explicit service requests.
- Keep retained prompts as adapters only.

Done when:

- One document workflow path has a single obvious owner for decision,
  execution, and metadata mutation.
- The retained bridge has less direct `App::I`/`Canvas::I` reach.
- Behavior is covered by existing or focused document-session validation.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_session_tests,pp_app_core_document_route_tests -TestRegex "document_(session|route)"
```

Mini-model packet:
Do not rewrite all document flows. Pick the narrowest path that removes live
bridge ownership.

### ARC-WKF-011 - Split Cloud And Brush Package Work Out Of UI Nodes

Status: Ready

Why now:
Cloud browse/download/upload and brush package import/export still mix UI node
lifetime, worker ownership, storage, network/asset behavior, and app singleton
reach.

Write scope:

- one family per slice:
  `src/legacy_cloud_services.*`, `src/node_dialog_cloud.*`,
  `src/legacy_brush_package_import_services.*`,
  `src/legacy_brush_package_export_services.*`,
  `src/legacy_brush_preset_services.*`,
  `src/node_panel_brush.cpp`

Read scope:

- `src/app_core/document_cloud.h`
- `src/app_core/brush_package_import.h`
- `src/app_core/brush_package_export.h`
- `src/assets/brush_package.*`
- relevant panel/dialog headers

Required work:

- Separate worker/network/asset execution from node lifetime.
- Use app-core requests and asset helpers where they already exist.
- Use checked handles for UI completion callbacks.
- Preserve current cloud and brush package UX.

Done when:

- One cloud or brush package path can be understood without reading panel or
  dialog internals first.
- The touched UI node is a view/controller shell, not the workflow owner.
- The touched worker cannot outlive its service/UI owner.

Validation:

```powershell
powershell -ExecutionPolicy Bypass -File scripts\automation\quiet-validate.ps1 -BuildTargets PanoPainter,pano_cli,pp_app_core_document_cloud_tests,pp_app_core_brush_package_import_tests,pp_app_core_brush_package_export_tests,pp_assets_brush_package_tests -TestRegex "document_cloud|brush_package"
```

Mini-model packet:
Cloud and brush package work are separate packets. Do not assign both to one
small worker.

## Deferred On Purpose

- Vulkan, Metal, WebGPU, and broad future-backend implementation.
- OpenXR implementation beyond boundary cleanup needed to remove OpenVR debt.
- Package-only migration that does not affect root app architecture.
- CLI/planner-only expansion.
- Broad warning cleanup without ownership movement.
- Documentation-only progress claims.
