Architecture
UltraCode Goal is a conductor. It orchestrates the installed BMAD epic toolbox and the TEA gates, composing Claude Code primitives (/goal, Auto Mode, Auto Memory, hooks, git branch isolation) and replaces none of them. This page covers the conductor model, the three enforcement layers in depth, the file layout, customization resolution, and why the hooks live where they do.
The conductor model
Section titled βThe conductor modelβThe skill owns no implementation logic of its own for building features or running tests. What it owns is the order, the gates, and the enforcement. It delegates:
- Epic toolbox:
bmad-sprint-planning,bmad-create-story,bmad-dev-story,bmad-code-review,bmad-correct-course,bmad-retrospective. - TEA gates:
bmad-testarch-framework,-ci,-test-design,-atdd,-automate,-test-review,-nfr,-trace. - Claude Code primitives: the
/goalloop drives execution; Auto Mode and ultracode session effort make the unattended run possible; Auto Memory carries learnings forward; hooks enforce invariants; git branches provide isolation and rollback.
Because it is a conductor, the truth of βis this doneβ lives in the artifacts its delegates produce, not in the conductorβs own reasoning. That is the whole design: the model arranges the work, but a script reads the verdict.
The three enforcement layers
Section titled βThe three enforcement layersβThese are the moduleβs non-negotiables. Each exists because the documented mechanics make the intuitive shortcut wrong (see why).
1. Deterministic gate truth
Section titled β1. Deterministic gate truthβscripts/gate_eval.py reads TEAβs gate-decision.json and maps its gate status to a routing verdict (PASS/WAIVED β advance, CONCERNS β defer, FAIL β reloop, NOT_EVALUATED β escalate). It never re-derives TEAβs thresholds and never reads the transcript. The /goal evaluator that drives execution can only see what the run surfaces, it cannot open the gate file, so it is structurally incapable of being the completion authority. The script is. In production, two extra signals can only downgrade an advance, never lift a lower verdict; in both profiles a scope=scoped tests-ran marker caps a story-moving verdict at reloop, so a scoped re-loop sweep can never advance a story. See the gate model for the full mapping diagram, thresholds, and the fail-closed contract.
2. Hooks as invariants
Section titled β2. Hooks as invariantsβA set of invariants must hold before the runtime lets a tool call through, and none of them can live in memory, which is context the model may or may not weigh:
scripts/hooks/guard_pretooluse.py(PreToolUse): inspects eachgit commit/git pushand enforces six invariants. It denies the command on a protected branch; denies agit commitwhen no tests-ran marker (<impl-artifacts>/.tests-ran-<story_id>) exists for the current story; denies it when that markerβsbaseline=<sha>does not match the SHA the story recorded at start; denies it when the staged index is empty or unreadable; and denies it, when armed withULTRACODE_TEST_ARTIFACTS, while the staged content of a checklist-enumerated acceptance test still containstest.skip(. A sixth invariant is not about git at all: it runs for every tool call, and while a run is active it denies claude-mem MCP calls and filesystem reach into.claude-memunless the Cross-Session Recall latch is green. It returns adenydecision in the hook JSON and also exits 2 with the reason on stderr so older clients that ignore the JSON still block. The marker, freshness, staged-index, and recall-latch checks fail closed, denying on an input they cannot read. Two do not: the protected-branch check fails open whengit rev-parsecannot report a branch, and the un-skip proof is out of scope entirely whenULTRACODE_TEST_ARTIFACTSis unset. The protected-branch check is also scoped to the sessionβs working directory rather than the repository the command names, so it denies agit commit/git pushaimed at another checkout whenever the sessionβs own repo is on a protected branch.scripts/hooks/budget_stop.py(Stop): counts turns for the current story againstmax_turns_per_story. On overrun it writes an escalation marker and surfaces a message, then lets the stop proceed. Its documented limitation: a Stop hook fires only when Claude is already trying to stop, so it cannot interrupt a/goalcondition mid-turn. The in-condition βstop after N turnsβ clause and the gateβs re-loop budget are the real bounds; this hook is the third, defensive layer.
Both hooks read their config from env first (so the conductor injects per-run values) and fall back to hardcoded defaults (main/master, 25, ultracode/epic-). Because of that fallback, a customize.toml override silently no-ops at the enforcement layer unless the conductor passes it through the hook env, so preflight injects ULTRACODE_PROTECTED_BRANCHES, ULTRACODE_IMPL_ARTIFACTS, ULTRACODE_MAX_TURNS, ULTRACODE_EPIC_BRANCH_PREFIX, and ULTRACODE_TEST_ARTIFACTS.
3. Budget enforcement
Section titled β3. Budget enforcementβA runaway story is bounded by three layers in order of authority: the in-condition ββ¦or stop after N turnsβ clause inside the /goal condition (the real in-loop bound), the gate re-loop budget (a reloop that would exceed max_turns_per_story becomes escalate), and the Stop hook as the defensive backstop described above. Rollback is git, not /rewind (an Epic branch off a protected branch, one commit per green story) because /rewind checkpoints miss the Bash-driven changes that make up the run.
File layout
Section titled βFile layoutβThe skill routes from a thin entry point down to just-in-time stage files, deterministic scripts, and an experimental asset:
skills/ultracode-goal/βββ SKILL.md # Entry point: overview, conventions, run modes,β # non-negotiables, the 6-stage table, headless contractβββ customize.toml # Config base layer (the [workflow] block)βββ references/ # Loaded just-in-timeβ βββ ingest-and-scope.md # Stage 1β βββ preflight.md # Stage 2 (the autonomy gate)β βββ define-done.md # Stage 3β βββ execute.md # Stage 4β βββ gate.md # Stage 5β βββ finalize.md # Stage 6β βββ health-check.md # Finalize self-improvement reflectionβββ scripts/ # Deterministic truth (run via `uv`)β βββ preflight_check.py # mechanical preflight facts + blocker budgetβ βββ gate_eval.py # gate status -> verdict (the completion authority)β βββ gate_trail.py # per-story evidence trail (gate-trail.md) at finalizeβ βββ formalize_check.py # readiness kernel behind the /ucg-formalize gateβ βββ status_render.py # read-side render behind /ucg-statusβ βββ red_ids.py # preflight-RED identity (the resolve join key)β βββ story_sizing.py # decomposition sizing + parent-AC claim reconciliationβ βββ drive_epic.py # one `claude -p` per story (--max-stories work bound)β βββ headless_envelope.py # the one five-key headless-envelope adapterβ βββ health_check_fp.py # health-check fingerprint + seen-cache plumbingβ βββ mem_observation.py # Cross-Session Recall write path (build/spill/drain)β βββ mem_recall.py # Cross-Session Recall read path (latch + filter)β βββ merge_config.py # install-time config merge into shared _bmadβ βββ merge_customization.py # install-time UCG-awareness fragment mergeβ βββ merge_help_csv.py # install-time help-CSV mergeβ βββ lib/mem_common.py # shared Cross-Session Recall primitivesβ βββ hooks/β βββ guard_pretooluse.py # commit invariants (PreToolUse)β βββ budget_stop.py # turn budget (Stop)βββ assets/ βββ module.yaml Β· module-setup.md Β· module-help.csv # install metadata βββ ucg-awareness/ # shift-left planning customization fragments
skills/ucg-formalize/ # Standalone readiness gateskills/ucg-resolve/ # Decide-surface for a blocked or escalated runskills/ucg-status/ # Read-only status view over a run # Top-level siblings of `ultracode-goal`, not children: # the IDE's skill loader enumerates one level, so a # nested command never resolves. Each stays thin and # reaches back into the parent via `{ucg-root}`.SKILL.md carries the routing and the contract; the references/*.md files carry each stageβs procedure and testable routing conditions; the scripts/*.py files carry the deterministic facts the model cannot fudge (the gate, preflight, readiness kernel, hooks, and the install-time/recall plumbing); the top-level sibling skills skills/ucg-* are the operator-facing surfaces (ucg-formalize the standalone readiness gate, ucg-resolve the decide-surface for a stopped run, ucg-status the read-only run view); and the assets/ hold install metadata and the planning-customization fragments. See how it works for the stages.
Customization resolution
Section titled βCustomization resolutionβConfiguration resolves in three layers, base β team β user, via resolve_customization.py:
-
Base:
customize.tomlin the skill root (the shipped[workflow]block). In an installed project that file is{project-root}/_bmad/ucg/ultracode-goal/customize.toml, a verbatim copy of the shipped one; it is the layer to read (every knob is there with its comment), not the layer to edit. -
Team:
{project-root}/_bmad/custom/ultracode-goal.toml, committed. -
User:
{project-root}/_bmad/custom/ultracode-goal.user.toml, gitignored by the*.user.tomlrule that ships in_bmad/custom/.gitignore.Neither override file is created by any installer, and a missing one is not an error: the resolver loads it non-required and treats absence as an empty table, so a fresh project resolves the base layer alone. You create them when you first need to override something. Do not mistake them for the
config.tomlandconfig.user.tomlalready sitting in that directory: those are BMADβs own configuration, and a UCG knob written there resolves to nothing.
Merge semantics: scalars override, tables deep-merge, arrays append. At activation the skill runs resolve_customization.py --skill {skill-root} --key workflow; if that fails, it resolves the three files itself in the same order. The shipped base layer defines the runβs knobs: the TEA/artifact paths (tea_config_path, trace_output_dir, implementation_artifacts, deferred_work_path), the git guardrails (epic_branch_prefix, protected_branches), the turn budget (max_turns_per_story; story_token_budget remains as a deprecated no-op key), parallel_max_concurrency (deprecated no-op since the --parallel retirement), the allowlist_commands, the two lifecycle hooks (on_epic_complete, on_escalation), the health check family (health_check_repo, health_check_seen_cache, health_check_queue_path, health_check_autosubmit), and the two optional third-party integrations, both off by default: cross_session_recall and graphify_integration. Teams and users override without editing the shipped file. Remember that a budget or branch override only reaches the enforcement layer because preflight threads it into the hook env (see layer 2 above).
The three TOML layers merge once, but a branch or budget value then travels two ways: the conductor reads it directly, while the hooks only see it if preflight re-injects it as env:
flowchart LR
BASE["Base - customize.toml"]
TEAM["Team - ultracode-goal.toml"]
USER["User - ultracode-goal.user.toml"]
BASE --> RES["resolve_customization.py merges base then team then user"]
TEAM --> RES
USER --> RES
RES --> WF["resolved workflow block"]
WF -->|"conductor reads scalars directly"| COND["conductor stages"]
WF -->|"preflight injects ULTRACODE_* env"| HOOKS["PreToolUse + Stop hooks"]
HOOKS -. "no env injected, falls back to defaults" .-> DROP["override no-ops at enforcement"]
classDef accent fill:#6366F1,stroke:#4F46E5,color:#fff
class WF accent
Why the hooks live in settings.local.json
Section titled βWhy the hooks live in settings.local.jsonβThe PreToolUse and Stop hooks are auto-merged into {project-root}/.claude/settings.local.json (machine-local, gitignored, honored after the workspace trust dialog), not into a committed settings file or memory. The reasoning: these hooks are enforcement, not context. A committed hook would impose this moduleβs commit guard on every contributor and every unrelated session in the repo; a hook in memory would not block a commit at all. The machine-local file scopes enforcement to the machine actually running the unattended Epic, and the gitignore keeps it out of shared history. The skill re-merges them every run (idempotently) and asserts they are active before the run goes unattended; it does not assume a prior run left them in place. Because the file is machine-local and executes on your machine, review what is merged; see SECURITY.md.