English | 한국어
ResearchTree
Every branch is an experiment. Every PR is its lab note.
Turn your Git branches and pull requests into a living map of your research,
a tree that grows from your first implementation, one experiment at a time.
How it works · Get started · Training API · Open science · Guide
[!NOTE] Available now: the hosted viewer, the VS Code extension, and the Python package (see Install). An Open VSX listing is coming.
See it on a demo project: researchtree-demo, a year of made-up text-to-speech research with five versions, competing hypotheses side by side, and intent and spec documents that evolve with the evidence (repository).
The viewer and the VS Code extension speak Korean and English: they follow your browser (or VS Code) language, and you can switch in Settings. The Python CLI output is still Korean only.
🌱 Why ResearchTree?
Research moves in small steps: you take working code, change one assumption, and see what happens. Those attempts usually end up scattered across dashboards, notebooks, chat threads, and commit messages. Later it is hard to tell which idea came from where, what was adopted, and why the others were dropped.
ResearchTree fixes that with a few rules you already know how to follow:
| Rule | What it means | |
|---|---|---|
| 🌿 | One branch = one experiment | A branch holds one small, focused change that tests one hypothesis. |
| 📓 | One PR = one lab note | The PR body records the hypothesis, the change, the metrics, and the conclusion. |
| 🌳 | Branching = the tree | Branch experiment B off experiment A, and B becomes A's child in the tree. |
| ✅ | PR state = the verdict | Open means running, merged means adopted, closed means rejected. |
The viewer reads those pull requests and draws them as a tree. Click any experiment to see its results. There is no database and no server-side cache: GitHub is the only data store.
📸 Screenshots
| Version panel: tag date, merged experiments, metrics, and what grew from it | Press-down morph: the camera turns top-down and the islands flatten |
| Flat view: the same scene seen from above, with only paths, plots and stones left | Sign-in: GitHub or a personal access token |
Screenshots use sample data.
🧭 How it works
Branches become a tree
research is the root: your initial implementation, and the line where adopted experiments are merged.
Every experiment lives on experiment/<name>. When adopted experiments are merged into research, you tag the merge with the next version, and the tree keeps growing from there.
%%{init: {'gitGraph': {'mainBranchName': 'research'}}}%%
gitGraph
commit id: "initial implementation" tag: "v1"
branch experiment/baseline
commit id: "baseline"
branch experiment/lr-half
commit id: "lr 3e-4 → 1.5e-4"
checkout experiment/baseline
branch experiment/warmup
commit id: "add warmup"
checkout research
merge experiment/warmup tag: "v2"
branch experiment/warmup-cosine
commit id: "cosine schedule"
In the tree above, lr-half gets closed (rejected), warmup is adopted and merged as v2, and warmup-cosine starts from v2.
| Branch | Role | Shown in the tree |
|---|---|---|
research |
Root of the tree, versioned with tags research/v1, research/v2, … (research/v1.1 works too; prefixed with the root branch so they never clash with release tags like v1 on main) |
✅ as the root and version milestones |
experiment/* |
One branch per experiment | ✅ as nodes |
main, develop, everything else |
Deployment, general development, unrelated work | ❌ hidden |
[!TIP] The root branch name (
research) and the experiment prefix (experiment/) are only defaults. You can change both per repository in the viewer's branch settings. SetRESEARCHTREE_PREFIXso the training-script API uses the same prefix.
The PR body is the lab note
Put a single yaml block at the top of the PR body. The viewer reads only the first yaml block. Everything below it is free-form Markdown.
```yaml
parent: experiment/baseline-moshi
hypothesis: Halving the depth-transformer LR reduces early divergence
change: lr 3e-4 → 1.5e-4
metrics:
val_loss: 2.31
wer: 0.184
wandb: https://wandb.ai/...
status: rejected
tags: [lr]
```
## Conclusion
Divergence went down, but convergence was too slow. Rejected.
| Field | Meaning | |
|---|---|---|
hypothesis |
required | One sentence you want to test |
parent |
recommended | Parent experiment branch, or research@vN for an experiment that starts from a research version. Falls back to the PR's base branch |
change |
recommended | Summary of the code or config change |
metrics |
optional | Final metrics (name: number | string). Agree on key names within your team |
wandb |
optional | Link to curves or other external records |
status |
optional | running | adopted | rejected, to override the status derived from the PR |
tags |
optional | Labels such as lr, data, arch |
Unknown fields are kept. When ResearchTree rewrites a PR body, it changes only the YAML block and preserves comments, key order, and the Markdown below. A broken YAML block is never overwritten.
How status, parent, and versions are resolved
Status, first match wins:
statusin the YAML- PR merged →
adopted - PR closed without merging →
rejected - Otherwise (open) →
running. Draft PRs are drawn as sprouts.
Parent, first match wins:
parentin the YAML.research@vNattaches to that version. An unknown version shows a warning and attaches to the first version.- The PR's base branch. If it is
researchand version tags exist, the node attaches to the latest version tagged before the PR was opened. - Anything else (for example
main): the node is marked as an orphan and attached under the root.
Versions
- An adopted experiment merged into
researchproduces a version: the tag on its merge commit, or the first tag after the merge. - A version continues from the end of the adopted chain that was merged: if the merged experiment had adopted children merged into it (and those had their own), the version grows from the last one, since its code holds every change down to there. When a chain has several ends, or several experiments go into one version, the latest end is the parent and every other end joins the new island with a dashed merge line.
- A version's metrics are those of the chain end it grows from. They are the Δ baseline for experiments that start from that version.
Which PRs appear
- Only PRs whose head is
experiment/*become nodes. - A PR from
experiment/*into a non-research branch (main,develop) is hidden, unless its YAML has aparent. - If a branch has several PRs, the most recent one whose base is
researchorexperiment/*is used.
Starting a new version and a new experiment
# adopt an experiment into research and tag the next version
git switch research && git merge --no-ff experiment/depth-lr-warmup
git tag -a research/v2 -m "v2: adopt warmup" && git push origin research research/v2
# start a new experiment from that version (PR base: research, YAML: parent: research@v2)
git switch -c experiment/warmup-cosine research/v2
# or derive one from another experiment (PR base: the parent experiment)
git switch experiment/baseline-moshi
git switch -c experiment/depth-lr-half
Open a draft PR as soon as you start. To adopt, merge into the parent. To reject, write the conclusion and close the PR. Finished branches are cleaned up when you cut the next version:
researchtree release # plan: delete merged branches, archive the rest, keep ones with running children
researchtree release --yes # archive (revert the branch's commits, merge into research), tag research/vN, push, delete
Unmerged branches are never just deleted: their commits are reverted on the branch (a revert, not a reset) and the branch is merged into research, so the history stays reachable while the code is unchanged.
[!IMPORTANT] Turn off "Automatically delete head branches" in your repository settings. If a parent branch with running children is deleted, GitHub retargets the children's PRs and the tree loses that link. Let
researchtree releasedelete branches instead.
🏝️ The viewer
| 🏝️ Research island (default) | A pastel voxel island in isometric view. research is the big old tree. Dirt paths tinted with each branch's color lead to garden plots where experiments grow. Research versions are stone obelisks with banners, and every version gets its own island, set apart by open water. Where a path leaves an island it ends at a wooden ferry landing, and a little moored ferry with a sail and a flag in the branch color bobs in the channel. On first open, the island grows generation by generation. |
| 🌿 Plants show status | Adopted: a grown tree with fruit and a fence · Running: a swaying sapling · Rejected: a grey stump with fallen leaves · Draft: a sprout |
| 🗺️ Flat view | Press Flat view and the camera turns top-down while the plants are pressed flat. Ground, water, landings, ferries and decorations disappear, leaving the dirt paths, plots, version stones and label chips on the page; sea crossings become dashed path tiles so branches stay connected. In flat view you pan and zoom only. The same button then cycles on: Tree view turns the flat view so the first version is at the bottom and the latest at the top, 3D view raises the islands at that angle, and Island view goes back to the first view. Your selection is kept throughout. |
| 📋 Detail panel | Hypothesis, change, and conclusion (sanitized Markdown), metrics with Δ against the parent experiment or version, the commit list, child-experiment chips, and Open on GitHub. |
| 🗿 Version panel | Tag date and commit, merged experiments, the version's metrics, experiments that start from it, and the next version. |
| 🎛️ Navigation | Status layers (running / adopted / rejected), a label metric chosen per island in the version panel (each research version can show a different metric), a generation navigator, arrow-key navigation (← → parent/child, ↑ ↓ siblings, Esc to close), and a camera that flies to whatever you click. |
| 🍂 Timeline & seasons | A real time axis: nodes sit at their start date, and the year and season at the center of the screen are shown under the top-left card as you pan. Each tree keeps its branch color and shows the season of its last work with effects (spring blossoms and falling petals, summer fireflies, autumn falling leaves, winter snow caps), and the island ground changes season along the axis. |
| 🔗 Share links | ?repo=owner/name&node=experiment/x opens the same repository and experiment for anyone with access. |
| ⚠️ Warnings | Broken YAML, missing required fields, and orphan nodes get a badge next to the node. |
The viewer needs WebGL. Without it, the tree cannot be drawn.
🚀 Three ways to use it
One viewer, three hosts. Pick whichever fits your setup.
| Hosted web | VS Code extension | Local server | |
|---|---|---|---|
| Install | Nothing | darkpyonix.researchtree |
uv tool install researchtree |
| Open | https://darkpyonix.github.io/researchtree/ |
ResearchTree: Open Tree |
researchtree serve |
| Sign in | GitHub or a PAT | VS Code's built-in GitHub account | Device Flow or RESEARCHTREE_TOKEN |
| Repository | ?repo= or the picker |
Workspace origin remote |
Current directory's remote or --repo |
| Extras | Share links | Checkout a branch, diff vs. parent | Nothing goes through a central server |
| Status | Live | On the VS Code Marketplace | On PyPI |
🌐 Hosted web
Open the site, sign in, pick a repository. The repository list shows repositories you can access that have a research branch (or your configured root branch).
To limit access to chosen repositories, open Sign in with a personal access token (PAT) on the sign-in screen instead. For a fine-grained token, grant the target repository Pull requests (read/write) and Contents (read). The token is stored only in that browser.
🧩 VS Code extension
Run ResearchTree: Open Tree from the command palette. The repository comes from your workspace's git remote, and sign-in uses VS Code's GitHub account, so you only click Allow once. Inside VS Code you can also check out an experiment branch locally and open its diff against the parent in the diff editor.
Install it from the VS Code Marketplace, or run code --install-extension darkpyonix.researchtree.
💻 Local server
A self-hosted option. It needs no central site and no auth proxy, and your machine doesn't need Node, because the built viewer ships inside the wheel.
uv tool install researchtree
researchtree serve # opens http://127.0.0.1:7337 in your browser
| Command | What it does |
|---|---|
researchtree serve [--repo owner/name] [--port N] [--no-browser] |
Serve the viewer locally. Defaults to the current directory's repository |
researchtree login / logout |
Device Flow sign-in from the terminal (for headless GPU boxes) / delete the stored token |
researchtree open [--repo owner/name] |
Open the current repository in the hosted viewer |
On a machine without a browser, you can also pass a token through the environment:
export RESEARCHTREE_TOKEN=github_pat_...
researchtree serve
🧪 Log from your training script
Add the same package to your training project, and your training script can write straight into the current branch's PR.
import researchtree as rt
rt.log(val_loss=2.31, wer=0.184) # merge into `metrics` (None deletes a key)
rt.set(wandb=run.url) # set any YAML field
rt.conclude("rejected", "Converged too slowly", heading="Conclusion") # set status + write a "## Conclusion" section
- Never breaks training. If the branch isn't an experiment branch, no PR is found, or GitHub fails, you get a warning and the script keeps running.
- Distributed-safe. It acts only on rank 0 (
RANK/LOCAL_RANK). - Conclusion section. An existing conclusion section is updated in place, whether its heading is English (
## Conclusion) or Korean. A new one getsheading(the default is the Korean heading used by the original convention). - Lossless. It edits only the YAML block, using the same rules and the same test fixtures as the TypeScript viewer.
| Environment variable | Purpose |
|---|---|
RESEARCHTREE_TOKEN |
GitHub token. Takes precedence over the token saved by researchtree login (OS keyring or a 0600 file) |
RESEARCHTREE_PREFIX |
Experiment branch prefix. Default: experiment/ |
RESEARCHTREE_REPO |
owner/name, if it can't be inferred from the origin remote |
🧠 Research memory for agents
The same record doubles as memory for coding agents. rt.load() returns the whole PR tree as typed Python objects, versions (islands) → experiments → body sections → commits. An agent starts from a short manifest and drills down only where it needs to, so questions like "how many experiments were rejected per version?" become one-line computations instead of searches through notes.
import researchtree as rt
research = rt.load() # the repo of your `origin` remote
print(research.manifest()) # islands, best results, running work, alerts
e = research["duet-mix"] # .hypothesis .metrics .parent .children .conclusion
e.delta("tts_wer"), e.commits() # vs the parent; commits are read lazily from GitHub
research.experiments.where(status="adopted").best("val_loss")
{v.name: len(v.experiments.where(status="rejected")) for v in research.versions}
research.check() # deterministic rules: stale runs, regressions, missing metrics
From a terminal: researchtree memory, researchtree memory v3, researchtree memory duet-mix, --check, --json.
To teach a coding agent the whole workflow (branch and PR conventions, the PR body, rt.log, this memory API, researchtree release), install the bundled skill into your repository:
researchtree skill install # writes SKILL.md to .claude/skills/researchtree/ and/or .agents/skills/researchtree/
[!NOTE] Inspired by User as Code (Li, 2026), which keeps agent memory as typed Python state plus executable rules. Here the pull requests are the append-only log and the YAML block is already the typed state, so no separate structuring step is needed. GitHub stays the only data store.
Intent and spec documents
Keep why (INTENT.md: goals, claims with ids like N1, non-goals), what the system is now (SPEC.md: decisions only, one feature per section, each opening with a one-line > summary) and evidence (PR bodies) apart. An experiment that changes the design edits SPEC.md on its own branch, so each version's spec holds only adopted designs, and experiments list the claims they test with claims: [N1].
researchtree spec --summary # the latest design on one page
researchtree spec --diff # what changed since the previous version
researchtree spec --claims # each claim with the experiments that tested it
In the viewer, the version panel's Spec tab shows each version's spec in full, as a summary, or as changes since the previous version, with a history per section, and the experiment panel's Spec tab shows what that branch changed. Set other paths in .researchtree.yml on the root branch (spec:, intent:, prefix:). See the guide.
📦 Install
pip install researchtree # into a training project (or: uv add researchtree)
uv tool install researchtree # or as a standalone CLI
Optional: pip install "researchtree[keyring]" stores the token in the OS keyring instead of a file.
To install the latest development version from the repository, use pip install git+https://github.com/DarkPyonix/researchtree. The build hook compiles the viewer, so this needs Node and npm.
🔒 Security & privacy
- Your token stays on its host. It lives in browser
localStorage(web), the extension host (VS Code, never the webview), or the local Python process (local). It is only ever sent toapi.github.com, and every call goes through a path-only API check. - The auth proxy only exchanges codes. The small Cloudflare Worker turns an OAuth code into a token for the hosted site. It stores and logs nothing, and it accepts only allow-listed origins.
- The local server stays local. It binds to
127.0.0.1, rejects unexpectedHostheaders (DNS rebinding), requires a per-run session token, and blocksDELETEon its GitHub relay. - PR bodies are untrusted. Markdown is sanitized with DOMPurify, and a strict CSP allows only bundled scripts. Nothing loads from a CDN at runtime.
- No database. Everything is read from and written to GitHub. Nothing is cached on a server.
🔬 Open science
Every experiment is an ordinary Git branch and every lab note is an ordinary pull request, so the full record of a research project lives in the repository itself: the exact code of each attempt, the hypothesis behind it, the metrics it produced, and why it was adopted or ruled out. Nothing sits in a private notebook or a separate database.
That makes the whole experimental history easy to publish. Make the repository public and you share more than the final result: the path that led to it, including the negative results and abandoned ideas that papers usually leave out. Anyone can check out a branch to reproduce a single experiment, compare it with its parent, or branch off from it to continue the work.
ResearchTree supports open science in this way: transparent, reproducible research whose experiment records can be shared in full.
🛠️ Development
Repository layout and commands
An npm-workspaces monorepo with one level of products under apps/. tests/ mirrors the same names.
| Path | What |
|---|---|
apps/core |
TypeScript shared logic: Host interface, GitHub client, PR-body parser, tree builder |
apps/ui |
The viewer: the island view (three.js, 3D and flat), the web / local / extension hosts, and the Vite build for all three |
apps/extension |
VS Code extension host |
apps/proxy |
Cloudflare Worker for the OAuth code exchange |
apps/researchtree |
Python package: CLI, local server, Device Flow, rt.log / set / conclude |
tests/ |
vitest and pytest suites, plus fixtures shared by the TS and Python PR-body parsers |
npm install # from the repo root
npm run dev # web dev server on :5173
npm test # vitest
npm run typecheck
npm run build # web + local + extension builds
npm run package:extension # .vsix
uv run pytest # Python tests
uv build # Python wheel
The guide covers architecture and details.
🤝 Contributing
Issues and pull requests are welcome. A few conventions:
- Commit messages use
Type: Summaryon one line, in English and in the imperative mood, for exampleFeat: Add tree vieworFix: Keep selection on view switch. Types:Feat,Fix,Refactor,Test,Docs,Chore. - PR-body rules are implemented twice, in TypeScript and Python. Change
tests/fixtures/prbody-cases.jsonfirst, then make both implementations pass. - Run the tests and the typecheck for every package you touch.
📄 License
Apache License 2.0 © DarkPyonix
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file researchtree-1.1.1.tar.gz.
File metadata
- Download URL: researchtree-1.1.1.tar.gz
- Upload date:
- Size: 3.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae2c31cc806860d24a2912fbc665359d23c9e2d4eb7cb1d7aad7552e9412554d
|
|
| MD5 |
e3bb698201acffcef9d0ca060c4ac970
|
|
| BLAKE2b-256 |
b695cf2fb2b809a2c9ae3596b11d637618b00350b14168a0f88bf152f36296ee
|
File details
Details for the file researchtree-1.1.1-py3-none-any.whl.
File metadata
- Download URL: researchtree-1.1.1-py3-none-any.whl
- Upload date:
- Size: 3.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4588c9c0040f034485b71ba7950f5279bc8b1cfc44cb165b4137b50975a06f0d
|
|
| MD5 |
f690d34ce5d1caf40b1089d19f490025
|
|
| BLAKE2b-256 |
792003fa6cc3e61dc02f0e09474431887f1692509ab76f88cfa4d72a24420103
|