Skip to content
elliottshortPublic

About

Architectural plans (PDF first, DWG optional) to a verified Blender 5.2 building model, as a Claude Code plugin. Named after Ted Mosby.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

9 Commits

Folders and files

Repository files navigation

Mosby

Architectural plans (PDF first, DWG when available) to a verified Blender 5.2 building model, as a Claude Code plugin. Named after Ted Mosby, the architect in How I Met Your Mother.

Mosby is a harness, not a magic converter: it gives the model a phase plan with computable definitions of done, a mosby CLI whose audits write the reports that define "done", hooks that stop the most expensive mistakes mechanically, and skills that carry the method. The method was distilled from one real job: a two-level house that took 119 correction iterations, 310 scripts and three owner-rejected "deliveries" before it was acceptable. Mosby exists so the next job does not.

Be clear about what is enforced and what is advised. Enforced (hooks, no opinion needed): generated files (plan*.json, reports/*.json, status.json, ...) cannot be hand-edited; every Blender snippet is identity-checked against the job's file/scene and auto-checkpointed; headless builds cannot start while the P0/P1 gates are unpassed; the session cannot end on a "delivered / complete / verified" claim that status.json does not back, nor after three unobserved mutations. Advised (skills and the CLI's audits): everything else, which audit to run when, how to read a sheet, how to seat a fixture, how to word a finding. Advice is followed by a model that has read it; enforcement is not optional. The design is summarised under "Design" below; the words a finding or status may use are the claim table in skills/mosby/reference.md (section H).

Status

v0.1.0. Every CLI module listed in tools/mosby/constants.py (CLI_COMMANDS) exists and the 17 test suites pass (10 Python, 7 inside headless Blender). What has been proven end to end on a real plan set is the front half: the P0 (survey) and P1 (extraction) gates pass on a 17-sheet AutoCAD PDF with no hand edits to any generated file (numbers under "Quickstart"). The shell, finishes, interior, review and delivery stages have unit and Blender-side tests with planted defects but have not yet been driven through a full job under the harness. Known gaps: P2 reports go stale when a later build changes model.blend (whole-file hash binding), camera and lighting finding classes do not block, and mosby build finishes is a stub. Expect to iterate; that is what the repo is for.

Install

Requirements: Python 3.11+, Blender 5.2 LTS on PATH (or set MOSBY_BLENDER to the executable), the Blender MCP add-on running in the Blender you intend to drive, Claude Code.

git clone https://github.com/elliottshort/mosby.git
pip install -e mosby/tools                     # installs the `mosby` CLI and library
claude --plugin-dir mosby                      # loads hooks, skills and agents for this session
blender --version                              # must resolve; or set MOSBY_BLENDER

Blender MCP: one MCP-enabled Blender per job. If two Blenders run on one machine they must not share a port; record the port in job.json (blender.port). The house run edited the wrong instance twice because both listened on 9876.

Optional: pip install -e "mosby/tools[dwg]" for the DWG front end (needs the dwg-mcp server), [dev] for pytest.

Quickstart (one job, PDF only)

The sequence below is the one that took the house PDF (17 sheets, no DWG) through the P0 and P1 gates with no hand edits to any generated file. Replace the page numbers, rows and values with your own; every number here was read off the sheets or measured by the commands, never assumed.

mosby init jobs/myhouse --pdf C:/plans/HOUSE.pdf --levels L1
mosby pdf probe  C:/plans/HOUSE.pdf --job jobs/myhouse          # census per page -> reports/pdf_probe.json (route: layered_vector)
mosby pdf sheets C:/plans/HOUSE.pdf --job jobs/myhouse          # sheet kinds + regions -> reports/sheet_index.json
mosby pdf to-entities C:/plans/HOUSE.pdf --page 4 --job jobs/myhouse --level L1 --scale "1/4in=1ft"
mosby scale-check jobs/myhouse --printed-in 1484 --axis x       # wall extent vs the printed overall dimension (persisted in sources.json)
mosby pdf crop C:/plans/HOUSE.pdf --page 4 --job jobs/myhouse --bbox-pt 1280 1890 1720 2000 --reason "north arrow" --png evidence/pdf/north_arrow.png
mosby survey north jobs/myhouse --axis Y- --evidence evidence/pdf/north_arrow.png --status verified --sheet A2.1
mosby pdf datums C:/plans/HOUSE.pdf --page 10 --job jobs/myhouse --scale "1/4in=1ft" --region-pt 150 150 2900 1000     --reference-y 793.96 --names '{"793.96": "L1", "613.96": "bearing_plate", "576.31": "L2"}'     --levels '[{"name": "L1", "printed": "0'''-0\""}, {"name": "bearing_plate", "printed": "10'''-0\""}, {"name": "L2", "printed": "12'''-1\""}]'
#   -> copy the verified heights into job.json levels[] (ceiling_height_m, datum_source)
#   -> fill decisions.md (every D-xx row gets an adopted default), then:
mosby survey conflicts jobs/myhouse
mosby audit gate p0_survey jobs/myhouse                         # host-only gate: prints the writer for anything missing/stale

mosby pdf blobs C:/plans/HOUSE.pdf --page 4 --job jobs/myhouse --min-height-pt 4 --out jobs/myhouse/evidence/pdf/blobs_p4   # blobs + contact sheets + template
#   -> transcribe the contact sheets into transcriptions.json (Read the PNGs; "?" for illegible), then:
mosby pdf inject-text jobs/myhouse/entities/L1 evidence/pdf/blobs_p4/transcriptions.json evidence/pdf/blobs_p4/text_blobs.json
mosby extract jobs/myhouse --level L1                           # plan_L1.json + debug png + plan_validate/plan_lint/source_completion
mosby plan validate jobs/myhouse --level L1                     # + reports/openings_complete.json
mosby registration jobs/myhouse
mosby audit gate p1_extraction jobs/myhouse
#   corrections ONLY through plan_flags.yaml (labels, openings, gap/cell/room dispositions), then re-run extract

mosby build shell jobs/myhouse                                  # headless blender -b: walls, headers, jambs, slabs, ceilings, stairs, cameras
mosby audit gate p2_shell jobs/myhouse                          # mesh_validity, section_vs_source, coplanar_faces, protrusion, headers_present, ...
mosby render batch jobs/myhouse && mosby render inspect jobs/myhouse
mosby status jobs/myhouse                                       # phase, gates, stale reports, next step

What the house run produced at the P1 gate: scale delta 0.0 in on 1484 in, four datums within 0.2 in of their printed values, 204 labels injected (104 multi-word merges), 194 walls, 24 doors, 14 windows, 3 cased openings, 25 rooms, 0 leaks, 41/41 openings with resolved widths and heights.

Inside Claude Code you rarely type these: the mosby router skill detects the phase from status.json and calls the phase skill, which runs the commands. The job argument may be omitted when the current directory is, or contains, exactly one job.

Design

Five root causes accounted for most of the house run's cost, and each has a mechanism in the plugin that removes it rather than a rule that asks the model to remember it:

Root cause Share Mechanism
Geometry built from the wrong reference: finishes, floors, trim, fixtures and cameras placed from semantic room polygons and nominal datums instead of the evaluated faces of the model (103 floor gaps, 110 detached baseboards, 105 floating plates) ~40 % Every finish, fixture and camera builder takes host faces from the evaluated depsgraph, never from plan.json; coplanar_faces, finish_reach, fixture_seating and protrusion run as pre-render gates
Source not exhausted before building: 141 wall polygons omitted by parallel-line pairing, door hinge and swing never extracted, headers absent, PDF layers never probed ~20 % source_completion (closed cells of raw lines vs the wall set) is a P1 gate; door records carry hinge, swing, rough and clear width from source arcs and jambs; PDF probe, scale gate and datum protocols are P0 steps
Verification narrower than the claim: door-throat rays reported as walkability, eye-level cameras reviewed as whole-house, contact sheets as native inspection ~20 % Computable definitions of done per phase in status.json; route and body-sweep audit; camera coverage and line-of-sight validator; native-resolution review receipts; a Stop hook that blocks delivery claims the gates do not back
Process without enforcement: a 6-iteration cap in prose, duplicate iteration numbers, fix-one-instance instead of fix-the-class, four full re-render generations ~15 % Hooks own the bookkeeping (act log, checkpoints, cap counter); class audits run after every apply; renders happen once after gates pass; freeze generations instead of "pending" markers
Infrastructure surprises: two Blenders on one MCP port, an Eevee material-preview crash after mesh replacement, PNG file locks, float32 CSG, boolean modifiers on architecture ~5 %, but catastrophic Scene-identity preamble injected into every execute_blender_code; headless-by-default mutations with live reload; render queue with staging files; analytic CSG on a 10 um grid proven on stored float32 coordinates

Principles that follow: source walls are immutable and finishes are separate objects that touch a host face; every builder consumes evaluated geometry (plan.json is the plan, not the model); nothing is done without a passing report bound to the source hashes and, from P2, the model hash and scene signature; class, not instance; observe once, correctly; defaults over blocking questions (conflicts get a recorded default in decisions.md and building proceeds); headless mutations, live viewing. Evidence is one contract: mosby.report.Report writes every audit, gate and receipt, mosby status computes gates and staleness from input hashes, and the hooks read it. Rubric scores do not exist; findings carry a class, and blocking classes fail gates.

Claim language is part of the design: built, verified, reviewed and delivered are four different words with four different evidence requirements, "passes" always names its audit and scope, a bounded check is PASS_LOCAL(scope), and an anomalous pixel is probed to an object before anyone calls it a visual limit. The full table is skills/mosby/reference.md section H.

Repository layout

mosby/
  .claude-plugin/plugin.json     plugin manifest
  hooks/hooks.json + *.py        enforcement (see "Hooks"); _hooklib.py is the shared I/O contract
  skills/<name>/SKILL.md         phase skills + `mosby` router (+ reference.md per skill)
  agents/                        independent-reviewer, source-transcriber, defect-hunter, gate-auditor
  tools/mosby/                   python package + `mosby` CLI (constants.py is the shared contract)
  tools/sync_codex.py            generates codex/ (AGENTS.md + .agents/skills) from skills/
  templates/job/                 job.json, brief.md, decisions.md, plan_flags.yaml, status.json
  tests/                         hook contract tests (synthetic events), validator tests, Blender-side suites
  codex/ (generated)             Codex mirror; never edit by hand

tools/mosby/constants.py is the one file every module, hook and skill agrees on: object roles and custom properties, tolerances, phase names, the audits each gate requires (GATE_AUDITS), which command writes each host-side audit (HOST_AUDIT_WRITERS), the job folder layout (JOB_FILES, JOB_DIRS) and the CLI surface (CLI_COMMANDS). Skills refer to CLI commands and audit names, never to Python function names.

Hooks: what is enforced, and how to get past a block

All hooks are registered in hooks/hooks.json and run as python hooks/<name>.py so they behave identically under PowerShell, Git Bash and POSIX shells. Every hook is a no-op unless a Mosby job folder (a directory holding job.json, brief.md, plan.json, model.blend or sources.json) exists under the project, at most three levels deep. A hook that crashes never blocks: the error is swallowed and logged as a note. Set MOSBY_HOOK_DEBUG=1 to append every decision to $CLAUDE_PLUGIN_DATA/hook_debug.log.

Hook Event (matcher) What it does when a job exists Blocks? How to get past it
session_start.py SessionStart (startup, resume, compact, clear) Injects the active job, phase, gate states, Blender target (file/scene/port), carried-over unobserved acts, and the 8 global rules. Phase is read from status.json or inferred from files, never from prose. no -
prompt_context.py UserPromptSubmit Injects a one-line status only when status.json changed, or when a check-in is due: 6 mutating acts since the last passing class audit. no Run the class audit (mosby audit <name>); a passing mosby audit/status/review command resets the counter.
guard_write.py PreToolUse Write, Edit, MultiEdit, NotebookEdit Refuses hand edits of generated files: plan*.json, entities/*.json, sources.json, status.json, reports/*.json, receipts.jsonl, anything under .mosby/; refuses any write into sources/ or to a .pdf/.dwg/.dxf. yes The block names the command that regenerates the file (mosby extract, mosby pdf to-entities, mosby status, mosby audit ...). Change the inputs (plan_flags.yaml, decisions.md) and regenerate. If the generator lacks a field, extend tools/mosby.
guard_blender.py PreToolUse mcp__*blender*__execute_blender_code* Rewrites the code: prepends a preamble that raises if bpy.data.filepath/scene differ from job.json, or a render job is running; the first mutating snippet of a session and the first after 10 quiet minutes also saves a checkpoint to .mosby/checkpoints/. In a live target: blocks read_homefile, read_factory_settings, quit_blender, and open_mainfile without the marker. Warns (does not block) on MATERIAL/RENDERED viewport shading, Boolean modifiers, object.join. yes (live resets) Mark a read-only probe with # mosby: read-only to skip the checkpoint. Mark the reload-after-headless-apply step with # mosby: reload. Fix job.json (blender.file, blender.scene, blender.port) if the preamble says WRONG FILE/SCENE; do not edit the wrong session.
guard_bash.py PreToolUse Bash Blocks recursive deletes of sources/, .mosby/checkpoints/, reports/, renders/ inside the job; blocks blender ... --python builds while a gate in GATES_REQUIRED_BEFORE_BUILD (p0, p1) is not passed; adds a note when PowerShell > redirection would write a UTF-16 log. yes Move evidence out of the job by hand instead of deleting. Run the missing audits (mosby status lists them). Log with | Out-File -Encoding utf8 or run through Bash.
log_acts.py PostToolUse Blender exec, Bash (blender/mosby/tool scripts) Appends .mosby/acts.jsonl (code sha, mutating?, status, error, checkpoint landed?); counts mutating acts since the last observation and since the last passing audit. no -
log_observations.py PostToolUse Read of an image, Blender render/screenshot tools Appends .mosby/observations.jsonl and resets the unobserved-mutation counter. no -
stop_gate.py Stop Blocks once when the last message claims delivery/completion/verification (delivered, final, complete, all checks pass, no remaining defects, 100%, ...) and the matching gate (p6_delivery or p5_review) is not pass and non-stale in status.json; or when 3+ mutating acts are unobserved. yes, once State the evidence scope instead (PASS_LOCAL(kitchen cameras), sampled: 34 door planes, unobserved), or run the missing audits and mosby status. The second stop attempt is always allowed (stop_hook_active).

Disabling: hooks fire only when the plugin is loaded (claude --plugin-dir); remove a row from hooks/hooks.json to switch one off for a session. Hook state (checkpoint receipts, counters, active job) lives in $CLAUDE_PLUGIN_DATA/state/<session_id>.json; deleting it resets counters.

Job folder

mosby init <path> [--pdf F] [--dwg F] [--target headless|live] [--port N] [--scene NAME] creates this from templates/job/. Names come from constants.JOB_FILES / JOB_DIRS.

<job>/
  job.json             name, sources[] (sha256), units, north, blender{target,file,scene,port}, levels[], acceptance{}
  brief.md             the user's words, deliverable, global decisions table, scope exclusions, progress log
  decisions.md         conflict table: id, sources, measured values, recommended default, status (default|user), depends
  decisions.json       machine form of decisions.md
  plan_flags.yaml      THE correction channel for extraction (per level); never hand-edit plan_*.json
  sources/             read-only inputs (copied by init unless --no-copy)
  sources.json         per sheet: page, title, scale, verified_scale, rotation, layers, kind   (generated)
  entities/<level>/    header.json, entities.json, layers.json, export_summary.json            (generated)
  plan_<level>.json    plan schema v2 (tools/mosby/schemas/plan_v2.schema.json)                (generated)
  model.blend          current model; model_final.blend + model.manifest.json after freeze
  reports/             <audit>.json per audit, p<N>_<phase>.json per gate, freeze.json          (generated)
  renders/<batch>/     png frames + receipts.jsonl
  evidence/            crops with sidecar json, overlays, debug plots
  scripts/             every act as a script: <phase>_<subgoal>_<n>.py
  ledger.jsonl         scoped, receipt-bound findings (replaces the house run's critique log)
  status.json          computed by `mosby status`; hooks read it                                (generated)
  .mosby/              acts.jsonl, observations.jsonl, transactions.jsonl, checkpoints/, last_session.json

Phase gates, and the audits each one needs (from constants.GATE_AUDITS; a gate passes only when every audit has a non-stale, whole-house, pass report on the current model sha):

Gate Audits
p0_survey pdf_probe, scale_check, north, datums, conflicts_resolved, sheet_index
p1_extraction plan_validate, plan_lint, registration, source_completion, openings_complete, text_transcription
p2_shell mesh_validity, section_vs_source, envelope_closure, coplanar_faces, protrusion, headers_present, stair_routes, diagnostic_cameras
p3_finishes finish_reach, coplanar_faces, trim_coverage, material_slots, protrusion
p4_interior programme_coverage, containment, door_sweeps, fixture_seating, room_connectivity, light_coverage, camera_adequacy
p5_review render_receipts, anomaly_scan, independent_review, edge_coverage
p6_delivery freeze, package, reopen, no_pending_markers

Audits that run inside Blender are started with mosby audit <name> or mosby audit gate <phase>; host-side audits (all of P0 and P1, review, delivery) are written by the command named in HOST_AUDIT_WRITERS, and mosby audit gate p0_survey prints that command for anything missing or stale instead of launching Blender. A report goes stale when any input hash it recorded (sources, plan, decisions, model) changes; mosby status shows why. A pending marker is <!-- PENDING:<id> --> in any job .md; the delivery gate fails while one exists.

Tests

python tests/run_all.py                       # 17 suites: 10 Python, 7 inside blender -b
python tests/run_all.py --no-blender          # Python suites only
python -m unittest tests.test_hooks -v        # 29 hook contract tests; feeds synthetic events to each hook
python tools/sync_codex.py --check            # codex/ mirror is up to date

Two optional fixture sets make more tests run: set MOSBY_HOUSE_DIR to a checkout of the house experiment (default: a sibling experiments/house folder) and MOSBY_SCRATCH to a folder holding a pdf_probe/ export; tests that need either skip cleanly when it is absent. The DWG integration tests need a running dwg-mcp server; set MOSBY_SKIP_DWG_INTEGRATION=1 to skip them. The hook tests deliberately run with a cp1252 pipe (no PYTHONIOENCODING) because the first end-to-end run lost the SessionStart banner to a UnicodeEncodeError. No pytest is required; with the [dev] extra, pytest tests also works.

Using the skills from Codex or another client

Hooks, agents and auto-loaded skills are Claude Code features. For Codex CLI (and any client that reads AGENTS.md and .agents/skills/) run:

python tools/sync_codex.py                    # writes codex/AGENTS.md and codex/.agents/skills/<name>/...
python tools/sync_codex.py --dest C:/my/workspace   # or generate straight into a workspace root
python tools/sync_codex.py --check            # CI / pre-commit: exit 1 when the mirror is stale

The generator reads skills/<name>/SKILL.md (+ sibling .md references), rewrites Claude phrasing to Codex phrasing (image inspection via view_image, skill mentions as $name, CLAUDE.md to AGENTS.md, references under references/), emits agents/openai.yaml per skill, and writes AGENTS.md from the mosby router skill plus the global rules and the hook contract (pulled from hooks/session_start.py and each hook's docstring) so a client without hooks at least reads what they would have enforced. AGENTS.md never names a job: job state comes from mosby status. The house run kept two hand-maintained instruction files that pointed at different jobs for 20 hours. Edit skills/, never codex/; rerun the sync.

Provenance

Everything here is distilled from one job: a two-level residence from a 17-page AutoCAD PDF (all text as stroked outlines, 352 OCG layers) plus its DWG. The first model was "delivered" at 04:11 after 3.5 hours; the owner rejected it on scale, layout, door swing and trim the next afternoon; 7.5 more hours and iterations 57-119 followed, with two further rejections after self-review had reported "39/39 PASS". Counted afterwards: 141 wall polygons the extractor omitted, 37 doors without headers, 103 floor gaps, 110 of 131 baseboards detached, 4811 cuboids with reversed normals, four full re-render generations, a 6-iteration cap blown by 2-10x on every finish sub-goal, and one Blender crash.

The run was analysed into 124 failure classes in eight clusters (source/scale/PDF, extraction, openings/stairs, envelope/finishes, fixtures/materials/lighting, review/verification, process/delivery, infrastructure/API), plus 63 classes added by a skeptic review. Every rule in a skill and every hook behaviour carries its class id in a trailing <!-- FC-... --> comment (FC-<cluster><nn>; skeptic-added classes are FC-<cluster>M<nn>). The ids are tags, so a rule can be traced to the defect that motivated it; the catalogue and the experiment they point into are not part of this repository. The skills' reference.md files carry the evidence each rule rests on.

Environment notes

  • Windows: every CLI and hook forces UTF-8 on stdout/stderr. Do not capture logs with PowerShell > (UTF-16, wrapped stderr); use Bash or | Out-File -Encoding utf8.
  • Blender stores mesh coordinates as float32; everything is authored on a 10 um grid and proven on the stored coordinates (constants.GRID_M, WELD_M). Validators read the evaluated depsgraph.
  • headless is the default Blender target: mutations run in blender -b on model.blend and the live session reloads. live runs under the identity check and auto-checkpoint described above.

Contributing

Issues and pull requests are welcome. A few conventions keep the plugin coherent:

  • Commits follow Conventional Commits (feat:, fix:, docs:, test:, refactor:, chore:), scoped to the area when useful (fix(plan): ..., feat(hooks): ...).
  • Every new rule, validator or hook behaviour carries a failure-class tag, and prefers a validator or hook over prose. Every gate audit has a writer: a bpy audit or an entry in constants.HOST_AUDIT_WRITERS.
  • Job constants never go in the library; they belong in job.json, decisions.md or plan_flags.yaml.
  • Edit skills/, then run python tools/sync_codex.py; codex/ is generated.
  • Run python tests/run_all.py (or --no-blender when Blender is not installed) before opening a pull request, and claude plugin validate . after touching the manifest, hooks or skills.

License

MIT. See LICENSE.

About

Architectural plans (PDF first, DWG optional) to a verified Blender 5.2 building model, as a Claude Code plugin. Named after Ted Mosby.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages