Skip to main content
Glama

sensorium

Record what a program actually did; ask it questions afterward.

Sensorium records one run of a program — every call, return and exception, with captured values — into a SQLite trace, and answers debugging questions from that trace in dense plain text shaped for a language model to read. It exists because reading logs is reading a diary; this is watching the execution.

Three recorders write that trace, and one command line reads it:

  • Pythonsensorium run -- <command> wraps one run with PEP 669 (sys.monitoring) instrumentation. Python 3.12+, no runtime dependencies.

  • Rustcargo sensorium test|run instruments a workspace's own crates at build time and writes one trace per process. Stable rustc, Linux.

  • TypeScriptsensorium ts run [--focus <spec>]… -- vitest run … (or -- node --test …) transforms the consumer's own sources at load time and writes one trace per test-file process. Node 24, vitest 4.1.

All three write trace format 4, so every query below answers on any of the three kinds of trace — and where a recorder declares a capability it does not have, the query refuses by name instead of answering from data that was never recorded. Python is documented first; Rust and TypeScript have their own sections here, and rust/README.md and typescript/README.md are their full references.

Two commitments run through all of it:

  • The instrument never answers from data it does not have. Truncated captures are marked and counted, sites a predicate could not be evaluated at are counted rather than skipped, a recording that died is labelled, and a rerun that turned out to be a different execution says so permanently.

  • Nothing here guesses. Every answer is a deterministic function of the recorded trace, and where the trace cannot settle a question the output says so instead of inferring. Sensorium reports; the agent reading it reasons. (refocus is the one command that executes anything — it re-runs your program, which is why its whole job is telling you whether what came back was the same execution.)

Install

uv venv .venv && uv pip install -p .venv/bin/python -e ".[dev]"

Requires Python 3.12+ — the recorder is sys.monitoring and nothing else, so on 3.11 it refuses to start rather than falling back to something weaker. No runtime dependencies; the trace is a SQLite file written with the standard library. One capability is version-gated: witnessing a multiprocessing spawn needs the _posixsubprocess.fork_exec audit event, which arrived in 3.14 — below it such a spawn is unwitnessed, and refocus says so by withholding the "no child process witnessed" line rather than claiming it (see "What the answers claim"). Everything else works identically on 3.12 through 3.14, all three of which CI exercises. The Rust recorder is a separate build and install: Install and record, under Rust.

Related MCP server: uacos

Use

sensorium run -- pytest tests/test_fog.py     # record
sensorium runs                                # what have I recorded
sensorium info last                           # what am I looking at
sensorium tree last --depth 3                 # what actually ran
sensorium frame last --fn compute             # one activation, in full
sensorium grep last compute --kind RETURN     # every event that mentions it
sensorium exceptions last                     # what blew up, what got caught
sensorium flow last --value 1800              # where did that number come from
sensorium flow last --object build_key:record # what happened to that object
sensorium diff RUN_A RUN_B                    # where two runs part
sensorium refocus last --focus fog:compute    # re-run deeper, verified
sensorium redact --all --dry-run              # what an older store still holds
sensorium mcp --allow-run                     # serve all of the above to a model (MCP, stdio)

Per-line state is opt-in at record time, so a predicate over locals needs a run that captured them:

sensorium run --focus fog:compute -- pytest tests/test_fog.py
sensorium watch last --at fog:compute --expr 'visible > 100'

Asked against a run recorded without that --focus, watch does not report zero hits — it reports NOTHING WAS CHECKED and prints the exact re-recording command. Every example above was run as typed, against a small fog.py whose compute sums cells into a visible local and whose build_key(record) takes a dict, and a two-test tests/test_fog.py.

On an asyncio program tree groups by task, and from trace format 3 every traced code object opens a frame — function, generator, coroutine, or async generator alike — so a coroutine's callees nest under it exactly like a plain function's do, tagged [generator], [coroutine], or [async_generator] when the kind isn't function. A frame that suspended carries a derived state as its tail: ~ cancelled (CancelledError thrown in at Ln), ~ abandoned (GeneratorExit thrown in at Ln), ~ unwound by X thrown in at Ln for any other exception thrown in, or ~ suspended at Ln at end of recording for one still parked when recording stopped. A caller named but not framed is still never re-parented — it started running before recording began (<- worker (no frame: started before recording)), and a trace from before frames existed keeps arc 1's (unframed) wording exactly. --focus, watch and LINE capture all work inside async def: a focused coroutine's locals are captured at every LINE and interleaved with its ~ YIELD/~ RESUME rows in frame's timeline. --window is an ancestry flag, not a call-stack depth, so it survives a suspension: another task's calls made while the windowed frame is parked are outside the window, and the windowed frame's own calls after it resumes are still inside. A generator or coroutine resumed on a different thread from the one that started it — a sync generator first stepped where it was made and finished from a thread pool, which is what a streaming response does — keeps the frame it opened: its suspensions, its return and anything it calls stay on that frame, and every row still names the thread that produced it.

Recording captures calls, returns, raises and handled-events for code under the working directory the run started in — so sensorium run -- pytest ... traces your tests and your code, and not pytest's. --focus module:qualname adds line-level capture with local-variable deltas for the named code; --window QUALNAME limits that capture to what runs inside one function's activations. Traces land in ~/.sensorium/traces (or $SENSORIUM_DIR/traces), one SQLite file per run — the Rust and TypeScript recorders write to the same directory, so one runs lists all three.

A run reference is a full run id, a unique prefix, or last (the most recently written trace). Every query takes one — runs takes none and diff takes two. Events are addressed as eN and frames as fN, and those ids are stable, so an answer from one command is a runnable argument to the next: --fn NAME, in both grep and frame, matches a qualname exactly first, then falls back to substring over the trace's distinct qualnames — one substring hit is used as if it had been named exactly, more than one is an ambiguous call, refused with every candidate listed rather than guessed among. frame --fn NAME --nth N picks among repeated activations and says how many there are when N is out of range, and flow --object takes either e<id>:<name> — any name captured at that event — or <qualname>:<name>, which resolves to that function's first CALL and so names one of its arguments (<qualname>:return follows the same activation to what it handed back). A name that was not captured at the event a spec resolves to is refused, with the names that were captured there listed.

MCP

sensorium mcp serves the same store to a model over the Model Context Protocol — one process on stdio, no runtime dependency, every query command above except redact a tool, and record and refocus only under --allow-run. Register the project's own venv, the interpreter a recording runs under:

claude mcp add sensorium -- <venv>/bin/sensorium mcp --allow-run

The tools, the result header, the cap, the audit file and what the server refuses: docs/mcp.md. Any MCP client, not only Claude, registers the same binary on stdio — the {command, args} shape is on that page.

For agents

Install, record one run, query it, serve it over MCP: ten steps in docs/agent.md. After install, prove the path with .venv/bin/python -m pytest tests/test_agent_smoke.py -q.

Exit statuses

The exit status is the caller's next action, not a health code:

exit

meaning

examples

0

the question was answered affirmatively — the trace says yes

grep found a match; refocus MATCH; watch SATISFIED

1

the question was answered negatively — the trace says no, or none

grep matches: 0; exceptions no exceptions recorded; refocus DIVERGED

2

the call is wrong — edit the command and ask again

an ambiguous --fn; --nth out of range; a run reference that resolves to nothing; refocus refusing before any rerun was attempted

3

the trace cannot settle it — change the recording and re-record

watch NOTHING WAS CHECKED; refocus REFUSED after a rerun; exceptions' uncaught-without-RAISE arm; exceptions on a Rust trace whose recorder predates err flow (recorder sensorium-rt 0.2.0 declares it does not produce (capabilities.err_flow: false))

run exits with the target's own status — it never applies this table to itself.

Every invocation is appended to <trace root>/invocations.jsonl (default ~/.sensorium/invocations.jsonl, or under $SENSORIUM_DIR) as one JSON line with utc, argv, exit and error — the exception class name or null — and nothing else: never the environment, never the working directory. Set SENSORIUM_NO_INVOCATION_LOG to any non-empty value other than 0 to turn it off for one process — =0 leaves logging on.

What the answers claim

This is the part worth reading. Each command's output is written to be exact about its own limits, and the commands differ in how much they can establish.

diff — shape, not location

diff compares two runs' causal streams on (file, qualname, kind); a MATCH is the same shape of execution, not the same values. A function moved to another file changes file on every one of its events, so a pure move reads DIVERGED at the first moved CALL. diff --ignore-moves pairs a function that left one file with the same-named function that appeared in another — only when that pairing is unique on both sides — and prints the pairing with the verdict as moved: helper a.py -> b.py. A name present under two files on one side is left unpaired and any divergence inside it is still reported. A planted call-site swap under the same move reads DIVERGED (tests/test_diff_moves.py).

refocus — three verdicts, and a bounded licence

refocus re-runs the recorded command with deeper capture and then asks whether the rerun was the same execution — two gates, two different exit codes, because "the call is wrong" and "the recording can't settle it" are different next actions (see "Exit statuses").

Cannot refocus at all (exit 2, no rerun was attempted): original trace is INCOMPLETE; original run consumed stdin; the target no longer resolves; the original working directory is gone; the original was recorded under the per-thread fingerprint basis and ran asyncio tasks; the original trace records no command to re-run, or no working directory to re-run from; or the recorder declares capabilities.refocus: false. Every one of these is caught before the program runs again, so the reader's next move is a different command, not a different recording.

The program DID re-run, and the verdict below is about what came back:

verdict

exit

means

MATCH

0

every thread that left a fingerprint in both runs produced the identical sequence of (file, qualname, kind) for CALL/RETURN/RAISE/HANDLED outside any asyncio task, every asyncio task's own stream has a counterpart of the same name and content on the other side (a multiset — the order tasks interleaved in is never compared), and there was at least one such event to compare

DIVERGED

1

the causal streams part, and the first divergence is named with a drill-in command for each side

REFUSED

3

the rerun happened — a new trace exists and is queryable — but no verdict could be issued against it. Four post-rerun reasons: there was nothing to compare (neither side recorded a causal event); a side could not be trusted (INCOMPLETE, or writes dropped after its database sealed); the two traces define a thread stream differently (a pre-0.4.0 trace against a 0.4.0 one, whenever either ran a task); or a trace ran asyncio tasks and holds no task fingerprint rows, so what those tasks did would drop out of the comparison in silence. Treat the new trace as a separate, UNVERIFIED execution — a new recording is what would settle it, which is what 3 means

diff --task NAME diffs one task's stream by name; unnamed tasks match only unnamed tasks. Traces recorded before 0.4.0 define a thread stream to include task events (info says per-thread basis); comparing one of those with a 0.4.0 trace is REFUSED whenever either ran a task. Asyncio's own default Task-<N> names count as unnamed too — the number is creation order, not an identity, so diff --task refuses a literal Task-<N> rather than pretend it picks anything (Ruling 4). The name a task is compared under is the one it had when it first ran traced code: the recorder reads get_name() once, at the moment it mints that task's identity, so a set_name afterwards is never seen by any comparison. A thread's fingerprint row can hold zero events under this basis: a thread whose traced code all ran inside asyncio tasks still gets a row of its own, just with nothing in it outside those tasks.

A MATCH is a statement about the shape of the execution, not a statement that the two runs were the same. It licenses one conclusion: the rerun took the same path, so the deeper capture describes that same path. What no verdict compares, sensorium prints beneath every one of them: argument and return values, per-line state, timing, the order threads ran in relative to each other — and the recorder's own footprint, which is structural and unfixable. Deeper capture runs the program's __repr__ inside hooks that suppress themselves, so an instrument that perturbs the program it is watching leaves no mark on the fingerprint at all. Comparing the two runs' captured output is the only cross-check available, and a side effect that prints nothing is invisible to that too.

Beside the verdict, refocus prints a licenceverified against <run> on exactly these points, and no others, followed by the list of checks that actually ran and agreed (source files unchanged by content, environment variables compared, threads started, children witnessed). It is a bounded enumeration of what was verified, not a summary judgment. Any check that could not run withholds the licence too, with the reason itemised, because "no git repository, so I could not tell" is a fact about the check and not evidence that nothing moved. Two things it will not call a change are the tool's own: a target directory that merely moved, and the recorder's own --extern sensorium_rt=… fragment inside RUSTDOCFLAGS, whose hash moves with every driver build — the first re-rooted before the compare, the second removed from it, and both named on the line. A third, session set 1 (the handles a shell, terminal or agent session hands a process, listed by name in docs/query.md), is counted and named without withholding, while every other differing variable withholds exactly as before. The verdict and the licence are both stamped into the new trace, so info and runs keep saying so long after the output has scrolled away.

DIVERGED is not a failure of the tool. For a program whose control flow depends on state outside the process, DIVERGED is the correct answer; the new trace is still recorded and queryable, and permanently labelled.

tree — derived parentage, and what a task group claims

A parent link is the caller frame, verified by code identity, never "the last frame opened on this thread" — coroutines resumed by the event loop, generators resumed by their consumer and callbacks from C all break that assumption, and v1 made it. From trace format 3 a generator or coroutine body opens a frame like any other, so NULL now means only that the caller was never traced (the event loop, a library) or that it started running before recording began — not, as on an older trace, that it was a generator or coroutine tree could not frame. A trace recorded by a format-1 sensorium is labelled parentage: ASSUMED because its links were the guess.

Every non-function frame is marked with its kind ([generator], [coroutine], [async_generator]) and closes with the state Trace.frame_state derived from its YIELD/RESUME rows — returned and raised render exactly as a plain function's do, and the suspension states each name where the frame stopped and why: cancelled, abandoned, unwound by some other exception thrown in, or still suspended at the end of the recording. A root frame whose caller is named but not framed shows that caller's name and, from format 3, the reason is always the same one — (no frame: started before recording) — never the arc-1 (unframed) reading, which is kept byte-for-byte on older traces because it names a limitation this version no longer has.

Grouping by task is a statement about causality within a task (one task is sequential) and says nothing about order between tasks beyond wall-clock event ids; the footer says so. Task identity is a serial minted per task object, not the task's name — two tasks named alike do not merge — and is NULL for everything that did not run inside an asyncio task (code before/after the loop, and loop callbacks such as call_soon/add_done_callback).

exceptions, watch and flow — in full, one file away

The three commands whose claims need the most saying are docs/query.md, moved there 2026-09-06 so this file stays under 800 lines, wording and order unchanged — and, since 2026-09-07, that file also holds a fourth section, refocus on a Rust trace, added there rather than moved. What each of the three claims, in one paragraph:

exceptions classifies every raise as swallowed, uncaught, re-raised, propagated or ambiguous, and SWALLOWED is claimed only when the recording establishes it — anything short of that is ambiguous with the reason printed. On a Rust trace it prints one block per SHAPE rather than one per chain, and it refuses outright on a trace whose recorder declares err_flow: false. docs/query.md carries the five dispositions, the Rust disposition rules, the grouping key and what was measured about it.

watch evaluates a restricted predicate at every recorded site of the named code — a CALL's arguments, a LINE's deltas — and prints a tally that accounts for all of them: sites, evaluated, hits, not-captured, errors. Zero hits never reads as "the invariant held": a site the predicate could not be evaluated at is counted with its reason, and a run where nothing could be checked says NOTHING WAS CHECKED instead of hits: 0.

flow follows a captured value by equality (--value) or one object by address-plus-type (--object) through calls and returns. It is lineage over captured values, not static dataflow analysis, and the command says so in its own header; --object is corroborated rather than asserted, because CPython recycles addresses.

info, runs, and the state of the recording itself

  • info prints recorder, lang, and a capabilities line — the recorder's own declaration when the trace carries one, and undeclared (pre-format-4 Python recorder; read as full by every command) when it does not, because reading an absent declaration as full is what a command does to decide whether to refuse, not something the trace ever said; from trace format 4, a bookkeeping field a trace's declared capabilities say should exist but does not is printed as the recorder's own declaration of that gap — never as a printed 0, and never as the pre-format-4 "predates that bookkeeping" wording, which is kept for traces that actually predate it.

  • Truncated captures are marked where they appear — a clipped string ends ~, a sampled container ends , ... — and counted in info.

  • A run whose recording died is labelled INCOMPLETE, in info and in the runs listing both — its causal stream can stop anywhere without saying so.

  • Writes dropped under load are reported as a lower bound (>=N): writes that arrive after the count was taken cannot be counted either.

  • Subprocesses that were noticed are listed as unwitnessed, never silently ignored — and an empty list is not evidence that none ran. A child that can only be counted and not named (a multiprocessing spawn, which reaches the OS without going through subprocess) is reported as a count on Python 3.14+, where the underlying syscall raises an audit event; on 3.12 and 3.13 that event does not exist, so such a spawn is unwitnessed entirely (which is why refocus withholds the "no child witnessed" licence there). Counted too are the threads a run started and any malfunction of the hook. None of these is printed when it is zero: a printed 0 would read as proof nothing was started, which is exactly what it is not.

What a trace file holds

A trace is one SQLite file under $SENSORIUM_DIR/traces (default ~/.sensorium/traces), created 0600 in a 0700 directory from 0.15.0 on — 0644 under your umask before that. It holds:

  • the process environment at record time, variable by variable, minus what rule v1 withheld: a secret-named variable is stored as <redacted> beside an HMAC under a key that never leaves the store, so refocus can still say whether it changed; info names what it took;

  • everything the program wrote to stdout and stderr, interleaved with the events it wrote them between;

  • the command line, the working directory, the git commit, and content digests of every source file the run traced;

  • captured argument, return and local values, clipped to the caps info prints and under that same rule by NAME, with any secret-shaped SPAN inside a stored text replaced where it stands (docs/redaction.md);

  • which asyncio task each event ran in, and the tasks' names;

  • one causal fingerprint per thread (events outside any task) and per asyncio task.

The file layout is trace format 4; docs/TRACE-FORMAT.md is the contract, with conformance vectors under docs/trace-format/vectors/. A Rust trace holds the environment, command line, source digests and captured Debug values the same way; its tasks are libtest tests and spawned threads, its output is declared absent, and its spool holds the same until conversion (rust/README.md).

info refuses to print the environment and refocus the variables it compared: both carry secrets. What rule v1 does not reach is stored as it was — a secret in a variable named x or a bare key, one no content pattern knows, the command line, a token split across two write()s, anything under SENSORIUM_NO_REDACT, every mode bit on Windows, and every trace already on disk until sensorium redact runs over it. Treat a trace as you would a core dump or a .env: sharing one shares all of the above, and SENSORIUM_DIR is the only control over where it lands.

What sensorium sees at all

Code that this run traced — for the Python recorder, Python code in files under the run's own root; for the Rust recorder, the workspace's own crates; for the TypeScript recorder, the eligible ES modules under the invocation's root. Nothing else. On a Python trace, no command here says anything about:

  • any child process, by any mechanism;

  • any thread not started through Python's own threading / _thread;

  • any file the program read or wrote — only source files are hashed, so config, fixtures, databases and inputs move unseen;

  • any code outside the run's root: the stdlib, site-packages, installed dependencies, PYTHONPATH modules, and whatever --include / --exclude filtered out;

  • the environment beyond the variables a command names as compared;

  • the clock, the network, and everything else the machine did.

This is stated as a category rather than as a list of mechanisms on purpose. Five review rounds of refocus each found a mechanism the tool could not see; an enumeration that looks complete is more dangerous than no enumeration, because a reader who checks the list concludes their case was covered. The Rust and TypeScript recorders draw their boundaries the same way, in rust/HONESTY.md and typescript/HONESTY.md with their blind-spot files beside them.

Overhead

Recording overhead and read-back cost, measured on this machine with python corpus/run_corpus.py --bench and reported table by table, are docs/overhead.md, moved there 2026-09-16 so this file stays under 800 lines, wording unchanged. The headline: 2.9× on the corpus's typical workloads, up to 194× on the call-dense one under --focus; record failing tests, never benchmarks.

Corpus

python corpus/run_corpus.py                          # verify against seeded bugs
python corpus/run_corpus.py --show                   # print the questions and commands
python corpus/run_corpus.py --bench                  # report recording overhead
python corpus/run_corpus.py --require-driver         # a skipped Rust or TypeScript case is exit 1
python corpus/run_corpus.py --via mcp --compare-cli  # every tool question through the MCP server, diffed against the CLI

Small programs with deliberately planted bugs, and questions registered before any output was looked at: the question in plain language, the known ground truth, the exact invocation expected to yield it, and why a print() cannot answer it. Ground truth is known because the bugs were planted. This is the regression suite, and it includes the honesty cases — the ones whose pinned answer is a REFUSAL. 115 cases and 253 questions: twenty-two Python programs with forty-eight questions, forty-five Rust cases and forty-eight TypeScript cases, thirteen of the last recorded under a --focus. All of them case by case in docs/corpus.md, moved there 2026-09-09 so this file stays under 800 lines, wording unchanged. A case whose recorder is not built is reported skipped BY NAME and counted apart from the passes, never as them; --require-driver turns such a skip into exit 1, which is what CI passes.

--bench reports; it never gates. Overhead is a tracked fact about a machine and a workload, not a pass/fail property of the tool.

Rust

cargo sensorium test/cargo sensorium run record a Rust workspace's own crates the same way sensorium run records a Python program: one sensorium trace per process, trace format 4, read by the same sensorium command line. rust/ ships sensorium-rt 0.7.0 (zero dependencies, the runtime linked into every instrumented unit, and the owner of the one sha256 the other two hash with), sensorium-transform 0.5.0 (the syn rewriter), and cargo-sensorium 0.8.0 (driver, workspace wrapper, target runner, converter — one binary, four roles). What it does and does not see is rust/HONESTY.md with rust/HONESTY-BLIND-SPOTS.md; rust/README.md is the full build/install/record reference.

What the licence is worth on a real workspace, measured. refocus over 61 #[test] pairs of a workspace nobody wrote this recorder for was re-measured 2026-09-08 as E4″ (docs/superpowers/acceptance/2026-09-08-sensorium-rung4-e4pp.md, pre-registered and byte-locked before the instrument existed): H1–H7 PASS, H8 a STOP on a cell of that record's own reader. The licence is granted on 57 of the 61 and withheld on the four tests that really do start threads, and the recorder's own RUSTDOCFLAGS fragment is out of the compare on 61 of 61 — under a driver build different from the originals', which is the only condition that tests the second claim at all. The eighth row missed because the reader compared a bare case name against a listing that spells Rust cases rust/<name>; its three commands came back green, the STOP stands as measured, and the one-line fix is ruled for the next slice.

Which versions, and when. E4″ ran on 2026-09-08 under cargo-sensorium 0.5.2, sensorium-transform 0.4.3 and sensorium-rt 0.4.0main as it stood that day — and its answers were read by Python 0.8.6. The 61 originals were recorded earlier, by cargo-sensorium 0.5.0: that difference between the recording driver and the re-running one is the CONDITION the second claim needs, not an accident of bookkeeping. The crate numbers at the top of this section are today's (0.5.0 / 0.5.0 / 0.6.0) and Python 0.13.0 reads these traces now. All four moved after the measurement — for the sha256 consolidation, then for the LINE row's unbound — and none is a version that produced a number above.

Install and record

cd rust && cargo build --release
cargo install --path rust/cargo-sensorium      # from the repository root
cargo sensorium test [--tier off|call] <cargo test args…>
cargo sensorium run  [--tier off|call] <cargo run args…>

Cargo stays the runner and the builder; sensorium only changes what gets compiled and watches what comes out. --tier off compiles the same artifacts and gates emission at runtime, so switching tiers rebuilds nothing. Traces land beside the Python ones, in $SENSORIUM_DIR/traces/.

What it answers

Tier call — the only tier this version ships, and the default — records CALL and RETURN with an outcome (ok/err/panic/none) and a captured Debug return value, panics, per-thread MAP_SHARED spools, and libtest tests plus spawn_child-named worker threads as tasks. From 0.3.0 it also records err flow: a RAISE at every ? on a Result, a HANDLED at each of the four written sinks (.ok(), .unwrap_or(..), .unwrap_or_else(..), .unwrap_or_default()), at let _ = <value>, and at every Err(..) => arm or if let Err(..) body, classified by what its body does. runs, info, tree, frame, grep, diff — including diff --ignore-moves across a refactor — and now exceptions all answer on a Rust trace; info adds the toolchain, per-unit instrumentation counts, child runs, live threads at exit, and the ? sites the transformer could not reach (partial fns: N), which it declares rather than losing.

What exceptions was measured to be worth. On the bloomery clone's --lib suite it printed 15 SWALLOWED lines and one was a false accusation — an Err(e) => arm whose format! product is the value the function returns — so the rung's pre-registered endpoint read STOP, the rule was amended, and the repair was re-measured rather than declared: 0 false accusations of 14, on two selectors and under both readings of the endpoint. Both records stand and both are worth reading before quoting any of it: docs/superpowers/acceptance/2026-09-04-sensorium-rung3-acceptance.md (§4, §5.1) and docs/superpowers/acceptance/2026-09-05-sensorium-rung3-e6ppp.md (§4, §5).

One gap, measured, then fixed. diff --ignore-moves pairs code objects correctly across a file split (28/28 paired, 0 added, 0 removed, in the acceptance run's own split of a real file), but a spawned worker thread's task NAME embedded its spawn site (<parent task> :: spawn@<file>:<line>), so moving that call site during the same split renamed the task and the comparison read DIVERGED even though nothing about the program's behaviour changed. Measured on bloomery's own registry.rs split — four spawned-task names moved, their stream hashes identical pairwise on both sides: docs/superpowers/acceptance/2026-09-02-sensorium-rung2-acceptance.md §3–§4, endpoint E5. Fixed 2026-09-03 (rung-3 entry decision, Brice's ruling (b)): a spawned task is now named <parent task> :: spawn@<qualname>#<k> — the enclosing named item's file-local qualname plus a source-order ordinal among its wrapped spawn sites, neither of which a file move changes (docs/superpowers/plans/2026-09-03-sensorium-rung3-entry-spawn-names.md, decisions N1–N6; rust/HONESTY.md §3). Verified by E5′ on the endpoint the fix exists for, which reads PASS (§4): across that same split, 28 code objects pair and all ten task streams pair, where rung 2 read DIVERGED; the four spawned children carry byte-identical names on both sides (E5′-names' first conjunct, 8 of 8 exactly the predicted string). E5′-coverage reads PASS (0 units fell back). The record's overall line is STOP, on a third endpoint, E5′-names, whose second conjunct asked that the multiset of (name, hash) pairs be equal across the split while naming the trace's STORED hash as the source — and that hash is defined over file, so a file move changes it by construction. That is a defect in the pre-registration rather than in the naming rule; it was read once, no repair was applied after the number, and it was ruled 2026-09-04: (b) withdrawn; see the record §5.1. Read §4 and §5.1 before citing any of this: docs/superpowers/acceptance/2026-09-03-sensorium-rung3-entry-e5prime.md.

Per-line answers, under --focus

cargo sensorium --focus <qualname> … (repeatable, and a container value selects its children on the :: boundary) splices a probe after every statement of each function it names, so the trace carries one LINE event per completed statement with the bindings that statement wrote. That is what makes watch and flow answer on a Rust trace: watch last --at fill --expr 'b == 2' settles the predicate at the statement that wrote b rather than at the function, and flow last --value 3 follows the value through LINE deltas as well as returns. --at Counter selects Counter::new on the same boundary rule Pot uses for Pot.add, so it never quietly answers about Counters::new. What the tier does not reach — closure and async bodies, place writes, macro bodies, and every function no focus named — is rust/HONESTY-BLIND-SPOTS.md item 3, narrowed to exactly that list.

From sensorium-rt 0.5.0, a block-like statement's LINE row also lists the names it unbound, so watch's fold does not read one as still in scope once its block has closed.

A Rust capture is Debug text, not a typed value, so the reading rule is written down: a literal is compared against its Debug rendering (5, 2.5, true, None, and a string WITH its quotes), a truncated capture never matches, and len() over one is NOTHING WAS CHECKED rather than a comparison to nothing. watch --expr and flow --value are inverses on that domain — what one calls a sighting the other cannot deny at the same site. x == 5 compared text, and docs/TRACE-FORMAT.md's LINE row says so where a reader meets it.

What refuses

watch and flow refuse on an UNFOCUSED trace — never answering from a capability the recorder declares it does not have. They print why and exit 3 — the recording, not the call, is what would have to change — because both need capabilities.line, which a build with no --focus declares false.

refocus answers from cargo-sensorium 0.5.0: capabilities.refocus is true for every trace that driver converts, and whether one PARTICULAR recording can be re-run is a refusal about that run, not about the recorder. Five such refusals exit 2 with nothing was re-run in them — --window, which the Rust runtime has no per-activation check for; a run that is one of n processes of its invocation, so no single trace is the answer; a trace recording no workspace_root; a workspace that has since moved; and no cargo-sensorium to re-run with. What the verdict then claims, and the two licence checks it cannot run at all, is docs/query.md.

exceptions answers from 0.3.0, and refuses on exactly one thing: a trace an older runtime wrote. The gate is capabilities.err_flow, so such a trace exits 3 naming the recorder and the capability, and no rule ever sees its records — what it lacks is a record, not a rule. Program output under libtest and per-line state in an unfocused build are the remaining "not yet"s: declared absent in the trace, never silently missing.

Cost, beside Python's

Measured on this same box (docs/superpowers/acceptance/2026-09-02-sensorium-rung2-acceptance.md §3.1): cargo test -p bloomery-daemon --lib plain against cargo sensorium test -p bloomery-daemon --lib at tier call, n=5 each, binaries pre-built — 0.058 s plain, 0.125 s call, ×2.1552, conversion INSIDE the timed command. That is not comparable to rung 1's ×1.0103, which timed the whole 8.25 s suite with conversion outside it (§5.3), and it is not comparable to Python's 4–9 µs/event above either: a whole-suite wall ratio and a per-event figure are not the same unit, and no section here states one as a translation of the other. The driver's own fixed cost is 0.073 s (n=5, no-op --tier off --no-run against straight cargo).

TypeScript

sensorium ts run -- vitest run records a TypeScript or JavaScript test suite the way cargo sensorium test records a Rust workspace: one trace per test-file process, trace format 4, read by the same sensorium command line. typescript/ ships sensorium-ts 0.6.0 — a transform whose edits never contain a newline, a runtime on AsyncLocalStorage, a vitest plugin, a node --test hook — with driver and converter in Python, so reading a trace needs no Node. What it sees and does not is typescript/HONESTY.md with its blind-spot file; typescript/README.md is the full reference.

npm ci --prefix typescript
npm --prefix typescript run check                        # type-check
sensorium ts run [--tier off|call] -- vitest run         # or: -- node --test src/
sensorium ts run --focus diceQueue.ts:parseDiceGroups -- npx vitest run src/lib

Everything after -- is yours, spawned as typed, and the driver exits with the harness's status. Tier call records calls and returns with a captured value, YIELD/RESUME at every await/yield, RAISE at every throw, HANDLED at every catch, and tests as tasks named as vitest names them, so tree groups by test and diff compares a test against itself. exceptions answers — the same five dispositions, from a how word the transform decides from each handler's syntax, merged across a whole invocation — while a trace an 0.1.x runtime wrote refuses at exit 3 on err_flow: false. flow --object answers on any recording from 0.3.0 on, focused or not, and answers exactly: identity is a per-object serial minted once and never reused, so the footer reads continuity: exact (serial identity). refocus re-runs the whole recorded invocation and pairs by test file (docs/query.md); package scripts, jest, a project with no typescript of its own and a vitest projects/workspace config are refused by name.

From sensorium-ts 0.4.0, a RETURN row follows the rows of any finally the return passed through.

Per-statement answers, under --focus

--focus <spec> (repeatable, <qualname> or <file>:<qualname>, and a container value selects its members on the . boundary) is resolved against your own sources with your own TypeScript before anything is spawned: a spec that names nothing is refused at exit 2 with the closest eligible qualnames, and one that names only functions this recorder does not instrument is refused with the reason and its count. What it selects, the transform splices a probe into — one LINE event per completed statement, its deltas the bindings that statement wrote, a guarded body's head names as a synthetic first row at each entry, and the block-scoped names a block-like statement declared listed unbound on its own row, which is the key watch's fold pops. The focused function's CALL carries its arguments; an unfocused one in the same recording still reads helper() <unread: locals>. So watch and flow --value answer on a focused run and refuse at exit 3 on an unfocused one, naming the recorder the trace itself carries; frame answers either way, saying timeline: not captured (record again with …) at exit 0.

A TypeScript capture is node's util.inspect text, so the reading rule is written down, and it is Rust's opposite in the place a reader meets first: flow --value 5.0 sights a JavaScript 5, because JavaScript has one number type. null, undefined, true and false are predicate constants in every language; a string is spelled with its quotes, and one past 100 characters was cut by the formatter, so it matches nothing rather than matching a prefix. Every spelling is generated rather than argued — 41 measured rows in typescript/test/fixtures/inspect-table.json — and docs/trace-format/TYPESCRIPT-KEYS.md § Under a focus is the reading. What the tier does not reach — place writes, this, a conditional assignment's write-or-not, a switch discriminant, a nested function no spec's prefix reached — is typescript/HONESTY-BLIND-SPOTS.md items 28–35 and 39, each a present row with a stated hole.

What four rungs measured

One lens under nearly every number, and it is somebody else's code: a tabletop VTT frontend at 0091e97372 test files, 4,278 tests — under vitest 4.1.9, node v24.16.0, 16 cores. Each rung was pre-registered and byte-locked before its own code existed. Two things none of it licenses: no endpoint says a TypeScript trace answered a debugging question nobody planted, and none was measured on a second consumer. Every figure, and what each rung left open, is in docs/superpowers/acceptance/ and in typescript/README.md.

  • Rungs 1–3. DONE-WITH-STOP, then DONE, then DONE: 372 containers of one test file each and 5,378 of 5,378 eligible sites instrumented, call/plain 1.1324 with E6′ STOPping on one timing clause of four; exceptions at 0 false SWALLOWED of 30 hand-adjudicated shapes; the catch-all blocks naming a reason at 0 false names of 20.

  • Rung 4 — DONE-WITH-STOP, the focus tier, on 830 files of that same frontend. H3 is the endpoint the rung exists for and it PASSed on the first reading: nine LINE rows for one parseDiceGroups('1d20') activation, their lines, every delta name and the one unbound list row for row against a hand count sha256-locked before the code existed, empty diff. H1 4/4, H6 4/4, H8 6/6, H7 reported at ×2.3816 plus 1.132 s of resolution. H2, H4 and H5 STOP — H2 because three typed specs select six sites (a container's spec reaches what is nested in it, which the pre-registration counted by names typed), H4 and H5 because the reading INSTRUMENT could not tell a loop's head row from its completion row nor see a CALL sighting its own transcript prints. All three were found after their numbers, so all three stand as findings, and the next slice re-registers them.

Not yet

What each slice deferred, and the ruling each deferral is waiting on, is docs/CARRIED-DEBT.md — appended at every merge, resolved items struck through rather than deleted.

Subprocess following, attach-to-live-server flight recording, native (rr) substrates. See docs/superpowers/specs/2026-08-21-sensorium-arc2-inspectable-coroutines-design.md (extends 2026-08-21-sensorium-async-design.md, arc 1's spec).

License

MIT — see LICENSE. Copyright © 2026 Brice Lancaster.

Versions after the Proprietary interlude are again under MIT (matching releases through 0.14.0).

Available Tools

9 tools
diffdiffB
Read-onlyIdempotent

first causal divergence between two runs. Aligns the two causal streams and names the first event where they part, with drill-in commands; MATCH or DIVERGED, or REFUSED when no verdict exists.

ParametersJSON Schema
NameRequiredDescriptionDefault
taskNocompare one recorded task's stream by name instead of the thread streams; what a task IS is the recorder's own, and `info` names the ones a run has
run_aYesthe first run: run id, a unique prefix of one, or `last` (the newest trace)
run_bYesthe second run: run id, a unique prefix of one, or `last` (the newest trace)
contextNocommon causal steps to show before a divergence
ignore_movesNopair a function that left one file with the same-named function that appeared in another, then compare; the pairing is printed with the verdict

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, covering the safety profile. The description adds the REFUSED outcome and mentions drill-in commands, but doesn't disclose more about side effects or error behavior. Given the annotations, this is adequate but not rich.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, with two sentences that front-load the purpose and then add details on output and behavior. It avoids fluff, though the first sentence is a fragment and the phrasing is somewhat jargon-heavy, preventing a perfect score.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With five parameters, two required, and no output schema, the description should clarify the return format more thoroughly. It gives verdicts (MATCH, DIVERGED, REFUSED) and says it 'names the first event', but doesn't describe the event structure or the drill-in commands. The schema covers parameters, so this gap is in the output semantics, making it incomplete but not severely lacking.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema provides 100% description coverage for all five parameters, so the schema already documents them. The description doesn't add extra meaning to parameters beyond referencing 'two runs' (run_a, run_b), so it remains at the baseline of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states the tool finds the 'first causal divergence between two runs' and explains it aligns streams and names the first event. This is a specific function distinct from siblings like 'runs' or 'tree' which are not comparative. However, it doesn't explicitly contrast with alternatives, so it stops short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the tool is for comparing two runs, but it doesn't explicitly state when to use it versus other tools or list exclusions. It mentions 'REFUSED when no verdict exists', hinting at a condition, but that's not enough to guide tool selection clearly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

exceptionsexceptionsA
Read-onlyIdempotent

raises, handles, swallows. Every raise classified swallowed, uncaught, re-raised, propagated or ambiguous, with the reason; SWALLOWED is claimed only when the recording proves it.

ParametersJSON Schema
NameRequiredDescriptionDefault
runNoa run id (or a unique prefix), 'last', or an invocation id as `runs` prints it; --after is refused for an invocationlast
afterNoevent ref to resume from
limitNomost rows to print

TDQS

A3.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already convey read-only, idempotent, and non-destructive behavior, so the description adds meaningful context by stating that SWALLOWED is claimed only when the recording proves it. This reveals an evidence-based classification policy and a conservative bias in the tool's output.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two short sentences with no filler. The core actions are front-loaded, and the evidence rule about SWALLOWED earns its place by conditioning how agents should interpret that category.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only inspection tool with fully documented parameters and safety annotations, the description adequately conveys what the tool returns: classified raises with reasons. It does not detail output formatting or ordering, and it lacks sibling routing, so it is not fully complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the parameters are already documented in the schema. The description adds no additional parameter-level meaning, such as format or interaction between run, after, and limit, so the baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description specifies that the tool classifies exception raises into categories (swallowed, uncaught, re-raised, propagated, ambiguous) with reasons, which is a specific verb+resource pairing. It does not explicitly differentiate itself from sibling tools like flow or info, though the exception-classification focus is fairly distinctive.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this tool versus alternatives. It does not mention sibling tools, preconditions such as an active run, or scenarios where another tool would be more appropriate; the intended use is only weakly implied by the name and description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

flowflowA
Read-onlyIdempotent

provenance of a value or an object. --value follows a literal by equality through captured arguments, locals and returns; --object follows one identity from the event that captured it; one of the two is required, and each refuses by name what the recording did not capture.

ParametersJSON Schema
NameRequiredDescriptionDefault
runNorun id, a unique prefix of one, or `last` (the newest trace)last
afterNoevent ref to resume from
limitNomost rows to print
valueNoliteral matched by equality; quote to force a string, as in "'1800'"
objectNoe<id>:<name>, where <name> was captured at that event (a CALL's argument, a LINE's local); or <qualname>:<name>, which resolves to that function's first CALL and so names one of its ARGUMENTS -- plus <qualname>:return for what that activation returned

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnlyHint=true, idempotentHint=true), the description adds genuine behavioral context: it follows through captured arguments/locals/returns, resolves object names against the event that captured them, and 'refuses by name what the recording did not capture,' which tells an agent how the tool fails on uncaptured identifiers. This is informative failure-mode disclosure that the annotations do not provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two dense sentences with the core purpose front-loaded and every clause earning its place. The telegraphic style ('provenance of a value') is efficient, though slightly cryptic for a first-time caller, which keeps it from a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 5-parameter tool with two exclusive modes and no output schema, the description covers the critical disambiguation between modes and the mandatory-selection rule. The main gap is that it never hints at what the provenance output looks like, which matters more given there is no output schema to fill that void.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaning the schema lacks: the semantic difference between equality-tracking (`--value`) and identity-tracking (`--object`), the mutual exclusivity requirement, and the refusal behavior for uncaptured names. It connects the parameters to tool behavior rather than merely restating formats.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a clear purpose — tracing the provenance of a value or object — and elaborates two distinct modes (`--value` by equality, `--object` by identity), which is specific enough to separate it from the sibling set. It stops short of 5 because it never explicitly names or contrasts any sibling tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage context is implied through the behavior described: equality-following vs identity-following, plus the explicit constraint that one of `--value`/`--object` is required. However, there is no statement of when to prefer `flow` over siblings like `grep`, `watch`, or `tree`, and no exclusions or alternative routing.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

frameframeA
Read-onlyIdempotent

one activation in full. Arguments, return or raise, and the per-line locals timeline when the run was recorded with --focus; frame ids fN come from exceptions, tree and grep.

ParametersJSON Schema
NameRequiredDescriptionDefault
fnNoqualname of the function: exact match first, else substring
nthNowhich activation (1-based)
runNorun id, a unique prefix of one, or `last` (the newest trace)last
frameNoframe ref (f12)

TDQS

A3.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint and idempotentHint, so the description correctly does not restate those. It adds useful behavioral context beyond annotations: the per-line locals timeline is only available when the run was recorded with `--focus`, and the tool reports either a return value or a raise.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is short and front-loaded with the core purpose ('one activation in full'), and the rest of the sentence packs in the key behavioral condition without filler. It is slightly telegraphic but every phrase earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description does explain what the tool returns: arguments, return/raise, and the conditional locals timeline. It also tells the agent where frame ids originate. It could be more explicit about the two addressing modes (`fn`/`nth` vs `frame`), but overall it is adequate for selecting and invoking the tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the core parameter meanings are already in the schema. The description adds a small amount of context by clarifying that frame ids come from sibling tools, but it does not explain the relationship between `fn`/`nth` and `frame`. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies the resource ('one activation in full') and the key output contents: arguments, return/raise, and per-line locals timeline. It lacks an explicit verb like 'show' or 'return', so it is not quite a 5, but it is clear enough to distinguish the tool's purpose from siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies when to use the tool by stating that frame ids fN come from `exceptions`, `tree`, and `grep`, which tells an agent where to obtain a valid frame reference. It does not explicitly state when to prefer this tool over alternatives or when not to use it, so guidance remains implicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

grepgrepA
Read-onlyIdempotent

search events by name or value. Every CALL, RETURN, RAISE, HANDLED or LINE event whose name or rendered value contains the pattern; --after resumes from an event id a previous answer showed.

ParametersJSON Schema
NameRequiredDescriptionDefault
fnNoqualname filter: exact match first, else substring
runNorun id, a unique prefix of one, or `last` (the newest trace)last
kindNokeep events of this kind only
afterNoevent ref to resume from
limitNomost rows to print
patternYessubstring matched against event names and rendered values

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already provide readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the tool is non-mutating and safe. The description adds context about the matching behavior (substring matching on names and rendered values) and the resume capability, which is not in the annotations. It does not contradict annotations, and the behavioral coverage is strong.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, focused sentence that states the primary function, the matching criteria, and the important resume capability. It is front-loaded with the action and purpose, with no wasted words. It is slightly dense but not overlong, earning a 4 rather than a 5 due to the use of a semicolon that could be simplified.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is a read-only search with a rich schema that covers all parameters. The description covers the matching semantics and the resume feature, which is the key non-obvious behavior. Given the annotations and schema, an agent has everything needed to call it correctly. No output schema is needed as the description implies event rows are returned. It is complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all parameters are described in the schema (e.g., 'pattern', 'fn', 'run', 'kind', 'after', 'limit'). The description adds value by clarifying that 'pattern' is a substring and lists the event kinds, but it duplicates schema info. It does not add new meaning beyond the schema, so a baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states a specific verb ('search') and the resource ('events'), and specifies exactly which events match ('CALL, RETURN, RAISE, HANDLED or LINE event whose name or rendered value contains the pattern'). It distinguishes itself from siblings like 'tree' or 'flow' by focusing on pattern-based search across event types, making the tool's purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly mentions the `--after` option and its use case ('resumes from an event id a previous answer showed'), indicating when to use it for continuing a previous exploration. It also implies this is the primary search tool for events, distinct from siblings like 'runs' (listing runs) or 'info' (details). This is clear enough for an agent to know when to select it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

infoinfoA
Read-onlyIdempotent

summarize one trace. Recorder, language, the capabilities the trace declares, what was recorded and what was not, focus, the redaction stamp; read it before any other question on a run.

ParametersJSON Schema
NameRequiredDescriptionDefault
runNorun id, a unique prefix of one, or `last` (the newest trace)last

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, covering the safety profile. The description adds value by specifying what the summary includes (recorder, language, capabilities, etc.), which is not in annotations. It does not disclose any surprising side effects, but none are expected given the annotations. It could mention that it reads the most recent trace by default, but that is covered in the schema default.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence that front-loads the core purpose ('summarize one trace') and then lists the key components concisely. There is no fluff or redundancy. It is efficient and well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description's list of what the summary includes (recorder, language, capabilities, etc.) effectively informs the agent of the return content. The tool is simple with one optional parameter, and the description covers its purpose and usage. It doesn't explain the 'redaction stamp' term, but that is domain-specific and not necessary for invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema describes the single parameter 'run' fully (run id, unique prefix, or 'last'). The description does not add any additional semantics about this parameter. Since schema coverage is 100%, the baseline of 3 is appropriate; the description does not need to compensate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'summarize one trace' and lists the specific content included (recorder, language, capabilities, etc.). It is not a tautology and provides a distinct purpose. It doesn't explicitly differentiate from sibling tools like 'runs' or 'tree', but the 'summarize' verb and the list of fields make its role clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes a usage directive: 'read it before any other question on a run.' This tells the agent when to use this tool first, implying it's a starting point. It does not explicitly mention alternatives or when not to use it, but the 'before any other question' guidance provides clear context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

runsrunsA
Read-onlyIdempotent

list recorded traces. One line per trace, oldest first: run id, exit, event count, the command; a refocus rerun names its original and its verdict.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is clear. The description adds useful behavioral context: output is one line per trace, oldest first, and includes specific fields (run id, exit, event count, command). It also explains the 'refocus rerun' naming behavior, which is beyond what annotations provide. However, it doesn't mention pagination, limits, or what happens with no traces.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, information-dense sentence. It front-loads the core action ('list recorded traces') and then packs the output format and a special case (refocus rerun) into a compact structure. Every word earns its place; no filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter, read-only listing tool with strong annotations, the description is nearly complete. It tells the agent what the output looks like, the ordering, and the fields. The only minor gap is that it doesn't describe edge cases like empty results or whether the list is truncated, but given the tool's simplicity and annotation coverage, this is acceptable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and schema description coverage is 100% (the schema is empty). With no parameters to document, the description doesn't need to explain parameter semantics. The baseline for 0 params is 4, and the description doesn't introduce any confusion about parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists recorded traces, with a specific verb ('list') and resource ('recorded traces'). It distinguishes itself from siblings like info, tree, frame, and flow by focusing on the run-level trace listing. However, it doesn't explicitly name a sibling alternative, so it loses a point for not differentiating from potential similar tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage: when you want to see recorded traces at a glance. It provides output format details (one line per trace, oldest first, fields) which helps an agent know what to expect. But it doesn't explicitly state when to use this vs alternatives like flow or info, nor does it mention any exclusions or prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

treetreeA
Read-onlyIdempotent

call-tree slice. Parentage derived from the event stream; --around centres the slice on an event id, --root on a frame id; nested dict contents are elided, frame prints them.

ParametersJSON Schema
NameRequiredDescriptionDefault
runNorun id, a unique prefix of one, or `last` (the newest trace)last
rootNoframe ref (f12)
depthNolevels of the call tree to print below the root
limitNomost rows to print
aroundNoevent ref (e40)

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare the tool read-only, idempotent, and non-destructive. The description adds useful behavioral context beyond annotations: parentage is derived from the event stream, nested dict contents are elided by default, and `frame` is the alternative for printing them. No behavior contradicts the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is one dense, purposeful sentence with no filler. It front-loads the core purpose, then conveys the two slicing modes and the key behavioral distinction from `frame` in a compact way.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has no output schema, the description covers the essential behavioral details: slice centering, depth default behavior implied by schema, and elision of nested dict contents. It could be slightly more complete by explicitly mentioning defaults like `run=last`, but those are fully documented in the schema, so nothing critical is missing for invoking the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents every parameter. The description adds meaning on top by clarifying the role of `around` versus `root` ('centres the slice' on an event id versus a frame id), which is not fully captured by the schema's terse 'event ref (e40)' and 'frame ref (f12)'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies the resource and scope clearly with 'call-tree slice' and explains the two slicing modes (`--around` on event id, `--root` on frame id). It stops short of a 5 because it lacks an explicit verb like 'prints' or 'shows', though the intended action is unambiguous and it distinguishes itself from the sibling `frame`.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives concrete context for when to use the tool's two main modes: `--around` for event-centered slices and `--root` for frame-centered slices. It also implies when the sibling `frame` is preferable ('nested dict contents are elided, `frame` prints them'), though it does not explicitly enumerate exclusions versus other siblings like `grep` or `flow`.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

watchwatchA
Read-onlyIdempotent

predicate over captured state. A gdb-style watchpoint evaluated after the fact at every recorded site of one function; needs a --focus recording, and NOTHING WAS CHECKED is a refusal, never a pass.

ParametersJSON Schema
NameRequiredDescriptionDefault
atYesmodule:qualname or qualname
runNorun id, a unique prefix of one, or `last` (the newest trace)last
exprYesnames, literals, one comparison, and/or/not, arithmetic, len(name)
afterNoevent ref to resume from
limitNomost rows to print
missesNohow many near-misses to show when nothing hit

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already establish read-only, idempotent, and non-destructive behavior. The description adds non-obvious behavioral context: evaluation happens after the fact, covers every recorded site of one function, requires a focus recording, and treats "nothing checked" as a refusal rather than a pass. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loaded with the core concept. Each clause contributes: predicate semantics, evaluation timing and scope, the focus prerequisite, and result interpretation. The phrasing is dense and domain-specific but contains no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With all 6 parameters fully documented in the schema and annotations covering safety, the description covers the main unstated constraints: focus requirement, per-function scope, and the nothing-checked failure mode. An example or explicit output shape would improve completeness, but the core contract is sufficiently clear.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaning beyond the schema: "predicate" and "gdb-style watchpoint" clarify the role of expr, while "at every recorded site of one function" and "needs a `--focus` recording" clarify at/run semantics. The refusal rule also gives practical meaning to near-miss and no-hit behavior.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific resource and operation: evaluates a gdb-style watchpoint predicate over captured state at every recorded site of one function. It distinguishes the tool from a generic search or dump, though it does not explicitly compare against siblings like grep or exceptions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives a clear prerequisite ("needs a `--focus` recording") and an interpretation rule ("NOTHING WAS CHECKED is a refusal, never a pass"). However, it never explains when to prefer this tool over alternatives such as grep, tree, flow, or exceptions, so usage selection is left mostly to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 9 tool updatesv0.18.1
    • First observeddiff
    • First observedexceptions
    • First observedflow
    • First observedframe
    • First observedgrep
    • First observedinfo
    • First observedruns
    • First observedtree
    • First observedwatch

TDQS

A4/5.0

Scored across 9 tools

Disambiguation5/5

Each of the nine tools owns a distinct slice of the trace-analysis workflow: run listing, run summary, call-tree navigation, frame detail, event search, exception classification, value/object provenance, predicate evaluation, and run comparison. Even close neighbors like tree/frame and grep/exceptions are cleanly separated by granularity and purpose. An agent should rarely misselect.

Naming Consistency4/5

All tool names are concise lowercase single-word commands, so the naming style is predictable and visually consistent. The only minor deviation is a mix of nouns/verb forms like info, tree, frame vs. grep, watch, diff, but there is no mixed casing or chaotic variation. This is a small style inconsistency rather than a real usability problem.

Tool Count5/5

Nine tools is an ideal scope for a specialized trace-analysis server: broad enough to cover the main investigative workflows, yet small enough that every command has a clear role. It is comfortably within the well-scoped range and does not feel over- or under-built.

Completeness5/5

The tool surface covers the full analytical lifecycle: discovering runs, reading summaries, exploring call trees and frames, searching events, classifying exceptions, tracing value/object provenance, evaluating predicates, and diffing two runs. The tools explicitly reference each other's outputs such as frame ids and event ids, enabling iterative investigation without dead ends. A raw event timeline is absent, but grep, tree, and exceptions cover that need effectively.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Records runtime events from any app via zero-code instrumentation and exposes them to LLMs through MCP for autonomous debugging.
    8 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Local-first code intelligence and safety layer for AI coding agents. MCP server exposes dependency graph, impact analysis, and AST-compressed repo context, backed by typed local memory, patch-scope safety gates, and git-independent transaction rollback.
    1
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Local MCP server that lets your AI coding agent query its own cross-tool project history - file/command freshness, past test failures, cost & token spend, cache status, and session handoff - over stdio, 100% local, no telemetry.
    46
    -
  • A
    license
    A
    quality
    A
    maintenance
    Records your terminal sessions per command (PTY + OSC 133) into local SQLite, so AI agents can search, retrieve, and diff what commands actually printed. Secret redaction is applied by default to everything served over MCP.
    4
    6
    MIT