wrap-file <file>Instrument a whole file, in place. A small runtime helper appears next to it and does the writing; nothing else is added.
Local and account-free. No account, no sign-up: whatever you type into the tools stays in this browser's localStorage and never reaches a server.
Our own counter records page opens and finishes: the address leaves, plus one tenth-of-the-text figure. No cookies, no outside trackers, your IP is not stored, Do Not Track is honoured. How to check that
The portal's own repository is not published, so we do not call it open source. What is open:
The portal lives on donations, paid consultations and requested write-ups, and on Workbench sales.
There are no plans to make the courses paid.
Deployment of 16 September 2026, snapshot 95043e5
What has changed on the portal since your last visit. The full list of deployments is in the “What has changed” section on the About page.
A record of every call a running program makes: which functions ran, what they were called with, what they returned and what they threw.
from ouroboros_runtime import log as _ouro_log
@_ouro_log
def discount_rate(total, member):
if member:
return 0.15
if total >= 10000:
return 0.10
return 0.0
@_ouro_log
def apply_discount(total, member=False):
rate = discount_rate(total, member)
return round(total * (1 - rate), 2)
@_ouro_log
def main():
for total, member in [(9999, False), (10000, False), (500, True)]:
print(total, member, apply_discount(total, member=member))
apply_discount("free")
main()
9999 False 9999.0
10000 False 9000.0
500 True 425.0
Traceback (most recent call last):
File "/home/user/shop/discount.py", line 24, in <module>
main()
~~~~^^
File "/home/user/shop/ouroboros_runtime.py", line 234, in wrapper
result = fn(*args, **kwargs)
File "/home/user/shop/discount.py", line 21, in main
apply_discount("free")
~~~~~~~~~~~~~~^^^^^^^^
File "/home/user/shop/ouroboros_runtime.py", line 234, in wrapper
result = fn(*args, **kwargs)
File "/home/user/shop/discount.py", line 13, in apply_discount
rate = discount_rate(total, member)
File "/home/user/shop/ouroboros_runtime.py", line 234, in wrapper
result = fn(*args, **kwargs)
File "/home/user/shop/discount.py", line 6, in discount_rate
if total >= 10000:
^^^^^^^^^^^^^^
TypeError: '>=' not supported between instances of 'str' and 'int'
{"p":"in","t":"2026-09-10T19:51:47.402","id":"fb17367a-f596-4198-9565-add097cefa0e","ci":-1,"th":"2.136045302235648","fn":"discount_rate","a":"'free', False","k":""}
{"p":"out","id":"fb17367a-f596-4198-9565-add097cefa0e","fn":"discount_rate","x":"TypeError: '>=' not supported between instances of 'str' and 'int'","d":3e-06}
{
"ok": true,
"path": "debug.info",
"calls_parsed": 9,
"malformed": 0,
"matched": 1,
"returned": 1,
"next_cursor": null,
"in_flight": [],
"in_flight_truncated": false,
"records": [
{
"index": 6,
"started": "2026-09-10T19:51:47.402",
"call_id": "fb17367a-f596-4198-9565-add097cefa0e",
"name": "discount_rate",
"args": "'free', False",
"kwargs": "",
"outcome_kind": "raised",
"outcome": "TypeError: '>=' not supported between instances of 'str' and 'int'",
"duration": 3e-06,
"cpu": null,
"thread": "2.136045302235648"
}
]
}
{
"ok": true,
"path": "debug.info",
"calls_parsed": 9,
"malformed": 0,
"total_calls": 9,
"in_flight": [],
"by_function": [
{
"name": "apply_discount",
"count": 4,
"result": 3,
"raised": 1,
"unknown": 0,
"duration_seconds": {
"min": 7e-05,
"max": 0.000103,
"mean": 8.7e-05,
"total": 0.000347,
"count": 4
}
},
{
"name": "discount_rate",
"count": 4,
"result": 3,
"raised": 1,
"unknown": 0,
"duration_seconds": {
"min": 1e-06,
"max": 3e-06,
"mean": 2e-06,
"total": 7e-06,
"count": 4
}
},
{
"name": "main",
"count": 1,
"result": 0,
"raised": 1,
"unknown": 0,
"duration_seconds": {
"min": 0.000693,
"max": 0.000693,
"mean": 0.000693,
"total": 0.000693,
"count": 1
}
}
],
"by_thread": [
{
"thread": "2.136045302235648",
"count": 9,
"functions": 3,
"cpus": []
}
],
"duration_seconds": {
"min": 1e-06,
"max": 0.000693,
"mean": 0.000116,
"total": 0.001047,
"count": 9
},
"timespan": {
"first": "2026-09-10T19:51:47.402",
"last": "2026-09-10T19:51:47.402",
"seconds": 0.0,
"timestamps_parsed": 9,
"timestamps_unparsed": 0
},
"note": "counts/durations are over completed calls; `duration_seconds` are REAL per-call durations (exit−entry) from each call's `d`. `by_thread` groups calls by the `th` token (CPUs each thread ran on); empty for traces with no thread field. `in_flight` = entered (`p:in`) but never completed. `timespan` is first→last entry time."
}
This is the real output of release v0.6.1, captured by a run on 2026-09-10 — not text typed by hand. The characters come from the run; the font and the highlighting are added, and long lines are wrapped to the window. The whole output is below as text.
The tool writes lines into your source that compute nothing and only record what
happened. The program then runs as usual, and every call leaves two records:
one on entry — the function name and the arguments — and one on exit: the result
or the exception, and the duration. Both carry the same call id and pile up in a
debug.info file, one JSON object per line.
Eight languages: Python, JavaScript/TypeScript, C, C++, Elixir, Go, Java, C#. The record schema is the same for all of them; what differs is the dialect each language prints its arguments in.
There are two ways to read the records. For a person, the command line: filter calls by name, outcome or duration, and aggregate a whole run into counters. For an AI agent, the MCP server: the same operations handed over as tools.
It is not a profiler — the records change the timing of the run. Not a debugger — the program never stops. And not coverage: coverage says a line ran, a record says what it was called with and what came out.
Python 3.12 or newer is required. Any of the three ways puts two commands on PATH — ouroboros and ouroboros-mcp.
brew install digitable-lol/tap/ouroboros
asdf plugin add ouroboros https://github.com/digitable-lol/ouroboros.git
uv tool install git+https://github.com/digitable-lol/ouroboros
wrap-file <file>Instrument a whole file, in place. A small runtime helper appears next to it and does the writing; nothing else is added.
wrap-functions <file> <names>Instrument only the named functions. That is what you do when the trace of the whole file runs hundreds of times longer than the source.
trace <debug.info>Filter calls: by name, by outcome — returned, raised, never came back — by duration, by thread.
trace-stats <debug.info>Aggregate a run into counters: how many calls each function had, how they ended, how long they took and which never returned.
There are 17 commands; these four are where people start. The same operations are handed to an AI agent by the MCP server — 17 tools, with the order of work stated in the server greeting. Release 0.6.1 is backed by 1257 checks and 100.00 % statement and branch coverage.