diff --git a/README.md b/README.md
index b68a684..eedfa75 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,11 @@
-bounded-loops
+bounded-loops: Bounded Loops & Graphs
-An AI coding agent can tell you it finished when it did not.
+Read the paper · arXiv:2609.27871
Research experiments: v0.6.6 · Current software: v0.7.6
+
+AI agents can claim done. Bounded Loops & Graphs verify the result.
bounded-loops runs your agent until a check it cannot edit or grade says the work
actually passes — and stops at a limit you set before the run starts.
@@ -20,7 +22,9 @@ Every run leaves a hash-chained record you can re-verify afterwards.
Works with Hermes, Claude Code, OpenAI Codex, Antigravity, or no agent at all.
-
+
+
+*Replay of the shipped keyless reference example: the worker is a stub, and the pytest gate is real.*
```bash
pip install bounded-loops
@@ -28,10 +32,14 @@ bl loops install bug-fix-red-green
bl run .bounded-loops/loops/bug-fix-red-green --yes # a real planted bug, a real pytest gate, no API key
```
-69 loop packages ship **inside the wheel**; 65 need no API key. `bl loops list` shows them all
-and `bl loops install` copies one into your project — nothing is downloaded, so this works
-offline. Installing is a step because `bl run` writes its ledger beside the loop, and
-`site-packages` is the wrong place to keep run receipts.
+The wheel includes 69 loop packages; 65 need no API key. This repository also ships seven
+digest-pinned graphs. `bl loops install` copies a package offline into your project, where
+`bl run` can write its ledger. Try a [failing test](loops/bug-fix-red-green/),
+[retail margin rule](loops/price-margin-floor/), or [invoice match](loops/invoice-3way-match/).
+Use a graph for joins, repair routes, or approval before an effect.
+
+**Research scope.** The [paper](https://arxiv.org/abs/2609.27871) evaluates v0.6.6;
+the current software is v0.7.6. Later changes are not paper results.
---
@@ -124,46 +132,34 @@ API key.
## See it working
-Every screenshot below is a real run of a shipped reference graph on a laptop, with no
-credential and no network. Nothing is mocked or staged.
-
-```bash
-bl graph run --execute graphs/solo-builder-ship/graph.yaml
-bl monitor # opens on 127.0.0.1, loopback only, one-time token
-```
-
-### The monitor
-
-`bl monitor` is a local web UI over the same run directory the CLI reads. It holds no state
-of its own — close it and nothing is lost, because the receipt log was always the truth.
-
-
+This HTML replay follows an archived local `solo-builder-ship` run from 18 August 2026:
+three passing checks, a join, approval pause, and local effect receipt. Its raw log contains
+machine-local metadata and is not distributed here, so the replay is explanatory. A
+[real-agent loop receipt](docs/real-run-example/) is checked in separately.
-### What the engine actually recorded
+
-Click a node and you get what the receipts say, not a summary of them: the isolation tier the
-OS really enforced, the independent gate's own verdict **and its reason**, and the artifact
-digest.
+**Actual monitor UI:** the existing screenshot shows a selected saved graph, including
+checks, join, approval, and publish nodes.
-
+
-### A human gate, and what saying yes releases
+To execute a reference graph yourself, use a host that can enforce the manifest's declared
+isolation:
-An approval node declares no effects of its own, so "approve this" reads as harmless right up
-until the publish it lets through. The confirm panel names the effects downstream of the gate
-and flags the ones stopping the run will not take back.
-
-Note the gate line: **"no verdict — the gate has not evaluated this node."** A human hold is
-not a gate pass, and the UI says so rather than painting a green check.
-
-
-
-### A shareable report
+```bash
+bl graph run --execute graphs/solo-builder-ship/graph.yaml
+bl monitor # opens on 127.0.0.1, loopback only, one-time token
+```
-`bl graph arena --run ` writes one self-contained HTML file. No server, no network, no
-build step — send it to someone who was not there.
+`bl monitor` is a local view over the same run directory the CLI reads. A node detail shows
+the recorded isolation tier, gate verdict and reason, and artifact digest. A human hold
+shows **no gate verdict** until its gate runs; its approval panel names the downstream effects
+the decision releases. `bl graph arena --run ` writes a self-contained read-only report.
-
+Existing [node detail](assets/screenshots/monitor-node-detail.png),
+[approval](assets/screenshots/monitor-approval-preview.png), and
+[Arena](assets/screenshots/arena-report.png) screenshots show more of the current UI.
---
@@ -289,7 +285,10 @@ A local web UI over the run directory — live DAG, per-node evidence, spend, an
controls. It detects which agent CLIs you already have logged in and lists your runs; it never
asks for a credential of its own.
-
+
+
+The [empty workspace view](assets/screenshots/monitor-workspace.png) is available for setup context;
+the selected-run screenshots above show the graph and its evidence.
It is a **view**, not a service. Loopback bind, a token per invocation that never touches disk,
and a same-origin requirement on every data route — so a page in another tab cannot drive it
@@ -425,7 +424,7 @@ review.** The cross-model audit engine (`--audit-plan`) is the overlay for that.
|---|---|
| Gate-verified DAG (worker ≠ gate, controller-enforced identity check) | Shipped — object-identity check only: one object cannot hold both roles, but gate logic is not proven independent ([detail](#the-engine-loop)) |
| Local-CLI + BYOK/HTTPS connectors via `bl graph run --execute` | Shipped |
-| `kind: loop` nodes executable via `bl graph run --execute` | Shipped — digest-pinned package, receipt-verifying gate. `isolation` is per-node and never defaulted; the six reference graphs pin `process_restricted`. `workspace_only` is NOT an OS sandbox |
+| `kind: loop` nodes executable via `bl graph run --execute` | Shipped — digest-pinned package, receipt-verifying gate. `isolation` is per-node and never defaulted; the seven reference graphs pin `process_restricted`. `workspace_only` is NOT an OS sandbox |
| No-secret egress broker (single-use leases; SSRF / DNS-rebind denied) | Shipped |
| Hash-chained event log; on resume, full chain re-verified | Shipped — local runs marked `LOCAL/UNVERIFIED` |
| Cross-model audit coverage gate (`--audit-plan` → Arena verdict) | Shipped — read-side; independence is receipt-asserted |
@@ -580,8 +579,22 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). A contributed loop needs a real failing
seed, a passing fix, a testable done-condition, and `bl lint --contrib` compliance.
Never use "an LLM decides" as the gate.
-Research citation metadata is in [CITATION.cff](CITATION.cff). Report gate
-bypasses or sandbox escapes privately through [SECURITY.md](SECURITY.md).
+For the research paper, copy this BibTeX entry:
+
+```bibtex
+@article{bhardwaj2026boundedloops,
+ title={Bounded Loops: Pre-Run Spend Bounds, Proved Termination, and Verified Completion for Agent Harnesses},
+ author={Bhardwaj, Varun Pratap and Singh, Garima and Bhardwaj, Arun Pratap},
+ year={2026},
+ eprint={2609.27871},
+ archivePrefix={arXiv},
+ primaryClass={cs.SE},
+ url={https://arxiv.org/abs/2609.27871}
+}
+```
+
+[CITATION.cff](CITATION.cff) cites the software. Report gate bypasses or sandbox
+escapes privately through [SECURITY.md](SECURITY.md).
[Apache-2.0](LICENSE). Copyright © 2026 Varun Pratap Bhardwaj / Qualixar,
an independent AI Reliability Engineering research initiative.
diff --git a/assets/launch-graph-replay-light.gif b/assets/launch-graph-replay-light.gif
new file mode 100644
index 0000000..420c497
Binary files /dev/null and b/assets/launch-graph-replay-light.gif differ
diff --git a/assets/launch-loop-proof-light.gif b/assets/launch-loop-proof-light.gif
new file mode 100644
index 0000000..df3d166
Binary files /dev/null and b/assets/launch-loop-proof-light.gif differ
diff --git a/graphs/README.md b/graphs/README.md
index ad76027..3c302ab 100644
--- a/graphs/README.md
+++ b/graphs/README.md
@@ -1,5 +1,7 @@
# Reference graphs
+For the research model behind bounded loops composed as graphs, read the [paper on arXiv](https://arxiv.org/abs/2609.27871). Its reported experiments use v0.6.6; this guide describes the current v0.7.6 repository.
+
Each directory here is one portable graph that **composes loop packages from [`loops/`](../loops/)**.
Nothing here is a new example: every node's gate is a real mechanical check that shipped and was
validated before the graph engine could run it.
@@ -17,7 +19,7 @@ so the generator and the test read one definition rather than two that agree by
## What ships
-Six graphs, one per domain, composing **24 distinct shipped loop packages**. Every one has the same
+Seven graphs across seven workflow domains, composing **28 distinct shipped loop packages**. Every one has the same
skeleton — three parallel checks → join → approval → one irreversible effect, plus a conditional
`when: failed` branch to a remediation loop — because that skeleton is what exercises the engine:
fan-out, cross-node causality, the guard grammar, a human checkpoint, and exactly one effect.
@@ -29,6 +31,7 @@ fan-out, cross-node causality, the guard grammar, a human checkpoint, and exactl
[`marketing-campaign-release`](marketing-campaign-release/) | marketing | content-editor | publish the campaign page |
[`engineering-release-gate`](engineering-release-gate/) | IT / development | release-manager | cut the release tag |
[`customer-data-request`](customer-data-request/) | customer | privacy-officer | send the data-subject response |
+[`operations-incident-gate`](operations-incident-gate/) | operations | sre-lead | send the incident-response notification |
[`solo-builder-ship`](solo-builder-ship/) | personal projects | maintainer | ship the release notes |
No shipped loop carries a `customer`, `personal-projects` or `marketing` `role:` tag. Those three
@@ -37,11 +40,20 @@ on why the domain belongs to the graph.
## Running a graph end to end
-All six reference graphs run end to end, keyless, with no spend — **from a checkout**.
+The seven reference graphs use keyless stub workers and mechanical gates. They need no
+metered model calls, but execution requires a host that can enforce each node's declared
+isolation. An explicit parameterized test executes each reference graph
+through its loop gates and join to the human approval checkpoint; separate approval
+tests cover the resume-and-publish control path.
Every loop node pins its package by content digest, and the `loops/` catalog ships in the
repository, not in the wheel, so a `pip install` alone leaves nothing for those digests to
resolve against. `git clone` first, or pass `--loop-roots` pointing at your own catalog.
-`tests/graphs/test_reference_graphs.py` runs all six on every CI push.
+The structural and digest checks run in the default test suite. The execution-to-approval checks are
+marked `external_tool` and must be selected explicitly on a capable host:
+
+```bash
+uv run pytest -q -m external_tool tests/graphs/test_reference_graphs.py
+```
```bash
bl graph run graphs/finance-payment-assurance/graph.yaml --execute --out /tmp/bl-run
diff --git a/npm/README.md b/npm/README.md
index c6a5d43..701b3d5 100644
--- a/npm/README.md
+++ b/npm/README.md
@@ -1,5 +1,7 @@
# bounded-loops
+**Research paper:** [Bounded Loops · arXiv:2609.27871](https://arxiv.org/abs/2609.27871). The paper's experiments use v0.6.6; the current software is v0.7.6.
+
[](https://pypi.org/project/bounded-loops/)
[](https://www.npmjs.com/package/bounded-loops)
[](https://pypi.org/project/bounded-loops/)
diff --git a/pyproject.toml b/pyproject.toml
index b37ec2a..2dc16e4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,11 +25,12 @@ dependencies = [
]
[project.urls]
-Homepage = "https://qualixar.com"
+Homepage = "https://github.com/qualixar/bounded-loops"
Repository = "https://github.com/qualixar/bounded-loops"
Documentation = "https://github.com/qualixar/bounded-loops/tree/main/docs"
Changelog = "https://github.com/qualixar/bounded-loops/blob/main/CHANGELOG.md"
Issues = "https://github.com/qualixar/bounded-loops/issues"
+Paper = "https://arxiv.org/abs/2609.27871"
[project.optional-dependencies]
qualixar-gates = [
diff --git a/tests/release/test_hermes_plugin.py b/tests/release/test_hermes_plugin.py
index b31a576..95c92f8 100644
--- a/tests/release/test_hermes_plugin.py
+++ b/tests/release/test_hermes_plugin.py
@@ -13,6 +13,7 @@
from pathlib import Path
import sys
import types
+from typing import Any
import yaml
@@ -87,7 +88,7 @@ def launch(self, request):
fake_agent = types.ModuleType("agent")
fake_lifecycle = types.ModuleType("agent.subagent_lifecycle")
- fake_lifecycle.SubagentLaunchRequest = Request
+ setattr(fake_lifecycle, "SubagentLaunchRequest", Request)
monkeypatch.setitem(sys.modules, "agent", fake_agent)
monkeypatch.setitem(sys.modules, "agent.subagent_lifecycle", fake_lifecycle)
@@ -149,7 +150,7 @@ def test_active_graph_discovery_is_silent_when_workspace_has_no_run(tmp_path) ->
def test_register_activates_the_packaged_skill_and_honours_executable_setting(monkeypatch) -> None:
module = _load_plugin()
- calls = {"commands": {}, "hooks": {}, "skills": []}
+ calls: dict[str, Any] = {"commands": {}, "hooks": {}, "skills": []}
class Context:
subagent_lifecycle = object()
diff --git a/uv.lock b/uv.lock
index 16206ab..871cadc 100644
--- a/uv.lock
+++ b/uv.lock
@@ -371,20 +371,20 @@ wheels = [
[[package]]
name = "httpcore2"
-version = "2.10.0"
+version = "2.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" },
{ name = "truststore", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a9/83/a896fc59940fc5a6e2aff3a4be1d92fa890112936803b331cae75a993c34/httpcore2-2.10.0.tar.gz", hash = "sha256:13c0cc3d1919d4f28457f60cd2c2abe04113a8af184ccf1142811beba936f9dc", size = 67427, upload-time = "2026-08-09T09:11:32.123Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/4f/d149104195a35e2853a2fc203a8e3477747e58c80e17dda686dace174383/httpcore2-2.10.0-py3-none-any.whl", hash = "sha256:7df06cfb34070cae4f7c89be69dc1095eca138e9704ceffb98d25c1912ab6f01", size = 83000, upload-time = "2026-08-09T09:11:29.555Z" },
+ { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" },
]
[[package]]
name = "httpx2"
-version = "2.10.0"
+version = "2.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
@@ -394,9 +394,9 @@ dependencies = [
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bd/3d/f9a8c07a3884f3e5b26205e8436a18b3af61c5d53192c3bea235574dbbec/httpx2-2.10.0.tar.gz", hash = "sha256:8741d7329fe2c7885fc9ceb61c8217acfb87a85f75723714b89ebf7ad7196338", size = 98749, upload-time = "2026-08-09T09:11:33.24Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b9/6d/a637d52449d98a6892d9a4dc0262587afdb6a66f201871842dce5a97b1c1/httpx2-2.10.0-py3-none-any.whl", hash = "sha256:5e3194a432701e1cc6f69a8b1b2fa199ef907013fede8d9a09a2c5b7b8141a18", size = 94355, upload-time = "2026-08-09T09:11:30.882Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" },
]
[[package]]