Repository navigation
Releases: tygryso/hypotree
Release list
v0.6.0
v0.6.0
Added
- Readable, truthful dashboard observability. Schema 14 adds optional node titles
(validated to 128 characters) across model, store, toolkit, API and dashboard surfaces.
Goal responses expose direct dependency progress and blocked reasons. Node evidence is
newest-first, filtered and paginated with metrics, deltas and attestation summaries;
conflicts and active claims have dedicated read routes and UI surfaces. - Responsive dashboard layout. Long legacy identifiers are bounded on the graph while
remaining complete in details, and compact viewports stack the narrative and canvas with
no horizontal page overflow. - Deterministic cross-platform history. Status transitions normalize naive timestamps,
preserve the initial interval when clocks tie, and use stable id tie-breaks for node lists. - Goal-status observability for embedding hosts.
get_goal_statusnow reports stale
supporting beliefs, VERIFIED confirmations outside the selected goal's support, and
active sample-ceiling settlements. These additive fields let hosts steer re-verification,
graph wiring and telemetry without reconstructing engine internals. - Runner-minted process attestations. Schema 13 adds immutable
attestations
plus evidence references and context-mismatch flags. Embedded runners mint through the
direct Python API;record_evidenceaccepts only anattestation_id, and no MCP tool
can supply runner fields. Known attestations derive duration, default source, and commit
context without changing posterior semantics; unknown IDs degrade to self-reported.
Evidence history reports provenance, attestation identity, artifacts, and context
disagreement, covered by adversarial contract tests. - A Python API:
HypoTreeToolset, and a package that finally exports its own names.
hypotree was described as a library and shipped as a server.from hypotree import HypoTreeEngineraised —__init__.pycontained a docstring and a version string and
nothing else — so every consumer reached throughhypotree.engineinto what was
nominally internal, including this project's own evaluation harness. The public API is
now re-exported and covered by__all__.HypoTreeToolset(db_path, …)is what a Python host holds instead of an MCP client:
.tools()returns OpenAI function-calling schemas,.call(name, args)executes and
returns a JSON string, and it is a context manager.from_engine()wraps an engine
the caller already owns without taking over its lifecycle, because two objects
that both believe they own one SQLite connection close it twice..callnever raises. A misspelled node id, a missingsuccess, an argument that
is not an object — all come back as{"error": …}. Every one of them is recoverable
by the model that caused it, and propagating them as exceptions turns a correctable
mistake into a lost session.- Tool selection is composable:
preset="essential"exposes the six tools that can
still run the loop,read_only=Trueexposes the eleven sensors and refuses every
write, andinclude/excludenarrow either. This is not a convenience — most clients
re-send every schema on every turn, and a host already carrying forty of its own tools
cannot also carry twenty of ours. A narrowed surface is a real boundary: a hidden tool
is refused by name, not merely left out of the list, because models routinely call
tools they were never given. - Each spec carries what a host needs and no JSON schema can express:
mutates(gate
these) andessential(ship these when context is tight).get_next_targetsis
classified as a mutation — it reads like a query and it issues leases, so it writes,
and a host inferring that from the tool's name gets it wrong.
Fixed
- Batch infrastructure evidence no longer requires a fabricated logical
success. The
JSON Schema now requires onlynode_idfor result items; dispatch still refuses logical
reports without success and accepts success-lessevidence_kind="infra"reports. - The evaluation harness's hand-written tool schemas are now pinned against the real
ones. Because the schemas were unreachable outside the MCP server, the harness wrote
its own copies of seven of them, and they drifted: arm B'screate_hypotheseswas
missingexclusion_closed, the one field that stops the engine deducing an answer over
an incomplete list of candidates. A parity test now fails on any field the harness
advertises that dispatch would silently drop — thesource_refclass of bug, which has
shipped once — and on any type disagreement. Deliberate omissions are enumerated with
the reason each is acceptable, so a restriction nobody chose can no longer pass for one
that somebody did. Two are worth stating outright: arm B cannot declare an open
candidate list, and it supplies noduration_s, so cost-aware ordering is not under
test in the current gate. - A no-op edge write moved the revision counter and lied to the audit log.
add_edge
paired anINSERT OR IGNOREwith an unconditionalEdgeAddedevent, so re-adding an
existing edge wrote a row asserting an addition that never happened and advanced
events.seq. That counter is documented as advancing "exactly when something changed
and never when nothing did", and two readers take it at its word: the dashboard pushes
it to every connected browser as the change signal, and keys its whole snapshot cache
on it — so one phantom bump costs a full graph relayout everywhere. The event is now
written only when the insert actually inserted. _handle_infra_errorrewrote every column of a node from a stale snapshot. The last
whole-row writer on an engine path, and a lost-update by construction: anything written
between the read and the save was silently reverted, and an infra error arrives
interleaved with exactly the posterior and claim writes that would be lost. Replaced
with a targetedincrement_infra_retries, mirroringincrement_evidence_count.get_next_targetscould mint a lease that was already expired. The reclaim sweep
compares inclusively, solease_ttl_s=0returned the node to the frontier on the next
call while the caller believed it held the work.renew_claimhas refused non-positive
TTLs since it was written; the issuing path was guarded only by the tool schema, which
every direct Python caller bypasses — and the engine is public API now.- The dashboard refused IPv6 loopback with a port.
Host: [::1]:7331contains three
colons, so the port split was skipped and the whole string was tested as a hostname.
It failed closed, so this was never a hole — an IPv6 browser simply could not open the
dashboard. Parsing is now bracket-aware, and a bracketed foreign host is still refused.
Changed
- The agent guide now distinguishes plan steps from falsifiable belief nodes
and defines safe continuation of unfinished goals. It names the
get_goal_status→generate_learning_path→get_active_claimsresume
sequence and clarifies when backward diagnosis leaves a goal pinned versus
when a growing forward pipeline adds a new last-stage dependency. - The CLI can open an explicit belief database with
--db-pathor
HYPOTREE_DB_PATH. Embedded hosts can keep state in their own isolated
storage namespace and later launchhypotree --no-mcp --db-path .../state.db
without copying it into hypotree's global workspace resolver. - Tool schemas and dispatch moved out of the MCP server into
hypotree.toolkit. They
were always transport-neutral — a name, a dict of arguments, plain data back — but they
lived behind a module whose first three imports are the MCP SDK. A caller who wanted the
belief state had to acquire a JSON-RPC stack to reach it, or reimplement the routing.- The MCP server is now one projection of the shared specs; an embedding host is the
other. Neither owns the contract, so neither can drift from it. import hypotreeno longer importsmcp, and there is a test that fails if it starts.- Internal moves, unreleased:
mcp_server._dispatch→toolkit.dispatch.dispatch,
_evidence_report→evidence_report,_parse_instant→parse_instant,
_hypothesis_item_schema→specs.hypothesis_item_schema._tool_definitions(),
dashboard_url()andSERVER_INSTRUCTIONSkeep their names and locations.
- The MCP server is now one projection of the shared specs; an embedding host is the
- SQLite is tuned for the write path it actually has.
synchronous=NORMAL(the
canonical WAL setting: a crash can lose the last transaction and cannot corrupt the
file), plus an explicitbusy_timeout, cache size and in-memory temp store. Every
mutation writes its audit event in the same transaction, so the defaultFULLwas
buying an fsync per belief change that no caller had asked for. - A stochastic node now records whether it settled on evidence or on budget. The
convergence gate returned a bare boolean, so a node whose credible interval was still
wide at the sample ceiling left exactly the trace of one measured to a tight interval —
and "why did this settle?" was answerable only by recomputing the posterior at the time.
Newconvergence_verdictreturns the reason alongside the decision, and the ceiling
case is appended to the status-change reason where the audit log and the agent can both
read it. The booleanconvergence_gateis unchanged, and so is whether any node
settles; only the record is richer. - The git-remote lookup is resolved once per process. Up to three subprocesses with a
five-second timeout each sat on the MCP startup path before the first handshake, so a
slow git or a remote behind a hung mount could look to a client like a server that
failed to start. Re-pointing ...
v0.5.0
v0.5.0
Added
-
add_edges— grow a graph forward without destroying a node to do it. A graph grows two
ways and only one of them was expressible. Backward growth discovers a premise underneath
something already pinned to the goal and leaves the goal alone. Forward growth extends a
pipeline and must re-pin the goal to the new last stage — a goal is met when its
DEPENDENCY parents are VERIFIED, so one still pinned to stage A reports itself achieved the
moment stage A verifies while B and C sit untested. The run stops early and calls it a success.- Until now the only way to re-pin was recreating the goal with
if_exists="overwrite", a
documented full replace: omittarget_metricand the goal silently loses the bar it is
measured against. Destroying a node to add an edge to it is not a reasonable price, and the
failure it invites is silent. - Nothing has to be removed. DEPENDENCY is AND, and a later stage already depends on the
earlier one, so a goal wired to both is satisfied exactly when the later one is: the
condition is tightened, never loosened. That is why there is no edge removal to get wrong. - Validated exactly as creation is and all-or-nothing: unknown endpoints, a goal used as a
DEPENDENCY parent (GoalDependencyError) and cycles are refused before anything is written.
An edge that already exists reportscreated=Falserather than raising, so re-sending a plan
is safe. Tool count 18 → 19.
- Until now the only way to re-pin was recreating the goal with
-
A belief diff over a range:
generate_learning_path(since=…). A belief state's changes
are more interesting than its state. "Three hypotheses were confirmed and one was withdrawn" is
the sentence a standup, a PR description or a review wants, and reconstructing it by diffing two
full narratives by eye is what people did instead. Returnssettled_in_window,
withdrawn_in_windowandprobes_in_windowalongside the lifetime counters, which keep
describing the whole history — "what did this cost in total" does not become a different
question this week. Combine withas_offor a closed window; a window running backwards is
refused rather than silently returning nothing, which would read as "nothing changed".- A withdrawn belief is the most interesting thing in a window and the easiest to omit, so
reversals are counted separately and a window with nothing in it says so in as many words. - On the dashboard, the scrubber that already picks an instant now picks two: Diff from
here marks a start, the window is shaded on the activity histogram, and the narrative beside
the graph becomes the diff. Same control, used twice. - Exposed as
sinceon the MCP tool andGET /api/learning-path?since=.
- A withdrawn belief is the most interesting thing in a window and the easiest to omit, so
-
Recorded
artifactsare read back inget_evidence_historyand the dashboard provenance
panel. Written since the first release and consumed by nothing — an audit trail that cannot
produce the log it refers to is not an audit trail, and it is the same defect that was fixed
forcontext_hasha year earlier. -
status_filteracceptsEXHAUSTED. It is the status the exclusion inference produces most
and the one that could not be queried directly; the SDK enforcesinputSchema, so the filter
was rejected outright and theview="settled"workaround bundles three other statuses. -
what_would_change_my_mind— name the experiment that would overturn the conclusion. Tool
count 19 → 20. For any goal, the beliefs holding it up on the least evidence, and the
cheapest experiment that would flip each. Not what do you believe but what would it take to
be wrong, which is the question a reviewer asks and a status report cannot answer.- Ranked by fragility, not by posterior, and the two disagree on purpose. A belief confirmed
by elimination carries a confident posterior and no observation at all, so it ranks first
however sure the engine is — and it is simultaneously the cheapest thing in the graph to
settle, because one probe touches what no probe has ever touched. Then confirmations
established shallower than the depth something was built on them at, then single observations. - Read-only: no lease, no dispatch, no mutation. Asking what would change your mind must not
change it, and a test pins that it does not. - An empty list is a finding, not a failure — nothing is resting on thin evidence — and the
response says so. A panel that always finds something to say is one nobody reads twice. - On the dashboard as a third panel beside the learning path and the frontier, and over the API
asGET /api/counterfactual.
- Ranked by fragility, not by posterior, and the two disagree on purpose. A belief confirmed
-
A short batch now says a competing answer is being held back.
get_next_targetshas always
refused to dispatch two answers to one question — confirming one retires the rest, so handing out
both spends a probe the inference would have saved. It did that silently, and a caller that
asks for two targets and gets one reads it as an exhausted frontier. Two full evaluation runs say
what happens next: every redundant probe in both wasclaimed=False— self-initiated — and the
commonest shape is the caller filling the empty slot with the exact sibling the rule was
protecting. The response now carriessame_question_withheldand says so in therationale.
Silent on a full batch, and silent when the batch was short because the work genuinely ran out.
Changed
-
--cost-awareis now--experimental-cost-aware. The mechanism is measured (77% less cost
for 1.5% more probes) but only against a scripted caller on a synthetic tariff, which justifies
the mechanism and not the default. The name now says which of those is true. It is expected to
become the default in a later minor release once a run with a live model has scored it, and the
flag disappears at that point; recordingduration_sandestimated_costis useful either way. -
Cost-aware selection: rank by value per unit of what a probe actually cost. Selection
weighed every candidate purely on how much it would settle, which is right only while all
probes cost the same. They do not: a three-day fine-tune and a one-second unit test were
ranked as if interchangeable, so the cheap decisive check waited behind the expensive one.record_evidenceacceptsduration_s, the wall-clock seconds the experiment took.
The engine reads back the mean per node and converts the frontier into median-relative
ratios — 4.0 means "four times the typical probe here" — then scoresvalue / cost.
Relative, because absolute seconds do not compare across workspaces, and a median is not
moved by the one overnight run that would drag a mean.- A node nobody has timed inherits its exclusion-group siblings' median before falling
back to the workspace median: the answers to one question are usually tested the same way,
so the closest available evidence is the sibling's, not the global average. estimated_costoncreate_hypotheses, because the observed model was blind exactly
where the saving is. A question is settled once, so at the moment the navigator chooses
between its competing answers none of them has been timed, and the sibling fallback handed
every one of them the identical number. And within a question is the only place cost can be
saved at all: ordering across questions changes nothing, since every question must be settled
either way, whereas the last answer standing is deduced rather than probed — so whichever
one is never reached is never paid for. Ordering cheap-first puts the expensive answer in that
free slot. Precedence is observations, then the estimate, then siblings, then the workspace
median. Safe to consume unmeasured in a way an accuracy prior is not: cost moves only what is
tried next, never what the belief state asserts, and the first realduration_soverrides it.- An expensive premise can be deferred but never starved. Dividing by cost defers a slow
node whenever a cheaper one keeps looking marginally better — and the cheap candidates are
genuinely better value at every comparison, so the expensive one loses every comparison it
is ever in, forever, and a premise that gates the whole graph never runs. The cost weight now
decays with waiting time: full effect at zero wait, exactly neutral afterCOST_PATIENCE_S
(one hour). The exponent decays rather than the ratio, so a 100x node and a 2x node relax at
the same rate instead of the expensive one being punished twice for being expensive. - Off by default (
hypotree --cost-aware). With it off, every ratio is exactly 1.0 and
ranking is bit-identical to a build with no cost term at all — the only honest way to add a
term to an acquisition function that a frozen gate has already scored. Recording
duration_sis always safe; it is stored and displayed either way, and the dashboard now
shows how long each result took beside it. - And it is now measured. The phase shipped with a falsifier nobody could run: every
criterion in the harness counts probes, which is defensible only because the oracle answers
in uniform milliseconds — so a probe is the unit of cost by construction,θ/candθinduce
the same order, and the claimed 20% saving was not merely unmet but unobservable. A falsifier
that cannot fire is not a falsifier.eval/cost_gate.pyplays every seed twice against a
cost-weighted tariff, once with the navigator allowed to see it and once not: cost falls
77.3% against a 20% threshold, probes rise 1.5% against a 10% ceiling, 30/30 seeds
cheaper and 30/30 still solved, capturing 94.2% of the achievable saving. Both halves come
from one mechanism, which is...
v0.4.2
v0.4.2
Fixed
- Align
choose_portsocket flags withasyncioon Windows. On Windows,SO_REUSEADDRallows probing sockets to bind over active listeners, reporting occupied ports as free and causingasyncio.create_serverto crash withaddress in use.choose_portnow appliesSO_REUSEADDRonly on POSIX (os.name == 'posix'), matchingasyncio's standard library implementation.
Full Changelog: https://github.com/tygryso/hypotree/blob/master/CHANGELOG.md#042---2026-08-05
Full Commit: https://github.com/tygryso/hypotree/commits/v0.4.2
v0.4.1
v0.4.1
Fixed
pip install hypotreewas broken by the release ofmcp2.0.0. The
dependency was declared asmcp>=1.28.1with no upper bound, so a fresh
install resolved to 2.0.0, which removed the low-level decorator API
(Server.list_tools,call_tool,read_resource) that the server binds its
handlers with. Every new install died on startup with
AttributeError: 'Server' object has no attribute 'list_tools'before the
first handshake. The requirement is nowmcp>=1.28.1,<2.
Full Changelog: https://github.com/tygryso/hypotree/blob/master/CHANGELOG.md#041---2026-08-04
Full Commit: https://github.com/tygryso/hypotree/commits/v0.4.1
v0.4.0
v0.4.0
Performance
- Selecting a target is 41× faster on a large belief state: 1200 nodes went from
4 494 ms to 110 ms per dispatch, and the curve from ~3.8× to ~2.3× per doubling of
nodes. None of this was visible at the 40–60 nodes an evaluation episode produces, which
is why it survived 662 tests — but every long-lived workspace lands on the wrong side of
it, and "the belief state survives across sessions" is the product claim.- The dominant cost was an N+1 query.
get_all_nodesderives each node'sparent_ids
from the edges table and was issuing one query per node; the navigator calls it
several times per dispatch, so handing out two targets cost ~6 000 SQL round-trips.
Parents are now resolved for every row in a single query. HypoTreeGraph.add_edgeran a fullis_directed_acyclic_graph()check per edge, so
reloading E edges cost O(E·(V+E)). Newadd_edges_bulkchecks the finished set once
and rolls back if it is cyclic. The per-edge path stays for interactive creation, where
naming the offending edge is worth the cost.HypoTreeGraph.parents()scanned every edge in the graph and the frontier calls it once
per node. Parents are now indexed bydst.get_next_targetssyncs the in-memory graph once per batch instead of once per pick.
The batch loop takes leases, which change node rows but never the topology.- A long-lived incremental graph — invalidating on write, never rebuilding — is
deliberately not done. A cached graph that disagrees with SQLite would produce
silently wrong selections, and at 110 ms there is no case for taking that risk.
- The dominant cost was an N+1 query.
Added
- The closed-world assumption is now declared rather than assumed. Deduction by elimination
— confirming the last survivor of an exclusion group for free — is sound only if the listed
candidates really are all of them, and nothing checked that. Withhold one axis's winning
value and the engine refuted the four that remained and confirmed the survivor with no
observation of its own: a free confirmation of a value that is wrong, recorded as a
confirmation. The run then endedempty_frontierwith the goal unmet and never said the
question had been incomplete. This is the mechanism the README credits for the moat, and it
had shipped unguarded for months.exclusion_closedoncreate_hypotheses(schema10,nodes.exclusion_closed,
NOT NULL DEFAULT 1— exactly what every group written before it meant). Defaults to
true. Passfalsewhere the next candidate always exists — "which learning rate", "which
prompt wording", "which threshold" — and the last-one-standing deduction is withheld, along
with the pruning that rests on it. Confirming a member still retires the others: that is an
observation about the confirmed value, not a claim about the list.- Openness is per-group and one member declaring it is enough. It withdraws an inference,
so the cautious declaration governs; the reverse would let a later call that forgot the flag
quietly re-enable deduction over a list its author knew was partial. - A deduction can now be withdrawn. When every question is answered and the assembly still
falls short, the belief with no measurement behind it is the one to doubt. The engine hands
it back asUNTESTEDand the navigator dispatches it, so one probe settles whether the value
was wrong or the list was incomplete. It never asserts the value false — nothing was ever
observed about it, and asserting on no evidence is the one thing the belief state may not do.
The same applies at conviction: a deduction named as a conflict's sole cause is tested rather
than convicted. _is_deducedreads the whole history, not its last entry. A deduction that passes
through conflict review comes back marked "released from review", which describes the last
thing that happened to it and erases where the confirmation came from — a node never once
observed looked like one that had stood up to scrutiny.dead_questionnow says which kind. Over a closed group it means the list was wrong or
one of the eliminations was, and what depended on those values is pruned. Over an open one it
means the answer is very likely one that was never listed — and nothing is pruned,
because an untried candidate could still satisfy it.- Measured. Across all 30 seeds × 5 axes with the winner withheld, 144/150 (96%) now
enddead_questionnaming the incomplete axis, against 0/150 before — every one of which
wasempty_frontier. Still 0 solved, which is correct: the winning value was never offered,
so the goal is genuinely unreachable, and what changed is that the run says why. A wrong
answer reported as wrong is a result; the same answer reported as an empty frontier is not.
Fully-declared self-play is untouched at 30/30, mean 15.9.
- The dashboard runs by default. It was behind
--dashboardwhile it was unproven, and the
result was that the people it was built for never saw it — nobody opts into a feature they
have not been shown. It binds loopback only, mints a fresh token per start, and cannot take
the MCP server down: a bind failure is reported and the server carries on, because the cost
of a viewer failing must never be the cost of the tooling failing.--dashboard-port PORTpicks the first port to try (it still probes upward, so several
workspaces can be open at once).--no-dashboardopens no socket at all.--no-mcpreplaces--dashboard-onlyand reads better beside its sibling. v0.4.0 is
unreleased, so the old spelling never shipped and is simply gone.
- The agent can hand over the dashboard link. The URL is minted at startup and printed to
stderr, which the model never sees — so an agent asked "where can I watch this?", which is
constant, had no way to answer.get_workspace_infonow returnsdashboard_url, and the new
hypotree://dashboardresource returns it on its own. Both carry the session token, which is
the whole credential;nullmeans none is running, and the text says how to start one. - Backward pruning over a complete question. The exact dual of deduction by elimination,
and the half of that symmetry that had never shipped: all-but-one candidate eliminated
confirms the survivor for free, and all of them eliminated says the question has no answer
among the candidates offered — so nothing that assumes one of them can ever be satisfied.
Those branches are pruned under their own reason marker and the navigator reports a new DONE
reason,dead_question, naming the group that ran out and the goal it was holding up.- Soundness rests entirely on the exclusion group. "All four approaches I tried failed"
does not entail "the objective is unreachable" — it entails "I need a fifth idea", and a
rule that could not tell those apart would let an unimaginative agent declare its own goal
impossible. What makes this version sound is that the caller declared these to be the
competing answers to one question. - Three guards, each closing a way it could fire on a live search: at least two candidates
(one value is not a question); every member eliminated on its own evidence, which is
strictly stronger than the elimination test deduction uses —PRUNEDdoes not count,
because it is a statement about an ancestor's subtree, and neither does a member with no
observation of its own; and no open conflict over any member, because conflict diagnosis
may hand one straight back. - It reaches what the refutation cascade deliberately spares. A premise settled as
EXHAUSTEDwas never refuted, so its dependents stay on the frontier being re-attempted
long after the premise ran out; that is the case this exists for. dead_questionis an instruction, not an ending: the fix is to add the candidate that was
never listed, and the eval harness treats it as a reason to continue.
- Soundness rests entirely on the exclusion group. "All four approaches I tried failed"
record_evidenceis batch-native. Passresults=[{node_id, success, depth, claim_id}, …]
to report every experiment from one turn in one call.create_hypotheses,update_status
andget_next_targetshave all been batch-native for a while;record_evidencewas the
last singular high-frequency tool, and it is what pinned a synchronous agent at two turns
per experiment — one to probe, one to report. The single-result form still works and is the
k=1shorthand.- Results are applied in order, so a refutation's cascade lands before the next result
in the same batch is read and the belief state ends where it would have ended had the
results arrived separately. - A refused report does not abort the batch. Every result was paid for by an experiment
that already ran, so discarding k-1 of them to punish one badnode_iddestroys evidence.
The refusals come back underfailed, named and explained. - The fused dispatch runs once, after the whole batch, and is still a top-up.
- Results are applied in order, so a refutation's cascade lands before the next result
get_conflictsnow reportsskipped_no_substituteper member.- A read-only web dashboard:
hypotree --dashboard [PORT]andhypotree --dashboard-only [PORT].
A belief state that revises itself is hard to appreciate from a status column. The
dashboard runs on the MCP server's own event loop and serves the graph, the narrative and
a timeline over localhost HTTP.--dashboard-onlystarts no MCP server at all — the
try-before-you-wire path for someone who has not configured a client yet.- The interface. Vue 3 over server-computed coordinates, with
d3-zoomfor
hardware-accelerated pan and zoom. Untested nodes glow at their real chance of being
dispatched...
- The interface. Vue 3 over server-computed coordinates, with
v0.3.2
v0.3.2
Fixed
- Conflict diagnosis now interrogates the least-corroborated question first. v0.3.1 replaced
the naming-dependent alphabetical order with a hash, which removed the bias but carried no
signal — the culprit landed at position 3.75 of 5, and conflict episodes cost +1.9
probes. A member is only as trustworthy as the question behind it was searched: beating four
refuted rivals is a real search, being the first thing tried while the exclusion inference
quietly retired the rest is not. Ordering by how many siblings were eliminated on their own
evidence moves the culprit to position 2.38 and is still independent of what nodes are named.
Self-play: 16.2 → 15.9 probes, better than the alphabetical order it replaced (16.0).
Added
list_nodes(view=...)— named presets (frontier,settled,verified,revision,stale)
instead of hand-assembled status filters that silently return an empty table.list_nodes(stale_only=True)and aStalecolumn: VERIFIED nodes whose newest evidence names
a commit that is no longer checked out.context_hashhad been captured on every evidence row.LogicalEvidence.source_ref— what was actually run to produce a number (path, URL, CI run id).AGENT_GUIDE.mdnow documents the 3 MCP prompts and 2 resources.
Changed
- Schema upgrades are now migrated: the belief state is the accumulated
record of a month of experiments, and surviving across sessions is the entire product claim.
Opening a database written by an older release now walks a forward migration chain, applying
each step and its version stamp in one transaction — an interrupted upgrade rolls back to the
version it started from rather than stranding the file between two. Every migration is written
to theeventsaudit log asSchemaMigrated, so "why does this differ from my backup?" is
answerable from the same place as every other question about the workspace. - Schema
"8"→"9"addsevidence.source_ref; the 8→9 migration is additive
(ALTER TABLE ... ADD COLUMN) and cannot lose data. - A database written by a newer hypotree is refused rather than downgraded — the newer code
may have stored things this version cannot represent, and dropping them silently is worse than
stopping. A version with no route forward now says to keep the file and open an issue.
Full Changelog: https://github.com/tygryso/hypotree/blob/master/CHANGELOG.md#032---2026-08-01
Full Commit: https://github.com/tygryso/hypotree/commits/v0.3.2
v0.3.1
v0.3.1
Fixed
- Windows CI: e2e stdio test timeout increased to 900s for slow GitHub Actions runners
- Conflict diagnosis no longer depends on hypothesis naming. Members were stored
alphabetically and interrogated in that order, so the number of diagnostic swaps was
decided by the id prefix rather than by the evidence — a workspace could be made to
converge faster by renaming its hypotheses. Members are now ordered weakest-claim-first
(confirmed_depth, thenevidence_count), tie-broken on a hash of the id, and frozen
at conflict creation soprobe_indexkeeps counting into a stable list. AGENT_GUIDE.md: tool table said 17 and listed 17 while the server registers 18 —
get_workspace_infowas missing. The identity section documented a 5-layer resolution
including aUV_PROJECT_ROOTlayer that no longer exists, a 64-character name limit
that is now 128, and~/.local/shareas the only store location; rewritten to the real
four layers, the[a-z0-9][a-z0-9._~-]{0,127}rule, the cross-platform Windows
reserved-name rejection, and%LOCALAPPDATA%/hypotree --info.
Changed
HypoTreeStore.add_nogoodnow preserves the caller's member order verbatim instead of
re-sorting it. The order is the diagnosis order andprobe_indexcounts into it, so
re-sorting silently overrode the engine's ranking.
Eval harness
pruned_reexecutionno longer fires when a result was obtained before the branch was
pruned. It could not distinguish spending a fresh probe on a dead branch from filing a
measurement already paid for, and the batch-probe protocol makes the latter routine —
a single such event flipped a run's hard gate. Transcript entries now carry a UTC
timestamp; unknown timings still fail open so fabricated results are caught.- Status transitions are now diffed on
(status, reason)rather than status alone.
Ruling a substitute out retracts its exclusion and re-settles it at the same instant,
so a status-only diff saw no change and the mechanism reported zero forever. - Criterion 4 derives its p-value from a seeded 20 000-draw permutation test when the
smallest expected cell falls below 5, instead of reporting a verdict from an asymptotic
χ² that does not apply at that sparsity.
Full Changelog: https://github.com/tygryso/hypotree/blob/master/CHANGELOG.md#031---2026-08-01
Full Commit: https://github.com/tygryso/hypotree/commits/v0.3.1
v0.3.0 - Initial Public Release
Initial Public Release (v0.3.0)
Added
- 18 MCP tools:
generate_learning_path,get_workspace_infoadded (was 17) - 3 MCP prompts as slash commands:
/hypotree-init,/hypotree-next,/hypotree-status - 2 MCP resources:
hypotree://guide,hypotree://state - Server-level
instructionsdelivered at MCP handshake AGENT_GUIDE.mdshipped inside the package, not as a repo-root file- CLI:
hypotree --version,--help,--info - Cross-platform CI: matrix over ubuntu/windows/macOS × py3.10-3.13
- Cross-platform identity resolution: uses
%LOCALAPPDATA%on Windows,$XDG_DATA_HOMEon Linux/macOS eval/runner/config.py: same XDG_DATA_HOME pattern fixed for cross-platform- PyPI metadata: authors, license, classifiers, project.urls, py.typed
hypotree.yaml.templateupdated with cross-platform storage paths
Gate results (Run D, qwen3.6:27b-q8_0, 30 seeds)
- C1b (B vs F): 7.5 steps median reduction (28.9%), δ=0.89, 30/0/0 wins — PASS
- C1a (B vs A): 25.0 steps (57.5%), δ=1.0, 30/0/0 — PASS
- C2 (navigator): worst case 55% below greedy, 0/30 lock-ins — PASS
- C3 (revision): 0 pruned re-executions, 104 revision events, 14/15 conflicts resolved — PASS
- C4 (status utility): χ²=45.2, V=0.185, decision=KEEP — PASS
Engine changes
- Differential ablation:
_eliminate_substitutenow mirrors_confirm_substitute - Fabricated claim_id handling: dropped silently, result kept
- Double-counting fix:
node_createdevents no longer added tobulk_createcounts - Group adoption denominator: compositions without exclusion_group correctly counted
- Record→dispatch fusion:
count_next_targetsparameter onrecord_evidence
Full Changelog: https://github.com/tygryso/hypotree/blob/master/CHANGELOG.md#030---2026-07-31
Full Commit: https://github.com/tygryso/hypotree/commits/v0.3.0