DEV Community

Cover image for Giving Claude Code a Real Engineering Process
Mallikarjun H T
Mallikarjun H T

Posted on Originally published at dev.to

Giving Claude Code a Real Engineering Process

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:

  1. 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.
  2. 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.
  3. 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.
  4. Gate 2. Same approval mechanism, now against the plan's Definition of Done and finalized task list.
  5. 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_DRIFT and 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.
  6. 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, or FAIL. A FAIL reopens 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
    )
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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/ to skills/wiz/ during the repackaging, but install.sh still 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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.py imports retrieval/relational.py unconditionally at module load —
  from . import graphify as _graphify
  from . import lucenedb as _lucenedb
  from . import relational as _relational
Enter fullscreen mode Exit fullscreen mode

— and relational.py needs a KnowledgeGraph class:

  try:
      from ..knowledge.graph import KnowledgeGraph
  except (ImportError, ValueError):
      from knowledge.graph import KnowledgeGraph
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)