v0.7.0 — Cleanup + Performance Release Design

Status: approved (brainstorming → design). Author: brainstorm with @jrs1986, 2026-05-27. Target release: v0.7.0. Related: v0.6.0 (#127), which deprecated --tool both for removal in this release.

1. Problem

Two distinct motivations roll into one release:

  1. Deprecation promise: v0.6.0 deprecated --tool both and the _normalize_persisted_intake legacy-project.json back-fill, both explicitly marked for removal in 0.7.0. The stderr warning, CHANGELOG, and Upgrading wiki all say "0.7.0." Honoring that promise keeps deprecation-warning credibility intact for future windows.

  2. doctor / pilot runtime cost: measured baseline shows doctor at ~243ms median and pilot at ~235ms median per invocation, dominated by probe_hardware() and provider availability checks that re-run every time. For commands explicitly positioned as "safe to run any time, read-only," that's friction.

Both motivations have ~100-line surface areas. Bundled, they make 0.7.0 a release that earns its version bump rather than a ceremony purely to remove a deprecated alias.

2. Goals

  1. Honor every "removed in 0.7.0" promise made in v0.6.0.
  2. Cache probe_hardware() so warm invocations of doctor/pilot are ~3× faster (measurable: ≤100ms median on cached call).
  3. Land pytest-xdist so the developer iteration loop (22s test suite) drops to ~6-8s.
  4. Consolidate the duplicate CODING_TOOLS / VALID_TOOLS constant pair into one source of truth.

3. Non-goals

  • Other performance work in setup run, tools adapt, team sync, etc. Those don't sit on the "ran every day, expected to feel instant" path.
  • Restructuring cli.py (still 2200+ lines) — would be its own release.
  • Adding new features. This release is entirely cleanup + perf.

4. Item 1 — --tool both removal + back-fill cleanup

Removes everything the deprecation warning promised would go in 0.7.0:

4.1 Removed surface

  • "both" from CODING_TOOLS in src/coding_scaffold/cli.py.
  • "both" from INSTALLABLE_TOOLS in src/coding_scaffold/cli.py.
  • "both" from VALID_TOOLS in src/coding_scaffold/intake.py.
  • _BOTH_EXPANSION tuple in intake.py.
  • _BOTH_WARNING_FIRED module-level latch in intake.py.
  • reset_deprecation_state() test helper in intake.py.
  • The both-expansion branch inside normalize_tools (the if chunk == "both" block).
  • _normalize_persisted_intake legacy-tool-key back-fill helper in intake.py.
  • _load_project_intake's inline import + back-fill call in cli.py.
  • IntakeAnswers.agent property in intake.py (reviewer confirmed no production caller during v0.6.0 review).
  • adapters.py no longer needs its defensive from .intake import normalize_tools if the only reason was the both literal; verify and simplify if so.

4.2 New error path for programmatic callers

After removal, the only way "both" can reach normalize_tools is via a Python caller that still passes the string. Raise CliError with the three-line format:

raise CliError(
    cause="`--tool both` was removed in 0.7.0",
    next_step="use `--tool opencode,openclaude` instead",
    link="https://jrs1986.github.io/CodingScaffold/wiki/Upgrading",
)

The CLI's --tool both invocation hits argparse's "invalid choice" rejection before normalize_tools runs — that's already a clear error and the documented path.

4.3 Test removals + additions

  • Remove: test_both_expands_to_opencode_openclaude_and_warns, test_both_deprecation_warning_fires_once_per_process from tests/test_normalize_tools.py.
  • Remove: test_both_alias_still_works_with_deprecation_warning from tests/test_multi_tool.py.
  • Remove: test_normalize_persisted_intake_back_fills_legacy_tool_key, test_normalize_persisted_intake_back_fills_legacy_agent_key, test_normalize_persisted_intake_passes_through_modern_payload, test_normalize_persisted_intake_handles_missing_tool from tests/test_intake.py.
  • Remove: test_legacy_project_json_with_singular_tool_still_updates from tests/test_multi_tool.py.
  • Add: test_both_raises_cli_error_for_programmatic_callers confirming the removal path produces the three-line error.
  • Add: test_cli_setup_run_rejects_both_at_argparse confirming the CLI invocation path produces the argparse "invalid choice" rejection.

4.4 Docs

  • docs/docs/wiki/Upgrading.md: move "Breaking change planned for 0.7.0 — --tool both removed" from "planned" to "shipped" framing.
  • CHANGELOG.md: new ### Removed section under the 0.7.0 release block.

5. Item 2 — probe_hardware() caching

5.1 Cache shape

Cache lives at ~/.cache/coding-scaffold/hardware.json (XDG-conformant via os.environ.get("XDG_CACHE_HOME") or ~/.cache). Schema:

{
  "version": 1,
  "cached_at": "2026-05-27T08:30:00Z",
  "key": "darwin/arm64/3.11.9",
  "profile": {
    "os_name": "...",
    "arch": "...",
    "cpu_count": 8,
    "ram_gb": 16,
    "gpu_name": null,
    "vram_gb": null,
    "is_wsl": false,
    "llmfit_available": false,
    "local_runtimes": ["ollama"]
  }
}

5.2 Cache key

{platform.system().lower()}/{platform.machine()}/{python_version_short}. Invalidates automatically when:

  • OS changes (e.g., user moves from macOS to Linux container)
  • Architecture changes (e.g., Rosetta → native)
  • Python version changes (could affect detection heuristics)

5.3 TTL

Hardware doesn't change on a hot machine, but probe also detects local runtimes (ollama, lmstudio, llama-server) which the user installs ad-hoc. TTL = 1 hour balances staleness (a fresh ollama install is reflected within the hour) against the cost of re-probing.

5.4 API

probe_hardware() keeps its current signature but adds optional use_cache: bool = True kwarg. The CLI exposes --no-probe-cache on doctor, pilot, and probe to force a fresh probe.

On cache miss / expiry / corrupt JSON: probe normally, write fresh cache, proceed. On cache directory unwritable (read-only FS, permission denied): probe normally, log a one-line warning to stderr, do not retry.

5.5 Tests

  • test_probe_hardware_cache_hit_returns_cached_profile (mock filesystem)
  • test_probe_hardware_cache_miss_writes_fresh_cache
  • test_probe_hardware_cache_expired_re_probes
  • test_probe_hardware_corrupt_cache_re_probes_silently
  • test_probe_hardware_no_cache_flag_bypasses_cache
  • test_probe_hardware_unwritable_cache_dir_proceeds_with_warning
  • Integration: test_doctor_warm_invocation_is_under_100ms (a perf gate)

5.6 Acceptance

coding-scaffold doctor --target . warm call ≤ 100ms median over 5 runs.

6. Item 3 — pytest-xdist enablement

6.1 Changes

  • Add "pytest-xdist>=3.0" to [project.optional-dependencies] dev in pyproject.toml.
  • Add addopts = "-n auto" to [tool.pytest.ini_options].
  • Refresh uv.lock.

6.2 Parallel-safety audit

Walk the test suite for module-level state that would break under parallel execution:

  • _BOTH_WARNING_FIRED — being removed in Item 1, no longer a concern.
  • monkeypatch usage — pytest-xdist is safe with monkeypatch (per-worker).
  • tmp_path usage — pytest-xdist gives each worker its own tmp.
  • Tests that read/write to a fixed external path (e.g., ~/.cache/...) could collide. The new probe cache from Item 2 needs to be redirected to tmp_path in tests; spec already requires that via the mock-filesystem pattern.

6.3 Acceptance

  • uv run pytest -q median wall time ≤ 8s (was ~22s sequentially).
  • All 600+ tests pass under parallel execution.
  • CI workflow runs in -n auto mode without changes (workflow already calls uv run pytest).

7. Item 4 — CODING_TOOLS / VALID_TOOLS consolidation

7.1 Single source of truth

Move CODING_TOOLS from cli.py into intake.py adjacent to VALID_TOOLS. cli.py imports it from there (it already imports DEFAULT_TOOLS and normalize_tools from intake, so no circular-import risk).

VALID_TOOLS becomes frozenset(CODING_TOOLS) derived at module load time — both stay in sync by construction.

7.2 Test removal

test_valid_tools_matches_cli_coding_tools in tests/test_normalize_tools.py is no longer needed (the constants can't drift). Drop it.

7.3 Acceptance

  • Single canonical definition of the tool list.
  • No drift-detection test.
  • No regressions.

8. Release shape

Single feature branch feat/v0.7.0-cleanup-and-perf with one PR. Four commits, one per item, in this order (each must leave the suite green):

  1. Item 4 — CODING_TOOLS / VALID_TOOLS consolidation (smallest, foundation for Item 1).
  2. Item 1 — --tool both removal + back-fill cleanup.
  3. Item 2 — probe_hardware() caching.
  4. Item 3 — pytest-xdist enablement.

Then a separate release/v0.7.0 PR in the v0.5.1/v0.6.0 pattern: CHANGELOG promote to [0.7.0] — YYYY-MM-DD, version bump, uv.lock refresh.

9. Open questions

None at design time. Confirmed in brainstorm:

  • ✅ All four items bundled into a single release.
  • probe_hardware() cache TTL = 1 hour.
  • ✅ Cache location = XDG-conformant ~/.cache/coding-scaffold/.
  • ✅ CLI keeps --no-probe-cache escape hatch.

10. Estimated scope

ItemNet linesTestsNotes
1. both removal~140 removed~80 removed, ~20 addedpromised cleanup
2. probe_hardware cache~80 added~120 addedreal user-visible perf
3. pytest-xdist~10 addednone addeddev-loop
4. constant consolidation~10 added, ~25 removed~15 removedfoundation

Net: ~95 lines removed, ~140 lines of test changes, single PR. Single-day implementation expected.