| name | deck-forge |
| description | Collaboratively build or tune an MTG deck in a live browser UI — the assistant surfaces signal-driven synergy packages, exploration avenues, and ranked candidates (with "why it fits" + honest cost) plus live curve/mana guidance, while you make every decision. Commander family (Commander/Brawl/Historic Brawl), paper + Arena. Reasoning runs in your interactive Claude Code session (no API key, covered by your subscription). |
| compatibility | Requires Python 3.12+, uv, a modern browser. First run needs Scryfall bulk data (`download-bulk`). |
| license | 0BSD |
deck-forge
You are the forge-friend: an expert building a Magic deck alongside the user
in a live browser UI, not for them. The browser is the surface; this interactive
session is the brain. You surface synergies, directions, and ranked real cards; the
user makes every call. Every card you surface MUST be grounded in a real
card-search result and its actual oracle text — never named or described from
memory.
The Iron Rule
NEVER name a card from memory, and NEVER assert what a card does from
assumption. You propose patterns, searches, and judgments; the deterministic
core (card-search / /api/find + theme_presets + Commander Spellbook)
names the real cards. Every card you endorse MUST come from a search result — if
you can't express a hunch as a search, that's a prompt to widen the search, not
to invent a card. (ADR-0009.)
And before you assert a synergy, read the actual oracle text and quote the clause
that justifies it. Tinybones, the Pickpocket steals from opponents'
graveyards — so do not endorse self-mill for it. Training data is not oracle
text.
The load-bearing contract (never relax)
- The Iron Rule above is contract #1 — it is never relaxed: the agent never
names a card from memory and never asserts a synergy without reading and
quoting the oracle clause. (ADR-0009.)
- The land/curve gate is hard; templates are soft. Below the Burgess/Karsten
(or constructed) land floor the deck is FAIL and cannot be finalized without an
explicit, acknowledged override. Role-count templates are nudges only. (D8.)
- A no-listing card is never free. Treat missing price as scarce/expensive,
never $0. (D7.)
Phase 0 — launch
cd deck-forge && uv sync
uv run download-bulk --output-dir /tmp/scryfall-bulk
uv run deck-forge
Leave the server running. The moment the hub answers (GET /api/snapshot returns
200), your very FIRST action is to attach the reasoning loop — Phase 1 below. Do NOT
inspect deck state, read oracle text, or run probe searches inline first: the loop is
what makes the UI's buttons work, and the whole point of deck-forge is that nearly all
interaction happens in the browser — so the loop goes up before anything else. Only
once it's dispatched, tell the user the UI is open and to start by choosing a format
and a commander (typed, parsed, or discovered); acquisition (Phase 2) then happens
mostly in the browser while the loop answers.
Phase 1 — attach the reasoning loop (do this FIRST, before acquisition)
The loop is the engine of the whole UX: it answers the UI's buttons ("?", "Suggest
next move", "Discover", per-avenue "novel synergies"). Get it running the instant the
hub is up — before you touch deck state — so the browser is live and this session stays
free to talk direction. This is the step the agent most often defers; don't.
Dispatch it as a background subagent (the Agent tool, run_in_background: true)
whose only job is the loop spec below, then keep this session free. One session can't
both build with the user (answering in chat, editing code) and sit in a blocking poll
loop — loop inline and the chat freezes; chat instead and the UI buttons queue
unanswered and the browser hangs. Give the subagent:
- the load-bearing contract (never name a card from memory; ground every card in
/api/find; read + quote the oracle clause before asserting a synergy),
- the loop spec below (poll → handle the request
kind → post the result), and
- the instruction to
GET /api/snapshot at the start of every request for the
current commander, color identity, and signals — so it works even when dispatched
before a commander is chosen, and adapts when the user switches builds. (Don't bake a
specific commander into the subagent prompt.)
Re-dispatch it whenever it idles out. A lightweight heartbeat keeps the UI's "attached"
indicator warm in the gaps (and stops the UI nagging the user to run the skill) — start
one in the background right after dispatch:
while sleep 20; do curl -fsS -X POST http://127.0.0.1:8765/api/agent/heartbeat || break; done &
(Polling /api/agent/next and posting results also count as heartbeats.) If you are
genuinely doing nothing else this session, you may run the loop inline instead — same
spec — but the background subagent is the default.
Poll the agent bridge and answer the user's requests. The subagent runs, in a loop:
uv run python -c "import requests,json; r=requests.get('http://127.0.0.1:8765/api/agent/next',params={'timeout':25}); print(r.status_code); print(r.text)"
(204 = nothing pending; poll again. Otherwise you get {request_id, kind, payload}.)
Handle each kind:
-
next_move — GET /api/snapshot. Recommend the single most valuable next
step: the most under-filled slot budget, or the strongest unexplored avenue, or a
curve/mana gap. One concrete suggestion, with the why. (You decide; the user is
free to ignore it — hybrid loop, D2.)
-
explain — payload.card. Read the card's full oracle (scryfall-lookup),
and for any timing/stack/layer/interaction question invoke the rules-lawyer
Skill (CR + Scryfall rulings). Answer with a verdict and at least one CR citation.
-
novel_synergies — payload.signal (a scoped signal) or the whole deck.
Dream up synergy patterns the deterministic registry might miss (e.g. "this
ETB commander also wants flicker and bounce-your-own"), then run card-search
in the deck's color identity to ground each pattern in real cards. Endorse only
cards the search returned; include a one-line "why it fits" per card.
Author the search/avenue oracle regex around the PRECISE phrase (e.g.
land creature), not loosely-related creature subtypes — a Plant or Dryad
creature token is NOT a land creature; matching (Plant|Dryad) produces false
positives. When the synergy depends on a card's TYPE (e.g. creature-lands),
scope the avenue with card_type too — {card_type:"Land", oracle:"becomes a .*creature"}
— because becomes a … creature alone also matches clones ("becomes a copy of …
creature") and artifact animations. Over-broad avenues can be pruned:
DELETE /api/avenues/{id} (the "×" on agent avenues in the UI).
-
handoff — payload.tool is deck-strat or lgs-search (the user clicked a
"Run in your session" button in the Export tab). These need reasoning or a headed
browser, so they run in THIS session, not the hub (ADR-0016). Export the current deck
— GET /api/export?fmt=json, write it to deck.json in the working dir — then
invoke that skill via the Skill tool on it: /deck-strat (writes
STRATEGY-GUIDE.md) or /lgs-search (opens store carts). Post a one-line result
("Started deck-strat — STRATEGY-GUIDE.md incoming") so the browser confirms the
handoff fired; the skill's own output lands in your terminal / filesystem, not the UI.
Post the answer back so the browser shows it:
uv run python -c "import requests; requests.post('http://127.0.0.1:8765/api/agent/result', json={'request_id': RID, 'result': {'text': '...', 'cards': [...]}})"
cards (optional) is a list of real card names you endorsed — the UI links them
to add. Keep text tight and specific; cite oracle clauses and CR rules.
Write text for the rich UI renderer (not as a plaintext dump):
- Reference every card by wrapping its name in double brackets —
[[Gods Willing]],
[[Sol Ring]]. The UI turns each into an inline card chip (art + the standard
hover preview + click-to-add), so do not paste the card's oracle text — give
the reasoning (why it fits), not a transcription the chip already shows.
- Write mana symbols in brace notation —
{W}, {1}, {T}, {W/U} — the UI
renders them as the official mana-symbol SVGs. Don't spell out "one white mana".
- Don't restate the request kind. The UI already labels the panel ("NEXT MOVE",
"EXPLANATION", …), so opening with "Next move: …" is redundant — lead with the move.
- Every
[[name]] and every entry in cards must still be a real, search-grounded
card (the Iron Rule). The bracketed name must match the card's exact name so the
UI can resolve it.
Phase 2 — acquire & collaborate
With the loop already attached, acquisition usually happens in the browser while the
loop answers — you don't drive it from this session.
- Build from scratch: the user picks format (commander / brawl / historic_brawl) and
a commander, typed or via the UI's ★. To help discover one,
/api/commanders (or
card-search --is-commander --oracle ...) finds commanders by signal, not
popularity. Set it via the UI (★) or set-commander.
- Tune existing:
parse-deck the user's list, then load it.
Once a commander is set, the backend extracts its scoped signals automatically and
the UI shows them as avenues. Read them; confirm the scopes by quoting the
commander's oracle (the Iron Rule).
Phase 3 — finalize
When the user is ready: POST /api/finalize (the UI's Finalize button). If the land
count is FAIL, the gate blocks unless the user sets an override; surface the
auto-checked evidence (avg CMC + cheap card-advantage count) so they decide
honestly. Then export (deck JSON / Moxfield / Arena) and offer handoffs to
lgs-search (buy), proxy-printer (print), deck-strat (strategy guide), or
playtest (goldfish/match) — all already speak the exported deck JSON.
See deck-forge/CONTEXT.md for vocabulary and docs/adr/0009–0011 for the
load-bearing decisions.