Skip to content

Releases: tygryso/hypotree

v0.6.0

Choose a tag to compare

@tygryso tygryso released this 20 Aug 18:39
Immutable release. Only release title and notes can be modified.

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_status now 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_evidence accepts only an attestation_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 HypoTreeEngine raised — __init__.py contained a docstring and a version string and
    nothing else — so every consumer reached through hypotree.engine into 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.
    • .call never raises. A misspelled node id, a missing success, 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=True exposes the eleven sensors and refuses every
      write, and include/exclude narrow 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) and essential (ship these when context is tight). get_next_targets is
      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 only node_id for result items; dispatch still refuses logical
    reports without success and accepts success-less evidence_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's create_hypotheses was
    missing exclusion_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 — the source_ref class 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 no duration_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 an INSERT OR IGNORE with an unconditional EdgeAdded event, 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_error rewrote 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 targeted increment_infra_retries, mirroring increment_evidence_count.
  • get_next_targets could mint a lease that was already expired. The reclaim sweep
    compares inclusively, so lease_ttl_s=0 returned the node to the frontier on the next
    call while the caller believed it held the work. renew_claim has 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]:7331 contains 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_claims resume
    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-path or
    HYPOTREE_DB_PATH.
    Embedded hosts can keep state in their own isolated
    storage namespace and later launch hypotree --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 hypotree no longer imports mcp, 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() and SERVER_INSTRUCTIONS keep their names and locations.
  • 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 explicit busy_timeout, cache size and in-memory temp store. Every
    mutation writes its audit event in the same transaction, so the default FULL was
    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.
    New convergence_verdict returns 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 boolean convergence_gate is 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 ...
Read more

v0.5.0

Choose a tag to compare

@tygryso tygryso released this 08 Aug 12:40
Immutable release. Only release title and notes can be modified.

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: omit target_metric and 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 reports created=False rather than raising, so re-sending a plan
      is safe. Tool count 18 → 19.
  • 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. Returns settled_in_window,
    withdrawn_in_window and probes_in_window alongside the lifetime counters, which keep
    describing the whole history — "what did this cost in total" does not become a different
    question this week. Combine with as_of for 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 since on the MCP tool and GET /api/learning-path?since=.
  • Recorded artifacts are read back in get_evidence_history and 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
    for context_hash a year earlier.

  • status_filter accepts EXHAUSTED. It is the status the exclusion inference produces most
    and the one that could not be queried directly; the SDK enforces inputSchema, so the filter
    was rejected outright and the view="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
      as GET /api/counterfactual.
  • A short batch now says a competing answer is being held back. get_next_targets has 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 was claimed=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 carries same_question_withheld and says so in the rationale.
    Silent on a full batch, and silent when the batch was short because the work genuinely ran out.

Changed

  • --cost-aware is 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; recording duration_s and estimated_cost is 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_evidence accepts duration_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 scores value / 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_cost on create_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 real duration_s overrides 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 after COST_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_s is 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, θ/c and θ 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.py plays 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...
Read more

v0.4.2

Choose a tag to compare

@tygryso tygryso released this 05 Aug 10:06
Immutable release. Only release title and notes can be modified.

v0.4.2

Fixed

  • Align choose_port socket flags with asyncio on Windows. On Windows, SO_REUSEADDR allows probing sockets to bind over active listeners, reporting occupied ports as free and causing asyncio.create_server to crash with address in use. choose_port now applies SO_REUSEADDR only on POSIX (os.name == 'posix'), matching asyncio'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

Choose a tag to compare

@tygryso tygryso released this 04 Aug 16:29
Immutable release. Only release title and notes can be modified.

v0.4.1

Fixed

  • pip install hypotree was broken by the release of mcp 2.0.0. The
    dependency was declared as mcp>=1.28.1 with 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 now mcp>=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

Choose a tag to compare

@tygryso tygryso released this 04 Aug 16:13
Immutable release. Only release title and notes can be modified.

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_nodes derives each node's parent_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_edge ran a full is_directed_acyclic_graph() check per edge, so
      reloading E edges cost O(E·(V+E)). New add_edges_bulk checks 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 by dst.
    • get_next_targets syncs 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.

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 ended empty_frontier with 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_closed on create_hypotheses (schema 10, nodes.exclusion_closed,
      NOT NULL DEFAULT 1 — exactly what every group written before it meant). Defaults to
      true. Pass false where 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 as UNTESTED and 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_deduced reads 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_question now 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
      end dead_question naming the incomplete axis, against 0/150 before — every one of which
      was empty_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 --dashboard while 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 PORT picks the first port to try (it still probes upward, so several
      workspaces can be open at once).
    • --no-dashboard opens no socket at all.
    • --no-mcp replaces --dashboard-only and 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_info now returns dashboard_url, and the new
    hypotree://dashboard resource returns it on its own. Both carry the session token, which is
    the whole credential; null means 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 — PRUNED does 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
      EXHAUSTED was 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_question is 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.
  • record_evidence is batch-native. Pass results=[{node_id, success, depth, claim_id}, …]
    to report every experiment from one turn in one call. create_hypotheses, update_status
    and get_next_targets have all been batch-native for a while; record_evidence was 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=1 shorthand.
    • 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 bad node_id destroys evidence.
      The refusals come back under failed, named and explained.
    • The fused dispatch runs once, after the whole batch, and is still a top-up.
  • get_conflicts now reports skipped_no_substitute per member.
  • A read-only web dashboard: hypotree --dashboard [PORT] and hypotree --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-only starts 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-zoom for
      hardware-accelerated pan and zoom. Untested nodes glow at their real chance of being
      dispatched...
Read more

v0.3.2

Choose a tag to compare

@tygryso tygryso released this 01 Aug 17:34

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 a Stale column: VERIFIED nodes whose newest evidence names
    a commit that is no longer checked out. context_hash had been captured on every evidence row.
  • LogicalEvidence.source_ref — what was actually run to produce a number (path, URL, CI run id).
  • AGENT_GUIDE.md now 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 the events audit log as SchemaMigrated, so "why does this differ from my backup?" is
    answerable from the same place as every other question about the workspace.
  • Schema "8" → "9" adds evidence.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

Choose a tag to compare

@tygryso tygryso released this 01 Aug 12:40

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, then evidence_count), tie-broken on a hash of the id, and frozen
    at conflict creation so probe_index keeps counting into a stable list.
  • AGENT_GUIDE.md: tool table said 17 and listed 17 while the server registers 18 —
    get_workspace_info was missing. The identity section documented a 5-layer resolution
    including a UV_PROJECT_ROOT layer that no longer exists, a 64-character name limit
    that is now 128, and ~/.local/share as 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_nogood now preserves the caller's member order verbatim instead of
    re-sorting it. The order is the diagnosis order and probe_index counts into it, so
    re-sorting silently overrode the engine's ranking.

Eval harness

  • pruned_reexecution no 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

Choose a tag to compare

@tygryso tygryso released this 31 Jul 19:21

Initial Public Release (v0.3.0)

Added

  • 18 MCP tools: generate_learning_path, get_workspace_info added (was 17)
  • 3 MCP prompts as slash commands: /hypotree-init, /hypotree-next, /hypotree-status
  • 2 MCP resources: hypotree://guide, hypotree://state
  • Server-level instructions delivered at MCP handshake
  • AGENT_GUIDE.md shipped 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_HOME on 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.template updated 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_substitute now mirrors _confirm_substitute
  • Fabricated claim_id handling: dropped silently, result kept
  • Double-counting fix: node_created events no longer added to bulk_create counts
  • Group adoption denominator: compositions without exclusion_group correctly counted
  • Record→dispatch fusion: count_next_targets parameter on record_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