openloops.tools#

The package’s single list of operations: plain functions, JSON-ready dicts.

Every surface openloops has or might grow — the ol command today, an MCP server or an HTTP endpoint later — dispatches from this module and nothing else. The functions here know nothing about argument parsers, transports or agent hosts; they take flat serialisable arguments and return flat serialisable results, and printing is somebody else’s job.

Keeping one list is what stops two surfaces from drifting apart. A parity test between two surfaces is a sign that there are two implementations, and there is one here.

Module Attributes

ROW_FIELDS

The keys every ls() row carries, whatever the digest happens to record.

Functions

sync(*[, source, since_days, force, ...])

Read what changed in the sessions and write the digests.

ls(*[, state, source, project, confidence, ...])

The digests in the store, newest last-turn first.

show(session, *[, source, digests_store])

One digest in full, found by session id or by a unique prefix of one.

status(*[, source, since_days, ...])

Where everything is, how much of it there is, and how stale the cache is.

owed(*[, verify, owners, trusted_owners, ...])

What you still owe your agents, with each obligation re-checked against the world.

blocked(*[, resolve, owners, repos, query, ...])

What your repositories are waiting on — and what is no longer waiting.

dashboard(*[, verify, resolve, owners, ...])

All three answers as one self-contained HTML page, for looking at rather than reading.

openloops.tools.ROW_FIELDS = ('session', 'source', 'state', 'title', 'ai_title', 'project', 'branches', 'started', 'ended', 'last_turn', 'turns', 'model', 'confidence', 'verified')#

The keys every ls() row carries, whatever the digest happens to record.

openloops.tools.blocked(*, resolve=True, owners=None, repos=None, query='is:blocked', limit=50, timeout=30.0, issues_source=None, blockers_source=None)[source]#

What your repositories are waiting on — and what is no longer waiting.

The sibling of owed(): an obligation points at a person, a blocker edge points at another repository, and both are commitments nothing is watching. Lists the open issues carrying a blocked_by dependency across owners (or exactly the repos you name), resolves every edge, and reports three states: unblocked (every blocker closed — the work is free and nobody has been told), blocked (naming the foreign repository it waits on) and unknown (nothing could be resolved). Nothing is ever closed, relabelled or written; see openloops.blockers for what discovery costs and what it is measured to miss.

The result is an envelope rather than a list, because listed=False — discovery itself failed — must not be readable as “nothing is waiting”.

Return type:

dict[str, Any]

>>> report = blocked(issues_source=[], blockers_source={})
>>> report['listed'], report['counts']['total']
(True, 0)
openloops.tools.dashboard(*, verify=True, resolve=True, owners=None, repos=None, limit=50, candidate_limit=50, max_sessions=40, title='Open Loops Board', standalone=True, path=None, made_at=None, owed_report=None, blocked_report=None, sessions=None, digests_store=None)[source]#

All three answers as one self-contained HTML page, for looking at rather than reading.

Runs owed(), blocked() and ls(), then renders openloops.dashboard.render_dashboard() over what they returned. The page is a snapshot: it stamps the moment it was made and states, in its largest type, that it has checked nothing since — because a published page cannot. Nothing is written to GitHub, here or anywhere in openloops.

path also writes the page there, scrubbed, and reports where it went. standalone=False returns the same page without the document scaffold, for a host that supplies its own <head>.

Each of the three reads is a seam: pass owed_report=, blocked_report= or sessions= and that one is not performed. With all three passed this reaches no network and needs no gh, which is how the tests run.

counts comes back alongside the page and carries null — not 0 — for any figure that could not be established, so a caller reading the JSON is held to the same three-state rule as a reader looking at the page.

Return type:

dict[str, Any]

>>> result = dashboard(owed_report={'listed': True, 'counts': {'open': 0}, 'rows': []},
...                    blocked_report={'listed': False, 'error': 'no gh'},
...                    sessions=[], made_at='2026-01-01T00:00:00Z')
>>> result['counts']['free_to_proceed'] is None, result['counts']['unknown'] is None
(True, True)
openloops.tools.ls(*, state='open', source=None, project=None, confidence=None, limit=20, digests_store=None)[source]#

The digests in the store, newest last-turn first.

state is open, archive or all. source, project and confidence narrow by the digest’s own header fields. Each row is that front matter plus its key.

confidence='high' is the one worth knowing about: most sessions land in open, and a good few of those are open only because nothing said otherwise. Filtering to high leaves the ones where the session itself said something.

Return type:

list[dict[str, Any]]

>>> rows = ls(digests_store={'m/open/s1.md':
...     '---\nsession: s1\nstate: open\nlast_turn: T9\n---\n'})
>>> rows[0]['session'], rows[0]['key']
('s1', 'm/open/s1.md')
openloops.tools.owed(*, verify=True, owners=None, trusted_owners=None, label='manual-task', limit=50, timeout=30.0, predicate_timeout=20.0, issues_source=None, run_predicate=None)[source]#

What you still owe your agents, with each obligation re-checked against the world.

Lists the open manual-task issues across owners and evaluates the **Verify:** predicate each one carries, reporting three states: open, discharged (the predicate returned 0 — done, but the issue is still open) and unknown (nothing could be checked). Nothing is ever closed, relabelled or written; see openloops.obligations for the trust boundary that evaluating a predicate crosses, and verify=False for the way to read without executing.

The result is an envelope rather than a list, because listed=False — the query itself failed — must not be readable as “nothing owed”.

Return type:

dict[str, Any]

>>> report = owed(issues_source=[], run_predicate=lambda command: 0)
>>> report['listed'], report['counts']['total']
(True, 0)
openloops.tools.show(session, *, source=None, digests_store=None)[source]#

One digest in full, found by session id or by a unique prefix of one.

An exact id always wins over a prefix, so a session whose id happens to be a prefix of another stays reachable. source narrows a store that several machines write into, where the same session can legitimately appear more than once.

Return type:

dict[str, Any]

>>> store = {'m/open/abcdef.md': '---\nsession: abcdef\n---\nbody'}
>>> show('abc', digests_store=store)['key']
'm/open/abcdef.md'
openloops.tools.status(*, source=None, since_days=None, digests_store=None, transcript_source=None)[source]#

Where everything is, how much of it there is, and how stale the cache is.

Reports the cache’s age because a read served from a cache that nothing has refreshed is the failure openloops is built to avoid — a periodic job that died leaves a confident, months-old answer behind, and the only defence is saying how old the answer is.

Return type:

dict[str, Any]

openloops.tools.sync(*, source=None, since_days=None, force=False, transcript_source=None, digests_store=None, state_dir=None)[source]#

Read what changed in the sessions and write the digests. Returns a summary.

since_days bounds the scan to recently-modified transcripts; force re-derives everything, which must produce an identical store.

state_dir scopes the change-detection cache. It matters because the seams do not isolate on their own: swapping transcript_source for a fixture and leaving the cache alone writes the fixture’s revisions into the caller’s real one.

Return type:

dict[str, Any]