Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 16 Sep 12:37
· 112 commits to main since this release
05ad31f

v0.4.0 — 2026-09-16

The release that stops recommending the box and starts running it.

bb uptake shipped in v0.3.0 to answer one question: of everything bb wire
installs in front of an agent, what does a session reach for? Sixteen
sessions later the answer was in. The MCP tools fired in 0. bb pinpoint
fired in 3 of the 13 sessions that opened five or more distinct files — and
those thirteen opened between 30 and 598 files each. The reference tables were
read in 5 of 16 sessions that ran a search.

Every surface was advisory, and an advisory surface loses to the model's own
habit about three times in four. So the work in this release moves the locating
to the side that does it for nothing, refuses to hand over what it already
found, and then makes it possible to say whether what came back is finished.

The completion half is not ours: the gate checker is
unlazy 2.1.0 (MIT, commit 1667149),
vendored and renamed rather than rewritten. src/finish/vendor/PROVENANCE.md
records every rename and why nothing else moved.

Measured

bb bench run — 12 tasks derived from this repository's own open findings. Both
arms are token counts over text on disk; neither calls a model.

bare    783.9k   search the tree, open the top 10 files whole
packed   32.7k   one pinpoint prompt per task
saved   751.2k   95.8% of the bare arm, 24x less context

bb bench swebench run --n 12 — SWE-bench Verified, 12 astropy instances, run
against a worktree at the commit before the ranker change and again after it.
Identical instance sets, verified by hash.

before after
gold files in scope (budgeted to be read) 2/16 — 12.5% 8/16 — 50%
gold files named (in scope, or pointed at) 6/16 — 37.5% 11/16 — 68.8%
bare arm (top 10 files read whole) 5/16 — 31.3% 5/16 — 31.3%
instances with every gold file in scope 2 6
packed context 35.8k — 91x less 50.5k — 64.5x less

Before the ranker change the packed arm localised worse than reading ten files
at random
: 12.5% against the bare arm's 31.3%. The brief was cheap and pointed
at the wrong files. The trade is stated rather than buried — packed context rose
41%, so the context saving fell from 91x to 64.5x, in exchange for four times the
localisation.

Not measured here: resolve rate, patch correctness, latency, tool calls, billed
cost. Those need the official harness, a model, and an A/B of real sessions.

Added

  • The enforcement axis (wire.auto_pinpoint, wire.serve_from_brief,
    wire.guard_searches).
    UserPromptSubmit now runs bb pinpoint on a
    task-shaped prompt instead of suggesting it — 0.47s measured against a 15s hook
    budget, because every input pinpoint reads is a stored artefact and it computes
    none of them. What enters the window is the map, about 420 tokens; the quoted
    regions stay on disk until a read asks for one, because paying for every
    located region on every prompt spends the saving on regions the session never
    opens.

    PreToolUse then answers from that brief. A read whose region is already
    quoted is DENIED and the quote comes back in the denial reason: measured on
    src/wire/brief.js, 392 tokens of region in place of 11,185 tokens of file. An
    exact duplicate read is denied. A single-identifier search the declaration
    index answers is denied with the rows. A file cut from the scope for budget is
    asked about rather than refused, because the brief's own instruction is to
    name it and why.

    A file in SCOPE is never fully blocked. Claude Code requires one successful
    read of a file before it will edit it, so blocking the scope blocks the change;
    the denial says to read the range, that read is allowed, and it costs the quote
    instead of the file. Grep is the declared tool and Bash is where the
    measured transcripts read and search, so both are guarded.

  • bb pinpoint gaps / bb pinpoint next. bb scan, bb oversight scan and
    bb auditor gate each produce a statement about the tree and then stop one
    step short of the window, leaving a session to spend its first turns finding
    out where the statement lives. Every open finding, oversight measurement and
    declared standard that failed or has no evidence now becomes a located,
    quoted, budgeted brief before a model sees it; next makes one the session's
    active brief, which is the record the guards answer from.

    unproven never collapses into failed. A standard nobody has looked at is a
    gap in the EVIDENCE and the brief says so, because a worklist that quietly
    promotes "nobody looked" to "this is broken" is the failure the auditor exists
    to remove.

  • bb finish — the acceptance ledger. The one axis this factory measured and
    never closed: bb auditor declares the bar before the first edit and
    bb pinpoint carries the gate into the brief, and then a session reported
    "done" with nothing here able to contradict it.

    The vendored checker already holds the parts that are easy to get subtly wrong
    — an approval binding the command, the expectation, the resolved cwd, the
    shell, the timeout and the whole inherited PATH; a definition digest, so
    editing a CHECK: invalidates the evidence that CHECK: produced; an
    abandonment that exits non-zero and reports a handoff instead of dropping the
    gate. What bundlebox adds is the join: bb finish init derives GATES.md from
    the active brief's scope, the detected gates and the open findings in that
    scope, because a ledger written from a blank page is a ledger nobody writes.

    bb hook stop is a fourth verification layer that executes nothing: it reports
    a declared gate still unmet when a session is about to say it is done, and is
    silent where there is no GATES.md.

  • arc/ — a Rust index compiler. The search guard asks one question per tool
    call: is this name declared, and where. It answered by reading 20,000 lines of
    symbols-*.md and running a regex per line. arc compiles those tables into
    one 86KB binary index — two sorted id arrays, one by lowercased name and one by
    the REVERSED name, so exact, prefix and suffix are each a single binary search,
    a suffix being a prefix of the reversal. 2,153 declarations, 20ms to compile.
    Dependency-free, like the kernel and for the same reason: it must build on a box
    with rustc, cargo and no network.

    per call
    markdown scan 1.80 ms
    arc index, cold 0.14 ms 13x — what a hook process pays
    arc index, warm 0.073 ms 25x — a CLI or MCP process
    spawning arc 2.28 ms 1x — the process is the cost

    So the win is the FORMAT, not the language. arc compiles; src/arc/read.js
    reads in-process, where the 13x lands; and a test holds both readers to one
    contract on every query shape, which is the only thing that keeps two
    implementations equal.

  • bb lathe — LATHE-1, the automation engine. Not a language model, and
    nothing in its path calls one: four count-based models over artefacts this
    workspace already produced. Closed, aperiodic, contiguous sequences over what
    sessions ran (PrefixSpan with a projected database and BIDE's
    closedness test, in bundlebox_expert/sequences.py); the expert's existing
    logistic outcome model, read rather than retrained; the janitor's decayed
    memory; and prefix entropy over the arc index.

    It emits what the workspace did by hand more than twice: tagged scripts a habit
    could become (@turns, @needs, and @safe false, because the model knows
    what ran and not whether running it unattended is safe), the regions the tree
    already repeats, the prologue a new file in each group starts with, and the
    prefixes that complete to exactly one name. Every row names the support behind
    it.

  • bb env — is the environment set up. That question had no answer.
    bb init wrote a config file and stopped, so a fresh workspace had no tables,
    no index, no findings and no page, and nothing said so — the first session
    searched the tree, which is the cost this box exists to remove. Nine
    artefacts, each with the verb and the declared gear that produces it, whether
    it is on disk and how old. Measured on a fresh workspace: 1 of 9 after
    bb init --apply, 8 of 9 after one bb env up --apply
    , commandcenter page
    included.

    One row is deliberately not a gear's to build. The janitor writes the memory
    files a PERSON wrote, so bb env reports where it comes from — the SessionEnd
    hook, or a hand — instead of promising to run it on a tick.

  • Two skills, installed by bb wire and removed by bb unwire. bb-finish
    carries the ledger discipline; antislop carries the fifteen prose rules. A
    skill is the only surface this box installs that costs nothing until it
    triggers — the instructions block arrives in every system prompt and is billed
    whether the session was about it or not.

  • Anti-slop on the axis it never covered. bb slop has always run over every
    brief, commit message and PR body this factory emits. What it never saw was the
    markdown the MODEL writes into the tree, which the next session re-reads and is
    billed for again, every time, until somebody deletes it — a document is the only
    artefact here that charges rent. The PostToolUse hook now lints a prose file
    the agent just wrote and names the rule, the line and the token cost. Advisory
    by design: a hedge is sometimes the honest word, and a hook that rewrites
    somebody's sentence unasked is worse than the sentence.

Changed

  • The ranker scores specificity and path, not just match quality. Enforcing
    a brief raises the cost of a bad locate, and the locator had two holes. "wire
    the pre-read guard so it denies a read the pinpoint brief already quotes"
    ranked slop/index.js, tokens/ledger.js and auditor/charter.js first —
    each declares a symbol called exactly read, guard or brief — and never
    put src/wire/hooks.js in scope at all.

    Two corrections. Inverse document frequency over the candidate set, so an
    exact match on a name the tree uses everywhere stops outscoring a loose match
    on a rare one. And path evidence, because a directory named wire is a
    deliberate statement about what a file is for and was worth nothing: a file the
    path names now enters the ranking whether or not a symbol matched, which is
    what had kept the answer out of the candidate set entirely. An explicit file is
    PINNED rather than scored, since specificity and path together can beat any
    fixed constant and losing an argument to the ranker is not something a human
    naming a file should have to do.

  • The grow loop's grep fallback fires on a NOISY index, not only a silent
    one.
    sym.length < 3 could not tell a real localisation from a statement
    whose words are common symbol names here, which returns dozens of hits and
    answers nothing. It now counts only the hits whose term is rare enough to be
    about WHICH file.

  • The cron tick rebuilds the page. watch is the gear that rebuilds the
    commandcenter, and watch was not in the factory chain. Every thirty minutes
    the tick folded the ledger, measured the window and rebuilt nothing, so the one
    page this workspace has showed the state of whenever somebody last ran
    bb commandcenter build by hand. orient also gained the arc index (directly
    after the tables it derives from) and the located worklist; buckmaster gained
    LATHE-1, after the models it reads. unsafeVerbs still returns empty for the
    whole chain: nothing on an unattended tick can spend.

  • The brief record is one slot per SESSION. A single brief.json is wrong
    the moment two sessions share a checkout, which is the normal case here and not
    the edge one — bb uptake counts sixteen sessions on this tree, and two others
    were running while this was written. Session B's prompt overwrote session A's
    brief, A's guards refused to answer from a record belonging to B, and the whole
    benefit quietly stopped arriving. Nothing broke; it just stopped working. Swept
    on session end, because age is the only signal a hook has for a session that
    ended without reaching it.

Fixed

  • bb wire --apply shipped a settings file with the read guard missing.
    addClaudeHooks stripped bundlebox's hooks once per ROW instead of once per
    event. Invisible while each event had one handler, and wrong the moment one had
    two: the PreToolUse(Read) row was installed by the first row and stripped
    again by the second, with no diff to say so.

  • rel() resolved a relative path against cwd, not the workspace root. It
    used path.resolve, so the two agreed only while cwd happened to BE the root.
    Anywhere else — bb run from a subdirectory, a hook whose cwd the harness
    chose, any test with a fixture BB_ROOT — rel("src/a.js") walked out of the
    workspace, failed the .. test and came back as an absolute path into a
    different tree. It surfaced as a pinpoint anchor pointing at a same-named file
    in the real repository while the brief described the fixture.

  • Every session ended by printing [object Object]. The SessionEnd hook
    stringified the object tokens/session.js returns instead of reading the one
    field meant for a person.

  • Three false denials by the new search guard, each found by it refusing its
    own author's search, each fixed with a rule rather than an exception.
    A
    search restricted to a directory asks who CALLS a symbol, and a declaration
    index has no rows for that. ONE identifier is the whole boundary of what an
    index may refuse — grep "usage|process.argv|exitCode" was refused on the
    strength of one of four alternatives, and a quarter of an answer is a wrong
    one. And parseBash split on | inside quotes, so grep "a\|b\|c" became
    three segments whose first looked exactly like a single-identifier lookup; the
    shell tokenizer now respects quotes and lives in core/util.js, because two
    copies of a parser are two behaviours.

  • The sequence miner did not finish on real data. It counted each candidate
    by scanning the whole corpus — |level| x |alphabet| scans per level — which on
    sixteen sessions ran four minutes of wall clock for 4.8 seconds of CPU and was
    still going. The cost was not the algorithm's shape but where the order was
    read from: 1.4GB of JSONL parsed in full to recover one string per tool call.
    The PostToolUse hook now records the SHAPE when the command runs — forty
    bytes, and never the argument, so the log cannot carry a secret somebody passed
    on a command line — and the learn pass went from four minutes to 1.4
    seconds
    . PrefixSpan with a projected database replaced the rescan.

  • Six rows describing one habit. The pipeline here is fourteen verbs long and
    MAX_LEN was 8, so every 8-item window had support 59 and every one of them
    looked CLOSED — not because the data said so, but because growth stopped at the
    cap and no 9-item extension existed to disprove it.

  • cd was the most frequent habit in the workspace. An agent prefixes almost
    every shell call with cd <root> &&, so taking the first binary of each command
    shaped the entire corpus as cd: 136 occurrences, lift 2.46. Shell grammar was
    mined too — (node ; for i ; do curl ; break ; done was reported as a habit
    with a lift of 68. Neither is work a script could displace.

  • out_of_scope -> out_of_scope, four hundred times over. The completion
    table's prefix loop runs up to a name's own length, so a prefix that IS the name
    completed nothing and was listed anyway.

  • A derived ledger the checker refused to PARSE. bb finish init left every
    EXPECT: blank, reasoning that a guessed success marker is an oracle that
    cannot fail. Right about the risk, and it turned an unfinished ledger into a
    malformed one and buried the instruction under three parse errors. A
    placeholder no command prints parses, and the gate fails until somebody fills
    it in — which is the honest state of work nobody has proved.

Corrected

  • Automating a surface must not make it unmeasurable. wire.auto_pinpoint
    runs the locator from a hook, and a hook leaves no turn in a transcript. The
    change that finally made pinpoint fire on every task-shaped prompt would have
    shown up in bb uptake as a surface that still never fired — the verb whose
    whole job is saying whether the wiring was reached for could not see the one
    case where it always is. The per-session brief record is the evidence, and it is
    the only evidence there can be. A separate auto row reports that case on its
    own, because a session that called bb pinpoint itself and a session that had
    it called for it are different facts.

  • Spawning a faster implementation is not faster. arc lookup answers a
    declaration query in 0.082ms against the markdown scan's 2.29ms, which is 28
    times faster and completely irrelevant: the process costs 2.20ms, so end to end
    the two were 2.28ms and 2.29ms. A guard that runs once per tool call cannot pay
    for a process. The Rust binary stayed the compiler and the reference
    implementation; the hot path reads its output in-process.