Per the v0.1 spec: if a task isn't on a milestone, it's an issue. The repo now has a remote, so new follow-ups are filed on the GitHub tracker (the verification-model sharpening lives there as #18 through #20, prioritised in ROADMAP.md). This file keeps the original spec non-goals and the launch follow-ups, with status as of the 0.1.0 release (shipped 2026-06-23).
- Multi-language support: v0.1 is Python-only (AST hashing and pytest runner are Python-specific). Each language needs a normalised-AST hasher and a test-runner adapter. (Go and TypeScript shipped post-0.1.0, chosen by impl extension; see ROADMAP.)
- Content-addressed implementation store: implementations remain plain files on disk; only their hashes live in the store. (v0.2 enabler, see ROADMAP.)
- Hosted / team-shared store: single-process, single sqlite file for now. The shared verification cache is the v0.2 theme (see ROADMAP).
- Semantic diff rendering:
put_contractreports that a contract changed and who is invalidated, not a pretty diff of what changed. (Queued for v0.2.) - Tessl spec-format compatibility: import/export adapter for Tessl-style spec files. Format is frozen for v0.1 as specced.
- New syntax of any kind: contracts stay plain YAML.
- PyPI release: ✓ Resolved in 0.1.0. Published as
heddle-mcp(the bareheddlename was held by a third-party placeholder) via GitHub Actions Trusted Publishing; the import name and CLI stayedheddlethrough 0.3.3. Since the 0.4.0 rename the distribution, import name, and CLI are allhashloom— see item 10. See RELEASING.md. - README gif: ✓ Resolved in 0.1.0. Recorded and embedded under the CI badge; storyboard in docs/demo.md.
- Pre-existing stale bytecode: partially mitigated. The verification runner passes
-B/PYTHONDONTWRITEBYTECODEso its own runs never cache bytecode, but with the defaultpycache_trust: truea stale user-written__pycache__(same size, same mtime second) could still be loaded. Mitigation shipped: setpycache_trust: false(or--no-pycache-trust) to clear__pycache__before each verify run. Making that the default remains a possible future change. - Name finalization: ✓ Resolved, then revised. "heddle" was the locked project name through 0.3.3 (PyPI:
heddle-mcp); renamed to hashloom in 0.4.0 after two unrelated "heddle" MCP servers surfaced — see the CHANGELOG's 0.4.0 entry. The barehashloomPyPI name is ours. - Demo gif: ✓ Resolved in 0.1.0 (same work as #8): the Claude Code to heddle loop is recorded and embedded.
- Contract names are global: ✓ Resolved. Subdirectory namespaces shipped, so the same short name can live in different folders (
contracts/billing/invoice.yamlis the contractbilling/invoice). Onecontracts/directory per project still holds. - TypeScript runner — live vitest/jest coverage: the TS adapter auto-detects vitest / jest /
node:testfrompackage.json.node:test(the zero-dependency default) is exercised end-to-end intests/test_typescript_adapter.py; the vitest/jest backends (a shared jest-shaped JSON report) are implemented and the detection logic is unit-tested, but a live vitest/jest run is not yet in heddle's own suite. Follow-up: add a hermetic vitest fixture and validate the JSON parser against a real run. - TypeScript
node:teststrip-types limits: undernode:test,.tsruns via Node's native type-stripping (Node >=22.6), which can't erase type-only constructs that need code generation (enum, namespaces, parameter properties) without--experimental-transform-types; such a test file surfaces astests_failed_to_run. Projects using those should declare vitest or jest (which transpile fully) — the adapter will auto-detect and use them. - Pull-without-reindex freshness gap:
verifycomputes keys fromstore.db's contract hashes, so agit pullwithouthashloom indexcan serve a cached-pass that no longer matches the contracts on disk. Identical with or without a shared store (the key is computed client-side). Candidate fix: a verify-time freshness check againstcontracts/on disk. Surfaced during the cross-graph-invalidation analysis. - Post-revert spurious re-run: the local name-keyed stale row survives a
git revert, shadowing a still-valid shared green at the old key and forcing one wasted pytest run. Sound (never a wrong serve), just a perf papercut. Candidate fix: a guarded fall-through inLayeredStore.get_verificationwhen the local row is stale but the shared row at the same key is green — sound only under the invariant that local staleness is content-triggered, so it needs its own reviewed change. - Automatic counter-evidence revocation: when a client's real run fails at a key where the shared cache holds a green, that contradiction is the flaky-test signature and could auto-fire
POST /stale. Deferred deliberately: one broken-env laptop computing the same key would tombstone keys green for everyone else — permanently under first-writer-wins, re-firing on every verify (a treadmill even against the restore route). Update: the declared dependency set is now in the key, so the false-contradiction class shrank to installed-env drift (a venv disagreeing with its lockfile) — narrower, but still exactly the broken-laptop case, so the trigger stays deferred until that class has a story. - Verdict-row retention: old-key verification rows are never deleted, locally or (now) server-side, and
status'sstale_verificationslists a name forever after a change even once its current key is green. Rows are tiny and content-addressed, so this is bounded-growth housekeeping, not soundness — but retention/GC should be a conscious decision. Also filed here: the optionalshared_stale_refusedobservability counter skipped in the revocation PR to keep the client diff at zero. RemoteStore.get_blobtrusts the server: the returned content is never re-hashed against the requested hash. One-line hardening (hashlib.sha256(content).hexdigest() == blob_hashor return None) that keeps a compromised/buggy shared cache from serving wrong weft.- Dep-source discovery is root-only: the dependency-set suffix probes fixed filenames at the project root (matching the runner-detection precedent). Monorepo/multi-file shapes escape it:
go.work, pnpm/yarn workspaces, Maven child modules,requirements.txt-r/-cincludes (only the root file is hashed),go.modreplace ... => ../localdirectives (local replaces never entergo.sum), Gradle version catalogs (gradle/libs.versions.toml— versions referenced frombuild.gradlelive outside the hashed file). All lean unsound-quiet (a change that should bust doesn't) — revocation covers discoveries; recursive/workspace-aware discovery is a conscious future change. - Strict resolved-env grain: the identity hashes the declared set (the committed lockfile), not the installed environment. An opt-in stricter grain (e.g.
importlib.metadataenumeration of the resolved interpreter) would catch venv-disagrees-with-lockfile drift, but any key-affecting per-client knob permanently partitions the shared cache by knob population — the reason config knobs were rejected for the base feature. If built, it must be a project-level (committed) setting, not client config. serve --pythonverify/status asymmetry (pre-existing): the MCP server threads--pythonintoapi.verifybutapi.statuscallstoolchain_identity(root)with no override, so under a version-different overridestatuscan call dirty whatverifyjust greened. The dependency-set suffix is override-independent, so it cannot widen this gap — but the version core can. Fix candidates: thread the override intobuild_server's status closure, or report both identities.- C# example project and live-runner breadth: the C# adapter shipped with a hermetic xUnit e2e in
tests/test_csharp_adapter.py, but there is noexamples/csharp-*project (Go/TS/Java each have one) and no live NUnit/MSTest run in the suite — both ride the same VSTestFullyQualifiedName~filter and console-logger output as xUnit, so they're expected-compatible but unproven, the same gap #13 tracks for vitest/jest. Also untested live: xunit v3 / Microsoft.Testing.Platform projects, whosedotnet testoutput format differs (failures there surface astests_failed_to_run, conservative). Follow-up: add an example project, plus an NUnit fixture and an MTP-output parse case.