Wizard: Giving Claude Code a Real Engineering Process
Ask an AI coding agent to "fix the bug" and it will fix a bug — usually the
one nearest the surface, in whatever file it opens first. It rarely stops to
ask whether the fix addresses the actual requirement, whether a sibling
caller has the same problem, or whether the change is something a reviewer
would sign off on before it ships. That's not a model failure so much as a
missing process: humans don't skip design and review because we're smarter
than that in the moment, we skip it because nobody built the discipline into
the loop.
Wizard is an attempt to build that discipline into the loop — a Claude Code
plugin that turns a single free-text request into a small software team's
worth of process: a business analyst who writes the spec, an architect who
turns it into a task-owned plan, a bench of specialists who each implement
their piece, and a QA lead who renders a final verdict against the plan's
own definition of done. All of it running as Claude Code agents, all of it
gated by you at the two points that matter most.
The core idea: spec → plan → code → review, with real stop points
Every plain /wiz <task> invocation runs through four phases:
-
Spec (Blake). A business-analyst agent reads the task, queries
whatever knowledge sources are available about your codebase, and writes
a
spec.md— problem, proposed solution, explicit non-goals, risks, and Given/When/Then acceptance criteria. No implementation detail yet. - Gate 1. You read the spec and approve, ask for changes, or cancel. The orchestrator does not proceed without an explicit answer — this isn't a rhetorical "does this look right?" the model can talk past, it's a real stop.
-
Plan (Archie). An architect agent turns the approved spec into
plan.md: a concrete Definition of Done, a file map, and a numbered task list where each task carries a suggested owner — one of twelve specialist roles, scored against trigger keywords (.java/spring→ the Java developer,CSS/accessibility→ the UI developer,CVE/OWASP→ security, and so on). The orchestrator re-scores every task itself afterward, deterministically, so owner assignment never depends on how convincingly the architect argued for a role. - Gate 2. Same approval mechanism, now against the plan's Definition of Done and finalized task list.
-
Code phase. Each task gets its own specialist, spawned with the full
spec, the full plan, and one explicit instruction: if the task
contradicts the plan or the actual codebase, don't improvise — report
SPEC_DRIFTand let a human decide whether the plan or the spec needs to change. Silent improvisation is exactly the failure mode this whole pipeline exists to prevent. -
Final QA (Quinn). Once every task is checked off, a QA agent
independently re-verifies the Definition of Done against the actual
repository state — not the task list's checkmarks — and renders
PASS,CONCERNS, orFAIL. AFAILreopens exactly the failing tasks; it doesn't restart the whole pipeline.
For a genuine one-liner, /wiz quick <task> skips all of this: one
knowledge query, one complexity estimate, one specialist, done. The
mandatory pipeline is the default because most tasks aren't one-liners, even
when they look like one at 9pm.
Twelve specialists, one routing table
Wizard doesn't have a single "coder" persona — it has an architect, a senior
developer as the generalist fallback, and dedicated specialists for Java,
Angular, React, Python, UI/CSS, DevOps, QA, business analysis, database
work, and security. Routing is keyword-scored against one fixed trigger
table, shared by the quick-mode router and the plan phase's task-owner
assignment — a task never gets routed one way in one mode and a different
way in another:
| Role | Triggers |
|---|---|
architect |
design, architecture, how should we, system, trade-off, ADR, strategy |
seniorDev |
implement, refactor, review, PR, pull request, code review |
javaDev |
.java, spring, springboot, maven, gradle, hibernate, JPA, @Bean, @Service
|
angularDev |
.component.ts, .module.ts, angular, @Component, @NgModule, rxjs |
uiDev |
CSS, SCSS, HTML, template, styling, layout, accessibility, Tailwind |
reactDev |
react, jsx, tsx, useState, useEffect, Next.js, hook |
pythonDev |
.py, django, fastapi, flask, pytest, pandas, pip |
devops |
Dockerfile, k8s, pipeline, CI, CD, Helm, Terraform, kubectl, yaml |
qa |
test, spec, assertion, bug, regression, JUnit, Jest, coverage |
ba |
requirement, user story, acceptance criteria, business rule, stakeholder |
dba |
SQL, schema, migration, index, stored procedure, Flyway, Liquibase |
securityEng |
vulnerability, CVE, OWASP, auth, token, injection, XSS, pentest |
Ties resolve architect > seniorDev > any specialist; nothing matching
falls back to seniorDev. It's a small detail, but it's the difference
between "an AI wrote this" and "the person who actually knows Spring Boot
wrote this."
Context that isn't just "paste the file"
Before any routing decision, Wizard queries a layered knowledge pipeline:
a pure-Java unified index (LuceneDb) covering both documentation and code
structure, falling back to a graphify code graph if the index hasn't been
built yet, plus a relational knowledge graph for typed entity relationships,
merged and weighted into a single context block. If none of that is
available, it falls back to a plain grep. The point isn't sophistication for
its own sake — it's that a spec written with zero awareness of the existing
codebase is a spec that will drift the moment implementation starts.
The merge logic itself is a short, readable cascade — each source is tried
in priority order, and only contributes if it actually returns something:
def retrieve(task_summary, working_dir, config=None):
"""Run all enabled knowledge sources and return a merged context string."""
cfg = config or {}
wd = Path(working_dir)
budget = int(cfg.get("maxContextTokens", _DEFAULT_BUDGET))
weights = {**_DEFAULT_WEIGHTS, **cfg.get("weights", {})}
parts = [] # (source_name, content, weight)
# Source 0: LuceneDb — preferred when built, no Node/npm/network needed.
lucenedb_used = False
if cfg.get("lucenedb", {}).get("enabled", True) and _lucenedb.available():
result = _lucenedb.query(task_summary, lucenedb_index_path, limit=10)
if result:
parts.append(("lucenedb", result, weights.get("graph", 0.5) + weights.get("qmd", 0.3)))
lucenedb_used = True
# Source 1: graphify code graph — automatic fallback if LuceneDb isn't built.
if not lucenedb_used and _graphify.available(graph_path):
graph_budget = int(budget * weights["graph"])
result = _graphify.query(task_summary, graph_path, budget=max(500, graph_budget))
if result:
parts.append(("graphify", result, weights["graph"]))
# Source 2: relational knowledge graph — typed entity links.
if rel_cfg.get("enabled") and _relational.available(rel_path):
rel_budget = int(budget * weights["relational"])
edges = _relational.query(_extract_terms(task_summary), rel_path, limit=10)
formatted = _relational.format_results(edges, budget_chars=rel_budget)
if formatted:
parts.append(("knowledge-graph", formatted, weights["relational"]))
# Last resort: plain grep, no index required at all.
if not parts:
grep_results = _grep_fallback(task_summary, wd, cfg.get("fallbackGlob", "**/*.{md,txt,java,ts,py}"))
if grep_results:
parts.append(("grep", grep_results, 1.0))
if not parts:
return ""
return "\n\n".join(
f'<knowledge_context source="{source}">\n{content}\n</knowledge_context>'
for source, content, _ in parts
)
No index built yet on a brand-new checkout? You still get grep results.
LuceneDb built? It wins by default weight. Nothing ever hard-fails just
because one source isn't available.
A case study in eating your own dog food
This plugin was itself repackaged out of an older, looser setup: a skill
file under ~/.claude/skills/wizard, a folder of agent definitions, and a
standalone Python wizard repo that supplied the knowledge-retrieval code.
The repackaging was supposed to make the plugin fully self-contained —
install it, and it runs, no sibling repo required.
It didn't, quite. A routing exercise (asking Wizard itself, in quick mode,
whether the new plugin would work without the old repo present) turned up
three separate breakages, each more interesting than the last:
- A stale absolute path. The plugin's knowledge-query step had a path hardcoded that didn't exist on the machine in question at all, because the source repo had since moved:
- sys.path.insert(0, 'C:/projects/wizard')
+ sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/scripts/wizard')
Wrong in two ways at once: pointing outside the plugin, and pointing at
a location that was already gone. ${CLAUDE_PLUGIN_ROOT} — the same
variable the plugin's other scripts already used correctly — resolves
inside the plugin's own install directory, wherever that happens to be.
-
An install script referencing the wrong folder name. The skill had
been renamed from
skills/wizard/toskills/wiz/during the repackaging, butinstall.shstill copied from the old name:
- cp "$REPO_DIR/skills/wizard/SKILL.md" "$SKILLS_DIR/wizard/SKILL.md"
+ cp "$REPO_DIR/skills/wiz/SKILL.md" "$SKILLS_DIR/wizard/SKILL.md"
It would have failed the moment anyone actually ran it. The same script
also checked for top-level knowledge/, retrieval/, wizard/
directories that no longer existed at that level — they'd been nested
under scripts/wizard/ during the repackaging:
- if [ -d "$REPO_DIR/knowledge" ] && [ -d "$REPO_DIR/retrieval" ] && [ -d "$REPO_DIR/wizard" ]; then
- ln -sfn "$REPO_DIR/knowledge" "$WIZARD_LIB_DIR/knowledge"
- ln -sfn "$REPO_DIR/retrieval" "$WIZARD_LIB_DIR/retrieval"
- ln -sfn "$REPO_DIR/wizard" "$WIZARD_LIB_DIR/wizard"
+ if [ -d "$REPO_DIR/scripts/wizard/config" ] && [ -d "$REPO_DIR/scripts/wizard/retrieval" ] && [ -d "$REPO_DIR/scripts/wizard/wizard" ]; then
+ ln -sfn "$REPO_DIR/scripts/wizard/config" "$WIZARD_LIB_DIR/config"
+ ln -sfn "$REPO_DIR/scripts/wizard/retrieval" "$WIZARD_LIB_DIR/retrieval"
+ ln -sfn "$REPO_DIR/scripts/wizard/wizard" "$WIZARD_LIB_DIR/wizard"
Silent, not loud: the original condition just evaluated false and skipped
the block, so install.sh never actually crashed — it just quietly never
linked the knowledge modules it advertised, which is arguably worse than
a hard failure.
-
A missing package, not just a missing path. The deepest issue:
retrieval/unified.pyimportsretrieval/relational.pyunconditionally at module load —
from . import graphify as _graphify
from . import lucenedb as _lucenedb
from . import relational as _relational
— and relational.py needs a KnowledgeGraph class:
try:
from ..knowledge.graph import KnowledgeGraph
except (ImportError, ValueError):
from knowledge.graph import KnowledgeGraph
Both branches assume a knowledge package sits somewhere reachable — as
a parent-package sibling, or as a top-level package on sys.path. Neither
was true: the entire knowledge/ directory (graph.py, extractor.py,
build.py, five files total) had simply never been copied into the
plugin's bundled Python tree during the carve-out. Fixing the path alone
would have traded one ModuleNotFoundError for another the instant
anything tried to import retrieval.unified. The fix required going back
to the original repo, confirming the missing package had no further
hidden dependencies of its own (a quick grep for its imports — only
pathlib, re, json, dataclasses, and its own relative imports came
back), and copying it in as a sibling of the packages that were already
there:
scripts/wizard/
├── config/
├── retrieval/
├── wizard/
└── knowledge/ <- added: graph.py, extractor.py, build.py, __init__.py, __main__.py
The lesson generalizes past this one plugin: "it imports correctly" and "the
path string points somewhere plausible" are different claims, and only one
of them is verifiable by reading the text of the code. The other one you
have to actually trace, import by import, or run.
Getting started
claude --plugin-dir /path/to/wizard-plugin
/wizard:wiz setup
setup walks through working directory, model tier preferences, and which
specialist roles you want enabled. After that:
/wizard:wiz <task> # the full spec -> plan -> code -> review pipeline
/wizard:wiz quick <task> # skip the gates for a genuine one-liner
/wizard:wiz knowledge status
Runtime configuration lives at ~/.wizard/config.json, outside the plugin
itself, so upgrading the plugin never touches your working-directory or
model-tier preferences.
Why bother with all this ceremony
Because the ceremony is the point. A model that writes code without a spec
will happily write code that solves the wrong problem, quickly and
confidently. A model that writes code without a plan will happily improvise
around a codebase constraint it hasn't actually checked. A model that grades
its own homework will mark it correct. Wizard doesn't make Claude smarter —
it makes the process harder to shortcut, which turns out to matter more.
Top comments (0)