Imported from Butanium/tinkerscope (
plugin/skills/cli/SKILL.md). Install upstream withnpx skills add Butanium/tinkerscope --skill cli. Copyright stays with the author.
tinkerscope
A local web playground that auto-discovers Tinker training runs under a
directory tree and lets you chat with / sample from their checkpoints, plus a
tinkpg CLI that drives the SAME view the human has open in their browser. You
trigger a chat from your shell; their screen fills in live, and the completion
also streams to YOUR stdout — so you read the samples directly while showing
them the human.
Start (or find) the server
tinkpg state # a server already running for this cwd? (auto-discovery probe)
tinkerscope <dirs> --port N # scan <dirs> for runs (checkpoints.jsonl + config.json); prints the URL
- Run the server in the background; it stays up. Default port = first free from
8765. Relaunching on the SAME scan roots points you at the existing instance
instead of starting a twin (so pass a different
--portfor a new root set). - Give the human the printed URL (e.g. http://127.0.0.1:8809).
tinkpgauto-targets the running instance whose scan root contains your cwd. Outside any scan root: setTINKERSCOPE_BASE_URLor pass--base-url.
Share packs (reproduce a setup) — these are tinkerscope subcommands, NOT tinkpg.
tinkerscope --pack <file|url> seeds this folder from a portable YAML bundle (public
checkpoints + default params + workspaces) then serves — hand a collaborator a
reproducible setup with no local run dirs. tinkerscope pack export <out.yaml> --dir <scan-root> [--workspace NAME] [--models-from panels|workspaces|all|runs] [--exclude-model SUBSTR] [--no-defaults]
authors one from the live state. Two gotchas: --dir must be the scan root the running
instance was launched with (find it: ps aux | grep '[t]inkerscope', or the state dir
won't match and export reads nothing); and --workspace NAME exports ONE saved workspace
(omit → all). Packs carry only models + params + workspaces — never highlights/pins
(--no-defaults drops the params too). Export keeps each node's raw_meta (the Raw view)
and strips token_logprobs unless you pass --logprobs — which you should pair with a
.gz output path, since the uncompressed form of a real workspace is 107 MB and GitHub
hard-blocks files over 100 MB (gzipped: 30 MB). Both --pack and ?w= un-gzip
transparently, sniffing the magic bytes rather than the extension.
Merge-safe (won't clobber params without --force).
Iterating on a pack (re-export → re-consume the SAME folder)? A plain re-apply keeps
write-once blobs and never drops workspaces — use tinkerscope --pack <file> --reseed to
mirror the file exactly (refresh raw_meta blobs, drop removed workspaces, overwrite params).
Full flags + format: docs/PACK.md.
A pack is also a LINK — no restart needed. Open <base>/?w=<path-or-url-to-pack>
in the human's browser (&open=<workspace-id> picks which one lands open) and it
installs in place. A ?w= value containing /, : or . can't be a workspace id,
which is how the two are told apart. ⚠️ Against the human's LIVE instance — which is
what you are usually driving — expect a dialog every time: a first, non-colliding open
asks Install/Cancel, and an existing copy asks replace-vs-keep-both. Don't tell them a
link "just opens"; tell them to expect it. (On a published static site a non-colliding
open installs unprompted — that install lands in their browser, not in a state dir — but
a collision still asks there too.) The URL then becomes the plain
?w=<id>, so a reload opens rather than re-installs. Same thing over HTTP:
POST /api/pack/apply {source} previews (which ids exist), {source, on_conflict}
applies ("overwrite" | "new") — the HTTP route has no prompt, so prefer it when
you're scripting and the human has already agreed.
Publish a read-only copy — tinkerscope site export <dir> [--dir <scan-root>] [--title T] [--workspace NAME] [--no-logprobs] [--open <ws-id>] [--pack-url URL] [--pack-link URL|PATH=URL]. Emits a static
site (built SPA + baked JSON) that needs no backend and no API key — GitHub Pages, S3,
python3 -m http.server -d <dir>. Visitors can read everything (workspaces, branches,
threads, the chart in all three modes, token probabilities, editable highlight rules,
pins) and sample nothing. Same --dir gotcha as pack export. ⚠️ Per-token
logprobs are ~97% of a real store's bytes (measured: 24 MB of workspaces vs 901 MB
of blobs) — the command prints a per-workspace breakdown and warns past 100 MB; use
--workspace to publish a subset, or --no-logprobs (costs the token inspector and
the chart's first token mode). Unlike a share pack, a site export DOES carry
logprobs by default, plus the chart's per-workspace view state. --workspace also
drops saved PINS (they carry responses + a local dataset_path and have no workspace
id to filter on — --pins forces them back) and narrows the chart-view state to the
exported workspaces. Full doc: docs/STATIC_SITE.md.
A published site is also a reader for other people's packs, so one deployment can
serve many workspaces without re-publishing: <site>/?w=<pack url> installs into that
visitor's browser (IndexedDB, so size is not a constraint), and the human can open a pack
straight off their disk with the ⤒ button beside the workspace picker or by dropping the
file on the page. A relative ?w=./demo.yaml.gz works too, for a pack published next to
its viewer. So "share this workspace" can mean pushing one .yaml.gz rather than
re-exporting the whole site — the trade-off being that a ?w= link is a fetch
instruction, not a permalink: if the pack URL dies, so does the link.
Pass --pack-url <url> when you publish both. The site's read-only badge opens an
"open this locally" panel that turns it into a runnable --pack command, so a reader can
go interactive in one copy-paste. Without it the panel has to tell them the command
starts an EMPTY tinkerscope and won't reproduce the page — correct, but a dead end.
Pass --pack-link <url> (or <local path>=<url>, repeatable) to make the resulting
?w=<id> links shareable. After a visitor installs a pack the URL becomes the tidy
?w=pack-<pack>-<ws> — but that id resolves only in the browser that already installed
it; anyone else gets "workspace not found". --pack-link bakes {workspace id: pack URL} into the manifest, so an unknown id fetches its pack (with a progress modal)
instead. The exporter reads each pack to derive the ids and PRINTS them — that printed
?w=… is the link to hand out. Use PATH=URL when the file isn't uploaded yet. One
--pack-link and no --pack-url implies the latter.
Drive the shared playground
tinkpg ls [--filter SUB] [--sampleable-only] # discovered runs (id, base_model, #ckpts, sampleable)
tinkpg checkpoints <run> # a run's checkpoints (name, step, has-sampler)
tinkpg open <run>[@ckpt] # select a run in the human's browser (single mode)
tinkpg chat <run>[@ckpt] "<prompt>" [opts] # sample; streams to stdout + browser
tinkpg compare <run_a>[@ckpt] <run_b>[@ckpt] "<prompt>" [opts] # A→left pane, B→right pane (REPLACES the layout)
tinkpg send "<prompt>" [opts] [--panel P ...] # NEW THREAD at the CURRENT panels — layout untouched (the safe probe)
tinkpg continue "<follow-up>" [opts] [--panel P] [--thread K] [--turn N] [--node ID] [--ancestry-file FILE] # LOOM: add a turn to existing thread(s), OR to an explicit external transcript
tinkpg battery <dir> [--n N] [--pause S] [--out DIR] [--panel P ...] [--no-first-token] # fire a DIRECTORY of probe *.txt files as sequential sends (one probe = one thread)
tinkpg url [ws] [--live] [--json] # the URL of the server you're driving — what to hand the human when they want a LINK
tinkpg state [--full] [--width N] [--no-link] [--json] [--include-folded] # DIGEST of on-screen panels (active path + matched saved conv)
tinkpg params [--temperature T] [--max-tokens M] [--n N] [--thinking/--no-thinking|--thinking-both] [--top-p P] [--system S|--system-file F|--clear-system] # show / SET the GLOBAL sampling params (browser sidebar updates live)
tinkpg ws # list saved WORKSPACES + branch metadata (alias: tinkpg conv)
tinkpg ws <id|name> [--panel P] [--full] [--tree] [--include-folded] [--thread K] [--deepest] [--json] # expand one: active branch + fork counts (--tree = all branches; --thread/--deepest = read a NON-active conversation; --json = export the transcript)
tinkpg threads [--min-turns N] [--ws W] [--model SUB] [--grep TXT] [--json] # cross-workspace index of EVERY root thread + its deepest-branch turn count
tinkpg probe <run>[@ckpt] "<prompt>" [--n N] [--ancestry-file F] [--json] # sample ANY model off-workspace: nothing broadcast, nothing committed
tinkpg samples [conv] [--panel P] [--thread K|--node ID] [--turn N] [--sample K] [--slice S[:L]] [--full] [--first-token] # ALL n-sample siblings at one fork + <tag> tally; --sample/--slice = read ONE sample in PIECES; --first-token = the model's P(first generated token) at this fork
tinkpg grep "<text>" [--ws WS] [--regex] [-i] [--link] # search EVERY branch of all workspaces: content + thinking + system prompts (server-side; = the browser's Ctrl+K); --link appends a ?w=…&node=… deep-link URL per hit that opens the browser AT the match
tinkpg node <handle> [--ws WS] [--logprobs] [--meta] [--raw] [--full] [--json] [--link] # reverse lookup: locate a bare node id (no ws/panel needed), dump its record + blobs
tinkpg trash list [--workspace W] [--json] # what's recoverable: every save that made nodes disappear, newest first
tinkpg trash restore <handle> --workspace W # splice a deleted branch back at its original sibling index
tinkpg trash purge --workspace W # forget one workspace's journal
tinkpg refresh # rescan filesystem + re-probe sampling capability
Deleting is recoverable, and you are the recovery path. Every node a save makes disappear is journaled server-side, so a branch deleted in the browser survives the tab that deleted it. Two things to know:
- Start with
trash list. A<handle>is an entry id, a deleted subtree's root node id, or any node id inside it — but nobody remembers the id of the thing they just deleted, so list first and copy one from the table. - Tell them to reload the tab after a restore. The browser is still the sole writer of trees; an open tab holds the post-delete tree, and its next save re-deletes what you just put back. (The restore is re-runnable if that happens.)
Blobs are write-once and never removed, so a restored turn comes back with its
token_logprobs and raw_meta intact — tinkpg node <id> --logprobs on it works
immediately. Deleting a WORKSPACE is soft too (moved to
workspaces/.deleted/<id>-<ts>/ for 90 days), but that one has no CLI front end
yet: move the directory back by hand and restart the server.
send/continue also take --logprobs (per-token logprob + top-5 alts, native
tinker sampling only: run_id + base_model at any n. A single n=1 fire to
a loose checkpoint or OpenRouter streams through a different, logprob-free path),
--first-token (print each panel's first-token probability table right after
the fire — same view as samples --first-token without a second command)
and --json (JSONL to stdout — one object per
sample + a closing {"event":"done"}; plan/progress text moves to stderr so
stdout stays parseable). samples/grep also take --json (one JSON object /
array, untruncated) — reach for these over regexing the human-formatted text
when you're going to tally/filter programmatically.
tinkpg battery <dir> — the MCQ/probe-battery workhorse. Fires every
*.txt in a directory (sorted) as sequential new-thread sends. A probe file =
optional --- front-matter (system: — the probe's THREAD prompt, so one
file = one (message, system) thread identity; no-system:, prefill:, n:,
temperature:, max-tokens:, thinking: on|off|both, panel: a,b; unknown
keys hard-error) + the user message verbatim. CLI options are the defaults
probes don't override. Per-probe JSONL → <dir>/results/ (--out); a
first-token table prints after each probe (--no-first-token to skip);
per-probe failures are non-fatal (summary + exit 1 at the end); --pause
(default 3 s) spaces the fires so the human can watch threads land in the
browser one by one.
chat/compare options: --n N (samples), --temperature T, --max-tokens M,
--thinking (thinking renderer), --thinking-both (n samples WITHOUT thinking +
n WITH in one chat — 2n total, no-think half first; overrides --thinking),
--system "…", --checkpoint NAME (overrides @). tinkpg <cmd> --help for
the rest. send and continue add --file <path> (read the user message from a
file — a reusable probe template, mutually exclusive with the positional prompt)
and --prefill-file <path> (read the assistant prefill from a file).
Params have two routes — per-call vs global. Param args on
chat/compare/send/continue apply to THAT CALL ONLY: any param you don't
pass inherits the human's current global state (their sidebar), and nothing you
pass is written back — so a CLI probe never clobbers their setup. --no-system
fires with NO system prompt at all — global AND thread part (--n never
inherits; explicit, default 1). To DELIBERATELY change the shared state (the
human sees their sidebar update live), use tinkpg params — no options = show
current. Requires a server ≥ the params_scope contract (older servers apply the
legacy clobber-on-chat behavior).
The human can MUTE the global system prompt in the browser (the split-chip
power dot: text kept, not applied) — params/state then show it as
(muted) and per-call inherits skip it. params --system "…" always
re-enables; an explicit per-call --system applies regardless of the mute.
Thread system prompts (send --system). A thread's first message can carry
its OWN system prompt, composed over the global one at fire time (global ⏎ thread — the global is the shared base, never clobbered). On send (always a
new-thread fire) --system sets the THREAD prompt: durable (recorded on the
thread's first message, shown in the browser as a system strip on the row +
in the ⑂ threads popover) instead of ephemeral. This is THE way to run a probe
battery: tinkpg send --file q.txt --system "Answer with only the letter",
then again with other framings — same first message under different prompts =
distinct, cycleable threads. On continue, --system keeps its per-call
GLOBAL-part meaning; the thread part is inherited from the TARGET thread's
first message automatically (including --node/--thread targets on
non-active branches), so a continue into a probe thread stays under that
probe's prompt. conv <id>'s threads index prints each thread's sys: line;
samples shows the fork's thread prompt in its header.
Reading state vs. workspaces (they are DIFFERENT stores)
Panel ids (p-1, p-2, …) are minted monotonically per workspace and NEVER
reused, so <panel>:<node> stays valid — closing a column doesn't free its id for
a different model. Workspaces saved before that change also carry the old reserved
names primary / compare; those remain valid ids, they're just never minted now.
open/chat/compare REPLACE the layout, but they reuse the ids already on
screen (and mint any extra above the workspace's counter) — so a handle copied
before such a fire still names the same column. What those commands do change is
which MODEL that column is bound to, so a handle stays a valid address while
ceasing to describe the model that produced the turn; re-read provenance from
raw_meta rather than the panel label.
Vocabulary: the saved container (panels + branch trees) = a workspace; a
branch-from-start first message starts a thread; a conversation is one
dialogue inside one panel. The wire matches (/api/workspaces, workspace_id,
?w=) as of v1.0.0.
tinkpg stateshows the live panels — the transient on-screen selection + each panel's LINEAR active path (the server's state bus has no branches). It's a compact digest (first-2/last-2 messages, whitespace-collapsed):--fullfor the whole path,--jsonfor the raw untruncated state (escape hatch). Do NOT expect branches here. It also names the OPEN workspace up top —open workspace: <name> (id) → tinkpg ws <id>— because the browser pushes its?w=workspace_id onto the state bus, so you can jump straight to its branches. If that id is absent (older browser, or a CLI-only session that never opened a saved workspace), it falls back to a per-panel EXACT active-path match (← ws: <name>, or an honestambiguous ×Nwhen a short path is shared).--no-linkskips the workspaces fetch entirely. Panels the human has FOLDED in the browser print as one-line stubs here too —--include-foldedexpands them (fold info rides the open workspace, so--no-linkshows every panel).tinkpg ws(aliasconv) reads the saved workspace trees (/api/workspaces) — this is the ONLY place branches live. The tree is opaque to the server; the CLI walks it client-side (mirrorsweb/src/lib/tree.ts). List shows per-workspacenodes/branches(total forks) /active(per-panel active-path length). Expanding annotates each active turn that sits at a fork as·k/N(branch k of N), reports forks-on-path per panel, and--treeprints the full branch structure with*marking the active branch. A panel with multiple ROOT threads (branch-from-start first messages — the human often probes several prompts in one workspace) gets athreads:index: one line per thread with its first message + fan-out size,*= active; thoseknumbers feedsamples --thread k. Panels the human has FOLDED in the browser UI print as one-line stubs (skipped, with a trailing "N folded panel(s) skipped" list) —--include-foldedexpands them all, and an explicit--panelalways overrides the fold. The live panels correspond to a saved workspace but there's no stored link — match by name/recency.tinkpg threadsis the FIND primitive for CONVERSATIONS (grep finds text; this finds shapes). One row per root thread across every workspace, withdeep= user turns on the thread's LONGEST branch andturns= on its selected one. That gap is the whole point: the selected child defaults to the NEWEST, so a long conversation the human later re-rolled reads as 1 turn inws/stateand is invisible. Reach for it on "where are my multi-turn / long conversations?",--min-turns 3to cut the one-shot probes,--model health_cigaretteto scope to one checkpoint family.- Reading a NON-active conversation:
tinkpg ws <id> --panel P --thread K --deepest --full.--thread Kwalks root thread K (numbers fromthreads/ thethreads:index) instead of the active one;--deepestfollows its longest branch instead of the selected one. Without these, a thread the panel no longer points at can be listed but never read —samples --thread Konly shows one fork's fan-out. - Exporting a conversation to render elsewhere (a report, an artifact, a diff):
tinkpg ws <id> --panel P --thread K --jsonemits the selected transcript as structured data — untruncated content, CoT, node ids, and each turn'ssibling_index/n_siblingsso a quoted sample stays citable. Build rendered transcripts from THIS, never by re-parsing the terminal text (it truncates, and its⟨thinking⟩/⟨answer⟩framing is display sugar).--jsonwith no selector lists workspaces instead. tinkpg grepis the FIND primitive for TEXT: it scans every node of every branch (content,reasoning/thinking, AND thread system prompts) across all workspaces — the one command that reaches text on non-selected branches without--treedumps. Since 2026-08-10 it runs SERVER-SIDE (GET /api/search, the same engine as the browser's Ctrl+K palette) instead of pulling every body over the wire, and workspace-LEVEL matches (name / panel model id / global system prompt) print before node hits. Hits areworkspace · panel · thread k · role · node id [thinking|system] + snippet;--jsonhits also carryparent,on_active_path,sib_index/sib_countand the snippet triple. Feed a hit's node id tosamples --node <id>to see the fan-out at that exact fork — the ONLY route to n-sample views on non-selected branches (--thread/--turn walk selected paths). Use grep FIRST when the human says "somewhere in my workspaces there's …".--link(also ontinkpg node) appends a?w=<id>&panel=<p>&node=<n>deep-link URL per hit — hand the human that instead of a location table: clicking it opens their browser at the exact sample, cyclers flipped, row flashed (works on hidden branches).tinkpg node <id>is the FIND primitive for a NODE ID: when you hold a bare id (the human pasted one from the browser's Copy-node-id button, orgrep/samples --jsonprinted it) it locates the workspace · panel · thread with no other context and dumps the node record — role, sibling k/N, parent/children, finish_reason, and cruciallyprefill(a Continue/authored prefix; the stored token stream only covers what came AFTER it — the cause when token counts look short) plus which heavy blobs exist.--logprobsprints the stored per-token stream + top-K alts,--metathe request/response record,--rawthe raw stream text. Reach for it BEFORE grepping state files or hand-parsing workspace JSON — it replaces both.- Node handles are
<panel>:<node>. An id is unique per WORKSPACE, not per panel — a tree cloned into another panel keeps its ids — so one id often names the same turn in several panels. The browser's Copy-node-id button therefore hands outp-4:nt03f1, and every--node(plustinkpg node <handle>) takes<node>,<panel>:<node>or<ws>:<panel>:<node>; the parts fill in--panel/ the workspace, and an explicit flag wins. A BARE id still works —nodeprints one block per copy, whilecontinue --node/samples --nodeerror and ask for--panel. tinkpg samplesanswers "what did the model say across ALL n draws at this fork?" — the one viewstate/convcan't give you, since they only walk the linear active path. It prints every sibling response at ONE fork (default: the last user turn of the open workspace, resolved via the pushed workspace_id;--turn N/--panel P/--thread Kto aim it —--threadreaches NON-active root threads, which no active-path view shows; the default panel is the leftmost non-folded one), each with its CoT (--fullfor complete reasoning), the active one*-marked. When the answers carry<tag>X</tag>verdicts it tallies them (GOLD ×1 · CONCERNING ×11) and flags doubled-draft samples (>1 tag — the nemotron generation glitch) so you don't miscount. Use it whenever you fan out n>1 and want the distribution, not one path.
Levers & gotchas the reader won't guess
- Live drive is the point.
chat/openbroadcast to a server-side state bus, so a CLI-triggered chat appears in the human's browser identically to one they typed. Best way to show them a checkpoint's behavior:openthe run, fire achat, tell them to watch — richer than pasting the sample. tinkpg urlwhen they ask for a link. Every command auto-discovers the running instance, so you can read a whole workspace without ever learning the URL — and then have nothing to hand over.urlprints it (bare on stdout, soopen $(tinkpg url)works);url <ws>orurl --liveprints a?w=<id>link that OPENS that workspace.state's header carries both too. Don't reach forps aux | grep tinkerscope— with two instances running you'd have to guess which one holds the workspaces you just read, andurl --jsonanswers that (pid,scan_roots). A?w=link lands on the WORKSPACE, at whatever branch is selected — to point at a TURN, usegrep --link/node --link, which append the?w=…&panel=…&node=…form that opens the browser AT the match.- Workspace selector: positional or
--ws, both work.ws/samplestake it positionally (the workspace is the subject) whilegrep/node/threadsmust use--ws(their positional is the pattern / node id). Since that's easy to get backwards mid-session,ws/samples/urlaccept--wsas well; passing two different ones errors rather than picking. - ⚠️
open/chat/compareREPLACE the browser's panel layout. They push a fullpanelslist onto the shared bus, so the human's multi-panel workspace reshapes live (and mid-generation state can be lost). Before firing any of them, runtinkpg state: if the human has a many-panel workspace open (orrunning=yes), don't — usetinkpg sendinstead, which fires at the panels as they are (new thread, layout untouched, refuses whilerunning=yesunless--force). Reading (state/conv/samples) never writes and is always safe. - Run resolution: ids contain
/, never split on it. A run arg resolves by exact id, else a UNIQUE case-insensitive substring of id/name (ambiguity errors and lists candidates) — so pass the shortest unique substring. Use@forrun@checkpoint(tinkpg chat foo/bar@final "hi") or--checkpoint. Omit the checkpoint and it defaults to the last one with a sampler (usuallyfinal). sampleableis tri-state. true / false / null. Roughly half the runs arefalsebecause their base model is no longer served by Tinker — chatting them refuses cleanly.--sampleable-onlyfilterslsto the ones that work. null = unknown (Tinker offline / no key); the CLI passes through and lets the server decide, warning once.- n==1 reads one completion; n>1 draws a distribution. Default
--n 1returns a single completion — streamed token-by-token (inline to your stdout, and into the browser) for a loose checkpoint / OpenRouter, but whole-sample for a discovered run or base model (they sample native — no token stream).--n 20fans out whole samples and the browser shows an answer-distribution chart — use it for "what does this model usually say to X". With--thinking, reasoning streams first, before the answer (dimmed in a real terminal, prefixed[thinking]when piped/captured). - The browser has model kinds the CLI doesn't drive.
tinkpgtargets LoRA training runs by id. The browser's "+ Tinker model" typeahead additionally offers raw base models (no LoRA) and loose sampler checkpoints (UUID-only, picked by id/UUID) — those are browser-only selections for now.
Collaboration patterns
- "Find the conversation where the model did X" (the human half-remembers a chat
across 20+ workspaces):
tinkpg threads --min-turns 2 [--model SUB]to get every multi-turn candidate with a locator →tinkpg grep "<phrase>"if you have a phrase → read each candidate withtinkpg ws <ws_id> --panel P --thread K --deepest --full. Fan the reads out over subagents when there are more than a handful — they're read-only and each report comes back with verbatim quotes. - Sample a model no panel is bound to:
tinkpg probe <run>[@ckpt] "<prompt>" --n 8 --json.chat/comparereshape the layout andsend/continuefire at the panels as they are, so all three are limited to models already on screen — and all three COMMIT a turn into a panel transcript.probesendsbroadcast=false, commit=false: it touches no workspace at all. Reach for it whenever you want a distribution from a checkpoint the human isn't looking at, and for anything you intend to quote as "model X said" — provenance is guaranteed by construction. Multi-turn via--ancestry-file(a JSON list of {role, content}; same provenance rule ascontinue— reuse generated turns, never author them). - ⚠️ Provenance: a panel's label is not a turn's author. A panel says what it is
bound to NOW; a turn in its tree may have been produced by another model (pasted,
loomed, or committed by a chat that named this panel while sampling something else).
Real saved workspaces on this box contain exactly that. Before quoting a stored turn
as evidence about a model, check the node's
raw_meta, which records the sampler that produced it:POST /api/workspaces/<id>/node-blobs {"nodes": [...]}returns{node_id: {raw_meta, token_logprobs?}}, and thetinker://<uuid>:train:0/ sampler_weights/<ckpt>path in its request section maps to a run via/api/models(each checkpoint'ssampler_path). Nodes with no blob have NO provenance — treat them as unverified, and re-proberather than trust them. - Survey the human's probe workspace (many panels, several prompts):
tinkpg state(which models are live now — folded panels collapse to stubs) →tinkpg ws <id>(per-panel thread index + forks) →tinkpg samples --panel P --thread kfor each interesting fan-out. All read-only; folded panels stay out of the way by default. - Add a probe to the human's workspace:
tinkpg send "<prompt>" --n 20— fires a NEW thread at every unfolded panel; the browser folds the replies in live and the ⑂ threads popover picks it up. The layout-safe way to propose and run a new prompt on the models the human is already looking at. - Loom / multi-turn a probe:
tinkpg continue "<follow-up>" --n 20adds a turn to the CURRENT thread at every panel (default target = the active leaf, read from live state). Aim it at a non-active branch with--thread K/--turn N(that panel's saved tree) or--node <id>(fromtinkpg grep). A--prefill "Hmm,"(or--prefill-file) seeds a thinking opener / the model's own truncated CoT when the target ends on a user turn (answer-level loom). Same layout-safe, folded-via-the-browser path assend. - ⚠️ A CLI fan-out (
--n K) persists only ONE representative into the saved tree. The server commits sample 0 to the panel transcript, and the browser folds a FOREIGN (CLI) chat via echo-reconcile = that one representative — only the browser's OWN sends fold all K siblings from the bus bucket. Sotinkpg sampleson a CLI-fired fan-out shows 1, not K. The full K-sample fan-out streams to the CLI's stdout (each--- sample i ---block with its CoT + finish_reason) — capture that (… > log.txt) for the distribution / a<tag>tally; the workspace keeps the representative + thread structure. Because of the fold-one-rep limitation above, the BEST example of a behavior from a wide fan-out often lives ONLY in that captured stdout, not in any tree —continue --ancestry-file <path>(a JSON list of{role, content}dicts) looms from it directly, no tree node needed. - Provenance rule for looming (
continue/--ancestry-file). OK: a full, VERBATIM, previously-generated workspace as ancestry — from a tree, a raw log, or another model entirely (grafting a real workspace model A produced into model B's context to see how B judges/continues it is a legitimate probe design); a tiny--prefillthinking-opener ("Hmm,"); continuing a model's own truncated CoT verbatim. NOT ok, ever: authoring or editing any part of a turn yourself — a hand-written or hand-edited assistant message, a partial answer you completed, a doctored transcript. The line is authored vs. generated, not fresh vs. reused — a full real transcript from anywhere is fine; one fabricated sentence anywhere in it is not. - "What does checkpoint X do here?":
tinkpg open <run>@<ckpt>→tinkpg chat <run> "<prompt>"→ human watches it stream; you read the same text in stdout. - Behavior distribution:
tinkpg chat <run> "<q>" --n 30→ the browser's distribution chart shows the answer spread across samples. - A/B two runs / checkpoints:
tinkpg compare <A> <B> "<q>"(e.g. base-trained vs instruct-trained, orrun@earlyvsrun@final). Both panes stream side by side. - Get a run's training JSONL path: in the browser, ⇧-click the copy button
next to a discovered run's name — it yields the absolute path of the JSONL that
run was trained on (plain click gives the checkpoint's sampler path), ready to
paste into a dataset viewer. Ctrl+⇧-click opens it in samplescope directly
(new tab; starts a viewer if none is running) — the
sscope viewcommands in thesamplescopeskill then drive that same view from your terminal.