Multi-Tool Intake — Design
Status: approved (brainstorming → design).
Author: brainstorm with @jrs1986, 2026-05-25.
Related: none open. Closes the "one repo, two coding tools" gap that's been
quietly worked around (this very repo ships both AGENTS.md and CLAUDE.md).
Target release: 0.6.0 (deprecation), 0.7.0 (removal of both).
1. Problem
coding-scaffold setup run --tool <X> accepts a single tool name. Projects that
use two coding tools at once (e.g., Codex + Claude Code on the same repo) have
no first-class path: they run setup run twice, or they hand-edit one of
AGENTS.md / CLAUDE.md to stay in sync. The existing --tool both literal
sounds like it solves this, but it expands to opencode + openclaude only —
not Codex + Claude Code, not Claude Code + OpenCode, not any other pair.
Concrete pain (observed in this repo):
AGENTS.md(Codex's project rules) andCLAUDE.md(Claude Code's project rules) both committed, written at different times, drift risk if either is hand-edited later.- The
IntakeAnswers.tool: strfield bakes one tool intorouting.json, which affects downstream model-selection guidance and thesetup updatereconciliation flow.
2. Goals
coding-scaffold setup run --tool codex --tool claude-codeworks in one pass and writes both tools' adapters.coding-scaffold pilot --tool codex,claude-codeprints one shared setup recipe plus a per-tool agent step.- Existing single-value invocations (
--tool codex) keep working unchanged. --tool bothdeprecation has a sane two-release path so existing scripts keep working in 0.6.0 and fail with a clear next-step in 0.7.0.
3. Non-goals
- Cross-tool canonical rules file (approach B from the brainstorm). AGENTS.md and CLAUDE.md remain independently rendered from the same intake; drift between them after hand-edit is not solved here.
- Per-tool policy enforcement (approach C). Policy/MCP/permissions still target one tool's surface at a time.
- New tool support. The known tool set
(
opencode | claude-code | codex | openclaude | hermes | pi) is unchanged.
4. CLI surface
4.1 New shape
4.2 Affected subparsers
The list-typed --tool lands on every CLI surface where setup-time tool
choice is meaningful:
setup run(canonical)init(hidden alias ofsetup run)wizard(hidden alias ofsetup run)setup tool(install / validate)setup-tool(hidden flat alias)tools adaptpilot(new — was single-valued)
doctor is unchanged; it surveys the artifact registry which already lists
both AGENTS.md and CLAUDE.md.
4.3 Argparse pattern
normalize_tools lives in cli.py and is shared by every surface that takes
--tool. It:
- Returns
["opencode"]when input isNoneor[](today's default). - Splits any comma-separated value (
["codex,claude-code"] → ["codex", "claude-code"]). - Expands the deprecated
bothliteral to["opencode", "openclaude"]and prints the deprecation warning to stderr exactly once per invocation. - Dedupes while preserving first-seen order.
- Rejects
manualcombined with any real tool — raises via the sharedfail_witherror helper with cause\--tool manual` excludes other toolsand next-steppick one of: `--tool manual` OR `--tool...``.
5. Data model
5.1 IntakeAnswers (in intake.py)
The Python tool (singular) attribute is removed. Callers that need a
single tool — there are four today (updater.py x2, writers.py x2) — switch
to intake.tools[0]. With the user base small, the cost of one breaking change
is lower than the cost of two parallel APIs forever. Same justification applies
to the JSON shape below.
Persisted form in .coding-scaffold/project.json:
5.2 routing.json
No singular tool key. Consumers reading older project.json /
routing.json files (the snapshot's min_supported_scaffold_version blocks
genuine downgrades, but in-place reads can still hit older shapes) must handle
the absence of tools by falling back to a tool (string) field for one
release; this back-fill lives in a small _normalize_persisted_intake helper
in intake.py so the rest of the code only ever sees tools. The helper is
removed in 0.7.0 alongside --tool both.
5.3 Interactive intake prompt
Validation matches the CLI normalizer — same manual-with-others rejection,
same comma split, same dedup.
6. Writers
6.1 write_tool_adapter (in adapters.py)
Signature widens to accept either a string or a list:
The existing tool == "both" branch (["opencode", "openclaude"]) is removed
when both itself is removed in 0.7.0. In 0.6.0 it's untouched — the
deprecation happens in the CLI normalizer, so by the time the adapter writer
sees the list, both is already expanded.
6.2 write_scaffold (in writers.py)
The tool-agnostic file set is unchanged. The only edit is in routing.json
generation (and the opencode.json / openclaude.json / hermes.json /
pi.json config dumps, which already emit independently of which tool is
"primary").
6.3 setup orchestration (in cli.py)
Adapter results aggregate across all tools — the printed
"Wrote N adapter file(s)" summary becomes "Wrote N adapter file(s) across
M tool(s):
7. Pilot multi-tool output
7.1 Text output
Notes:
--install-toolsappears on step 1 iff at least one of the selected tools is not on PATH (existing logic, generalized).- The agent step lists each tool's binary name (from
TOOL_BINARY_NAMES) once. environment_ok = all(tool_installed[t] for t in selected_tools) and python_ok and git_ok and has_creds_or_runtime.
7.2 JSON output
environment.tools is the canonical per-tool list. There is no singular
tool / environment.tool back-compat field — same rationale as the
IntakeAnswers / routing.json cut in §5: small user base, one breaking
change is cheaper than two parallel shapes. Consumers parsing pilot JSON read
tools[0] if they need a single tool.
7.3 Single-tool pilot stays identical
When --tool receives a single value, the output is bit-for-bit the same as
today (Tools: line elided, no "Then start a session with whichever tool"
header, agent step inlined as 3. like today's recipe). This preserves the
golden tests in test_cli_ux.py and test_pilot.py.
8. Deprecation of both
8.1 Release 0.6.0 — soft deprecation
-
bothremains a valid value inCODING_TOOLSandINSTALLABLE_TOOLS. -
The CLI normalizer expands it to
["opencode", "openclaude"]and emits a one-line warning to stderr: -
The warning fires once per CLI invocation regardless of how many surfaces saw
both(single-shot flag inside the normalizer). -
CHANGELOG
Deprecatedsection names this and points at the upgrade path.
8.2 Release 0.7.0 — removal
-
bothremoved fromCODING_TOOLSandINSTALLABLE_TOOLS. -
Two enforcement paths:
-
CLI users hit argparse's standard "invalid choice" message at parse time (before the normalizer runs).
-
Programmatic callers (Python code still passing
"both"towrite_tool_adapterornormalize_tools) hit the normalizer's explicit check and get the three-line error:
-
-
CHANGELOG
Removedsection + Upgrading.md "Breaking changes in 0.7.0" block.
9. Tests
9.1 Updates to existing files
9.2 New file
tests/test_multi_tool.py — end-to-end coverage:
setup run --tool codex --tool claude-codewrites bothAGENTS.mdandCLAUDE.mdin one pass.tools adapt --tool codex,claude-codeis idempotent on re-run (skipped count reflects both tools).pilot --tool codex,claude-code --jsonreturns the expected multi-tool shape.--tool bothproduces the deprecation warning on stderr and the same files as--tool opencode,openclaude.--tool manual,codexexits non-zero with the three-line error.
10. Docs & CHANGELOG
- CHANGELOG
[Unreleased]:- Added: multi-tool
--toolsupport across setup / tools adapt / pilot. - Changed (breaking):
routing.json,project.json, and the pilot JSON output no longer carry a singulartoolkey — onlytools(a list). TheIntakeAnswers.toolPython attribute is also removed. The reasoning: user base small enough that one breaking change is cheaper than two parallel shapes. - Deprecated:
--tool both; will be removed in 0.7.0.
- Added: multi-tool
- Glossary entry: "multi-tool project — a project with more than one
coding tool configured. Generated via
setup run --tool <a> --tool <b>." - Getting-Started.md: add a "Two tools in one repo" subsection under "Smallest Useful Path" showing the canonical invocation and what's written.
- Upgrading.md: add a "0.6.0 Breaking" block describing the
routing.json/project.json/ pilot-JSON shape change. Add a "0.7.0 Breaking" block (drafted now, activated when the removal lands) describing thebothremoval and replacement command. Both blocks point at the one-line migration: readtools[0]where you used to readtool. - Tool-Adapters.md: mention multi-tool in the capability matrix preamble.
Add an example showing
--tool codex,claude-code. - Doc audit (must check before merge, not just write): grep
docs/docs/wiki/and the README for any literal mention of a"tool":JSON key in routing/project context. Current audit finds none, but the audit must be re-run before merge so a doc added between brainstorm and implementation doesn't sneak the old shape back in.
11. Open questions
None at design time. Confirmed in brainstorm:
- ✅ Pilot honors multi-tool (
--tool a,bprints both). - ✅
bothdeprecated in 0.6.0, removed in 0.7.0. - ✅
manual + real-toolis invalid (rejected at parse time). - ✅ Default behavior when
--toolis omitted entirely is unchanged (opencode).
12. Estimated scope
Total ≈ 450 lines across one PR. Single-day implementation expected.