diff --git a/README.md b/README.md index b68a684..eedfa75 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ bounded-loops logo

-

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.

-![Ungated agent claim compared with a gate-verified bounded loop](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/demo.gif) +![Light-theme HTML replay of the shipped ungated claim and pytest-gated bounded loop](https://raw.githubusercontent.com/qualixar/bounded-loops/bc752dd21512e8c3f68e0504bc93443b2414fecd/assets/launch-loop-proof-light.gif) + +*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. - -![The bounded-loops monitor showing a completed seven-node graph run](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/screenshots/monitor-dag.png) +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 +![Light-theme replay of the archived seven-node local graph run](https://raw.githubusercontent.com/qualixar/bounded-loops/bc752dd21512e8c3f68e0504bc93443b2414fecd/assets/launch-graph-replay-light.gif) -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. -![Node detail showing enforced isolation controls, the gate verdict, and the artifact digest](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/screenshots/monitor-node-detail.png) +![Actual bounded-loops monitor UI with a completed seven-node graph selected](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/screenshots/monitor-dag.png) -### 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. - -![The approval panel naming external_write as an effect that cannot be undone](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/screenshots/monitor-approval-preview.png) - -### 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. -![The Arena report for a completed run](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/screenshots/arena-report.png) +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 monitor's workspace rail listing detected orchestrators and runs](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/screenshots/monitor-workspace.png) +![Actual monitor UI showing a selected graph node, its gate verdict, isolation tier, and artifact digest](https://raw.githubusercontent.com/qualixar/bounded-loops/main/assets/screenshots/monitor-node-detail.png) + +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. + [![PyPI version](https://img.shields.io/pypi/v/bounded-loops)](https://pypi.org/project/bounded-loops/) [![npm version](https://img.shields.io/npm/v/bounded-loops)](https://www.npmjs.com/package/bounded-loops) [![Python versions](https://img.shields.io/pypi/pyversions/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]]