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:
-
Deprecation promise: v0.6.0 deprecated
--tool bothand the_normalize_persisted_intakelegacy-project.jsonback-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. -
doctor/pilotruntime cost: measured baseline showsdoctorat ~243ms median andpilotat ~235ms median per invocation, dominated byprobe_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
- Honor every "removed in 0.7.0" promise made in v0.6.0.
- Cache
probe_hardware()so warm invocations ofdoctor/pilotare ~3× faster (measurable: ≤100ms median on cached call). - Land pytest-xdist so the developer iteration loop (22s test suite) drops to ~6-8s.
- Consolidate the duplicate
CODING_TOOLS/VALID_TOOLSconstant 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"fromCODING_TOOLSinsrc/coding_scaffold/cli.py."both"fromINSTALLABLE_TOOLSinsrc/coding_scaffold/cli.py."both"fromVALID_TOOLSinsrc/coding_scaffold/intake.py._BOTH_EXPANSIONtuple inintake.py._BOTH_WARNING_FIREDmodule-level latch inintake.py.reset_deprecation_state()test helper inintake.py.- The
both-expansion branch insidenormalize_tools(theif chunk == "both"block). _normalize_persisted_intakelegacy-tool-key back-fill helper inintake.py._load_project_intake's inline import + back-fill call incli.py.IntakeAnswers.agentproperty inintake.py(reviewer confirmed no production caller during v0.6.0 review).adapters.pyno longer needs its defensivefrom .intake import normalize_toolsif the only reason was thebothliteral; 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:
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_processfromtests/test_normalize_tools.py. - Remove:
test_both_alias_still_works_with_deprecation_warningfromtests/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_toolfromtests/test_intake.py. - Remove:
test_legacy_project_json_with_singular_tool_still_updatesfromtests/test_multi_tool.py. - Add:
test_both_raises_cli_error_for_programmatic_callersconfirming the removal path produces the three-line error. - Add:
test_cli_setup_run_rejects_both_at_argparseconfirming 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 bothremoved" from "planned" to "shipped" framing.CHANGELOG.md: new### Removedsection 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:
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_cachetest_probe_hardware_cache_expired_re_probestest_probe_hardware_corrupt_cache_re_probes_silentlytest_probe_hardware_no_cache_flag_bypasses_cachetest_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] devinpyproject.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.monkeypatchusage — pytest-xdist is safe with monkeypatch (per-worker).tmp_pathusage — 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 totmp_pathin tests; spec already requires that via the mock-filesystem pattern.
6.3 Acceptance
uv run pytest -qmedian wall time ≤ 8s (was ~22s sequentially).- All 600+ tests pass under parallel execution.
- CI workflow runs in
-n automode without changes (workflow already callsuv 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):
- Item 4 —
CODING_TOOLS/VALID_TOOLSconsolidation (smallest, foundation for Item 1). - Item 1 —
--tool bothremoval + back-fill cleanup. - Item 2 —
probe_hardware()caching. - 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-cacheescape hatch.
10. Estimated scope
Net: ~95 lines removed, ~140 lines of test changes, single PR. Single-day implementation expected.