✅ fresh
last synced 2026-09-29T21:12:19.681221+00:00 · coverage 100% (
bd-seam)Validation by Beadloom
doc_sync— same source assync-check.
bd Seam (component)
Internal building block of the MCP server service.
Source: src/beadloom/services/bd_seam/
Overview
The boundary between this project and the bd (beads) CLI, in two halves.
client.py is a single, thin, mockable seam. The MCP process-tools (task_init / complete_bead / checkpoint) drive the beads issue tracker; rather than scattering subprocess calls across the handlers, every bd invocation funnels through run_bd. Tests patch run_bd (or bd_seam.client.subprocess.run) so the tools run without a real bd binary.
invocations.py, assumptions.py and population.py derive the other half: where this project reaches bd and what each call form assumes about the answer. BDL-068's CONTEXT Q4 decided that shape — an External bd finding is answered by deriving our own call sites, never by a wrapper, because a wrapper is a second thing to keep in step with upstream and a derived population fails on a call site added later. beadloom bd-calls prints the report.
answers.py is the run-time half of the same question: which population an answer that already came back covers. The derivation judges a call FORM before it runs; this reads bd's own notice off stderr, bd's own suggestion block off stdout, and the argv we wrote. It re-implements no decision bd makes, which is what keeps it on the right side of Q4.
creation.py is the one path that creates beads: a bead's id comes from the tracker's answer, never from a number we authored. It composes the plan document bd create --graph accepts, whose edges name plan-local keys, and reads the key -> id mapping bd answers with. That removes BDL-UX #171's root rather than guarding it, and it is BDL-UX #165's remedy in the same act — one process instead of one per bead and one per edge.
Public surface
run_bd(args, *, cwd=None)— invokebdwith args (no leadingbd) and capture its output; raisesBdUnavailableErrorwheneverbddoes not answer.BdResult— frozen dataclass:returncode,stdout,stderr, plus anokproperty (True iffreturncode == 0).BdUnavailableError— raised whenbdcannot be run to completion: not installed, not executable, wedged past the timeout, or answering in bytes this process cannot read. A non-zero exit is not that case — that is bd answering, and it comes back as aBdResult._BD_TIMEOUT_S— the per-invocation timeout (60s).
The derived call-site population
text_invocations(sources, *, channel=...)/python_invocations(sources)— the one grammar, over(label, text)pairs. The text channel anchors on command position; the Python channel reads argv from the AST and resolves module-level string constants.BdInvocation—source,line,channel,text,words,flags,unresolved_arguments.call_sites(invocations)→BdCallSite, which addssubcommandandassumptions;report_of(sites, unreached=...)→CallSiteReport, which addsmeasured_against.Assumption—name,verdict,detail. The verdicts aresecured,unsecured,holdsandunmeasured.lock_invocations(invocations)— the bridge towave-plan's landing-lock judgement, so amerge-slotform is parsed once here and judged once there.project_report(project_root)— the whole population for a project, over four channels: the composed flow, the shipped templates, the installed package's Python, and.git/hooks/.BD_MEASURED_VERSION— the release every verdict was taken against,bd 1.0.4.population_flags(subcommand)— the flags that widen a subcommand's answer to its whole population, orNonewhen this derivation has not measured what population that subcommand's answer covers.
The population an answer covers
coverage_of(argv, stderr)→AnswerCoverage—subcommand,coverage,shown,total,widening_flags, plusas_askedand astatedsentence. The coverages areas-asked,filtered,truncatedandunchecked.ready_ids(stdout)— the ids in abd ready --jsonanswer, orNonewhen that answer cannot be read at all.suggested_beads(stdout)— the ids--suggest-nextnamed, read from bd's ownNewly unblocked:block.confirmed_suggestion(close_stdout, ready)→ConfirmedSuggestion—candidates,confirmed,still_blocked,compared, and astatedsentence.ready=Nonemeans the confirmation could not be made.COVERAGE,COVERAGE_AS_ASKED,COVERAGE_FILTERED,COVERAGE_TRUNCATED,COVERAGE_UNCHECKED,NOT_COMPARED,NOTHING_TO_CHECK,READY_COMMAND.
Creating beads without authoring an id
PlannedBead—key,title,bead_type,priority,parent_id,depends_on.depends_onholds PLAN-LOCAL KEYS and never ids.graph_plan(beads)→ the JSON documentbd create --graphaccepts; raisesAuthoredNumberErrorwhen a planned title states a bead number.allocated_ids(stdout)— thekey -> idmapping frombd create --graph --json, orNonewhen that answer cannot be read.created_id(stdout)— the id frombd create --json, orNone.plan_is_required(bead_count),PLAN_THRESHOLD,EDGE_BLOCKS,PLAN_SCHEMA_KEYS.
Invariants
A verdict names the release it was measured on. Every entry in the assumption table was taken on bd 1.0.4 with the streams read separately and the exit codes read without a pipe, and
BD_MEASURED_VERSIONrecords it.tests/unit/services/bd_seam/test_bd_call_sites.py::test_the_recorded_release_is_the_one_installedfails when a differentbdis installed, naming what has to be re-measured. That is not ceremony: three premises BDL-068 S5 inherited were re-measured and destroyed — BDL-UX #194 and #237 (bd merge-slotgrants no exclusion; it does, and 32 concurrent acquires produced exactly one winner per round) andbeadloom-l2f2(bd import -idoes not exist; it does, as a documented legacy alias, and imported 137 issues at exit 0). An External defect a laterbdfixes must fail loudly rather than quietly guard nothing.A withdrawal is a measurement too, and one shape is not enough. BDL-UX #97 was withdrawn in this module and the withdrawal was wrong. It rested on one dependency shape — a target with two blockers, one closed — where
--suggest-nextwas silent while the target was blocked and spoke when it became ready. Both directions of the OUTCOME; one shape.The mechanism has been characterised three times and no characterisation survived the next measurement. The correction to the withdrawal above concluded
--suggest-nextis silent in every shape where exactly one blocker had just closed; that reading rested on ten cells sharing ONE rig.beadloom-0mdo.52re-measured twenty-three shapes in twenty-three separatebd initrigs, which is the axis the shared rig could not hold constant, and that shape names a still-blocked bead. Sixteen of the twenty-three are false positives, on no shape rule any of the three sessions found. So this component records the OBSERVATION and never the mechanism: on bd 1.0.4--suggest-nextnames beads that are still blocked, andbd readywas correct in all twenty-three.unblocked-is-readyis thereforeunsecuredon the call form alone, andbd readyis what settles it.A fourth characterisation was taken and it disagrees with the other three, which is why the stance above is the right one.
beadloom-0mdo.55re-measured sixteen shapes in sixteen separatebd initrigs on bd 1.0.4, varying the number of blockers already closed, the number left open and whether the target was created before or after them:--suggest-nextnamed a still-blocked target in five of the sixteen, andbd ready --limit 0was correct in all sixteen. Individual cells contradict the earlier readings — a target created last with one blocker closed and one still open names it here and did not there — so three sessions have now produced three incompatible shape rules and one stable observation. A guard built on the observation holds; one built on a mechanism would have been wrong three times.An assumption no flag can reach is settled by the ARTIFACT, not by the line.
unblocked-is-readyissecuredwhen the artifact that instructs--suggest-nextalso namesbd ready, because the artifact is what a reader reads: a subagent runs from.claude/agents/<role>.mdalone, so a mitigation that lives inCLAUDE.mdnever reaches it. Three of the four role cores were in exactly that state beforebeadloom-0mdo.52, and the shared_trackerfragment now composes the rule into every role.call_sitestherefore runs two passes — collect which subcommands each source names, then judge — because a single pass could only secure a confirmation written ABOVE the call, which is a fact about ordering rather than about what the artifact tells its reader. The verdict states its own limit: that the two answers are actually COMPARED is not something a derivation of call forms can see.An answer that came back states the population it covers.
as-askedis deliberately not calledcomplete—bd list --status opennames a population and bd honours it, and every open bead is not every bead. bd's own notice outranks the call form: passing--allis an intention, and a silent stderr is what makes it a measurement.ready_idsreturnsNonefor an unreadable answer and()for an empty queue, because collapsing the two turns a failed confirmation into "every candidate is still blocked".An unjudged site never reads as a clean one. A subcommand outside the measured table carries
unmeasured-subcommand, and a subcommand measured to carry no breakable assumption carries none — those are different facts, and today 48 of this repository's 348 sites are the first kind (bd swarm26,bd gate22).The unreached region is part of the answer.
beadloom-0mdo.58measured the reach before the population existed: a Python sweep sees about a twentieth of the subject.population.UNREACHEDnames four regions with their reasons — the coordinator's launch prompt,.claude/development/(which QUOTES call forms as evidence and instructs nobody), abda person types, and string literals in this project's Python, whose emitted scripts are read where they run instead.A bead's id is allocated at creation, so nothing may author it first. Measured on bd 1.0.4: eight simultaneous
--parentcreates took.4through.11out of launch order, and four took.1through.4out of launch order in a second rig.bd create --graphracing those four returned four FLAT ids and consumed no number from that sequence, so the plan form closes BDL-UX #171 for the path it covers for two independent reasons — no positional number is allocated, and its edges name keys rather than ids. It closes nothing forbd create --parent, which is answered by--jsonand by the conventiongraph_planrefuses to break.beadloom wavesmakes the same comparison after the fact throughtitle_id_mismatches, and both read the one grammartitle_referencesexports.The echo is preserved, and the form that discards it is named.
bd dep addechoes both beads' FULL TITLES —✓ Added dependency: proj-027 (bead 59) depends on proj-9to (bead 60)— and reading that echo is the only reason #171's mis-wired edge was caught in seconds.bd dep add --file, the bulk form a reader of #165 reaches for next, prints✓ Added 2 dependenciesand no titles at all, which is why it carries its ownechoed-titlesassumption instead of being treated as the faster spelling of the same command. The fast form of the WIRING half discards the check; the fast form of the CREATION half removes the need for it.A helper that composes an argv hides the call site from the derivation.
invocationsresolves a list literal handed torun_bdand cannot follow a function call, so anargvbuilder increation.pyleft the scaffold's ownbd createabsent from the report — not unsecured, invisible. The argv is spelled at the call site inmcp_server.pyfor that reason, and a test reddens the day it is tidied back into a helper.The grammar reads command position, not the word
bd. Measured over this repository's 65 instructing artifacts, the anchored sweep returns 266 invocations and no prose; the unanchored one also reportsbd verifies,bd checks the,bd is availableanda bd comment with.Text is decoded by a stated codec, never by the image's locale (BDL-061.37).
bdis run withencoding="utf-8", errors="surrogateescape".text=Truedecodes withlocale.getpreferredencoding(False), so on a container whose locale is not UTF-8 a bead title with one non-ASCII byte either raised or came back as a different title — neither visible on a UTF-8 machine. UTF-8 is bd's own contract here rather than a guess: bd speaks JSON, and JSON is UTF-8 by definition (RFC 8259 §8.1).surrogateescaperather thanstrictbecause what callers decide with is machine tokens (ids, statuses, structure) while the non-ASCII part is display text:strictwould let one stray byte in one bead's title report bd unavailable and skipbead-claimedfor the whole project — a gate switched off by display text.surrogateescapeis injective, so every token still decodes to a distinct string; the cost is that such a title reaches the reader with\udcff-style escapes in it.One name for "bd did not answer". The handler is as wide as that sentence (
Exception, deliberately notBaseException), because every caller already has a right response to it — the MCP tools return a structured error, the guard probe returnsNoneso the guard skips with a reason. It previously caughtFileNotFoundErroralone, so a 60-second timeout, a non-executablebdon PATH and an undecodable answer escaped the seam and the probe and reached the guard boundary aserror/exit 2 — a blocked edit for a reason that was not the real one. Nothing is swallowed: the message names the underlying class and the original exception is chained.
Collaborators
The single funnel for the MCP process-tools (task_init / complete_bead / checkpoint) that drive the beads tracker. Tests patch run_bd (or the module-level subprocess.run) to run the tools without a real bd binary.
Component doc (BDL-051; the population added by BDL-068 S5,
beadloom-0mdo.51, the answer's coverage bybeadloom-0mdo.52, and the creation path bybeadloom-0mdo.53). Public surface verified againstsrc/beadloom/services/bd_seam/.