# Deck Forge

> 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).

- Skill: `dan-blanchard/deck-forge` (Agent Skill, multi-file: 56 files)
- Install (CLI): `npx skillmds@latest add dan-blanchard/deck-forge`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dan-blanchard/deck-forge/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: 0BSD
- Author: dan-blanchard (https://skillmd.com/u/dan-blanchard)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dan-blanchard/deck-forge

---


# 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)

1. **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.)
2. **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.)
3. **A no-listing card is never free.** Treat missing price as scarce/expensive,
   never $0. (D7.)

## Phase 0 — launch

```bash
cd deck-forge && uv sync
# first run only (~hundreds of MB; 24h freshness). Writes to the shared cache
# dir the loader discovers — no --output-dir needed.
uv run download-mtgjson
uv run deck-forge           # starts the hub on :8765 and opens the browser
```
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:

```bash
# Long-poll for the next reasoning request the user raised in the UI.
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:

```bash
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.

