Skip to main content
Glama
README.md
<p align="center">
  <img src="assets/knoten.png" alt="knoten" width="360">
</p>

<p align="center">
  <b>A research graph that remembers what didn't work.</b>
</p>

- **One shared graph for a team.** A graph is its own git repository, on GitHub or
  wherever you keep repositories. Push a node and everyone who pulls has it; one branch,
  nothing to merge, and every clone refuses a node that breaks the graph's rules.
- **Dead ends are the asset.** A dead hypothesis carries why it died, what would bring it
  back, and the script that killed it. Nothing is deleted; a correction is a new node.
- **Rules are data.** Your `graph.yaml` says what a claim must carry and what it must
  survive; the tool enforces it and knows nothing else about your field.
- **It gets wiser, not just bigger.** Several findings that say the same thing become one
  general finding that supersedes them, held to every check they passed.
- **Track the number you are trying to move**, from the results your experiments already
  record, with what each run built on.
- **Agents run it before the work.** `knoten frontier` says what to do next, `knoten index`
  whether it was tried in other words, `knoten gates` what a result must survive.
- **A visualizer you can hand to anyone.** `knoten viz` writes one HTML file: the graph as
  cards, as a map, and as your metric over time, with every record one click away.
- **Zero dependencies.** Markdown files in git and PyYAML. No database, no build step, no
  server, ever.

<details>
<summary><b>Visualizer</b>: the same graph as cards, as a map, and as a metric over time (click a picture to enlarge)</summary>
<br>
<table>
<tr>
<td align="center" width="33%"><a href="assets/viz-cards.png"><img src="assets/viz-cards.png" alt="cards view with a rule unfolded and its record open" width="300"></a></td>
<td align="center" width="33%"><a href="assets/viz-graph.png"><img src="assets/viz-graph.png" alt="graph view around the gate" width="300"></a></td>
<td align="center" width="33%"><a href="assets/viz-metrics.png"><img src="assets/viz-metrics.png" alt="the metric over time" width="300"></a></td>
</tr>
<tr>
<td valign="top"><b>Cards</b>, one column per stage of the loop. A general finding carries <code>covers 2</code>; open it and the two findings it retired hang beneath it, and its record says what each contributed, which gate it survived, and where its file is.</td>
<td valign="top"><b>Graph</b>, clustered around the busiest nodes. Here the gate is selected: its rule, why it exists, and the nine claims it judged, two of them killed.</td>
<td valign="top"><b>Metric over time.</b> Each point is a run that recorded <code>return</code>, in the order the runs were made; the ring marks the best so far, the red point a run whose claim died, and hovering a point says what it built on.</td>
</tr>
</table>

One self-contained HTML file, written by <code>knoten viz</code>, that anyone can open without installing anything.

</details>

## A node

````markdown
---
id: hyp-self-consistency
type: hypothesis
status: dead
cause: weak_baseline
links:
  - {rel: kn:killedByGate, to: gate-compute-matched-baseline}
repro:
  script: experiments/self_consistency.py
  model: Qwen3-8B-Instruct
  data: GSM8K test, 1319 questions
  cmd: python experiments/self_consistency.py --n 5 --temp 0.7
results:
  acc_greedy: 0.741
  acc_self_consistency: 0.792
  acc_compute_matched_baseline: 0.788
  tokens_per_question: 1420
  n_independent: 1319
---

# Self-consistency (sample 5, majority vote) beats greedy decoding

## Verdict: DEAD
Sampling 5 chains and taking the majority scored 79.2% vs 74.1% greedy. +5.1 points.
It looked like a free win.

## Why it died
It is not free. It costs **5x the tokens**, and given the same budget a longer-CoT
baseline reaches **78.8%**. The entire gain was compute, not method.

```python
# reproduce the kill:
python experiments/self_consistency.py --n 5 --compare compute_matched
```

## What would reopen this
A task where the majority-vote *aggregation* does real work, i.e. where the gain
survives a compute-matched baseline. Plausible for code execution or theorem proving.
GSM8K is not that task.
````

<details>
<summary><b>The loop</b></summary>

```bash
pip install git+https://github.com/BY571/knoten
knoten init my-topic          # a graph is a folder

knoten frontier               # what should I work on next?
knoten index --tag decoding   # anything LIKE this been tried?
knoten gates                  # what must a claim survive here?
knoten new hypothesis hyp-x   # scaffolded from this graph's rules
knoten commit hyp-x --frontmatter fm --body b   # checked before it touches disk
knoten update hyp-x --status dead --append post-mortem.md --field cause=weak_baseline
knoten attach hyp-x run.py accuracy.png
knoten viz --open             # the whole graph as one HTML file
```

A round goes source, idea, hypothesis, experiment, finding; a scaffolded graph refuses a
step that skips the one before it. Every read command takes `--json`. Exit `0` succeeded,
`1` refused, and a refusal is the feature: read it, fix the node, run it again.

</details>

<details>
<summary><b>Rules are data</b></summary>

```yaml
rules:
  - id: deaths-must-name-a-cause
    when_status: dead
    require_field_one_of:
      cause: [no_signal, cost_hurdle, weak_baseline, underpowered, crowding_decay]
    message: A cause of death you cannot filter on is a story, not an index.
```

Once the cause is a field rather than a sentence, six months later it is a query:
`knoten index --where cause=weak_baseline`. Your graph declares its node types, statuses
and tags the same way, and a typo is a violation, not a new type. A `gate` is a check a
result should survive; by default an alive claim that cites none is listed by
`knoten frontier` as unchecked, not refused, and one commented rule makes it mandatory.

</details>

<details>
<summary><b>Track your research metric</b></summary>

```yaml
metrics:
  return: {goal: max}
```

`knoten metric return` reads the `results:` your experiments already carry, along the
time axis, each against the best before it, with what it built on:

```
  return (max)   best 15.4  exp-batch-4096  2026-06-28
    2026-06-03  exp-baseline                     2.1   baseline  ★
    2026-06-06  exp-adv-norm                     5.1         +3  ★
    2026-06-14  exp-reward-scale                10.3       +5.2  ★
    2026-06-21  exp-entropy-bonus                9.8       -0.5
    2026-06-28  exp-batch-4096                  15.4       +5.1  ★  builds on exp-reward-scale
```

</details>

<details>
<summary><b>Compress</b></summary>

When several findings under one question say the same thing, write the statement that
makes them unnecessary and point it at each of them:

````markdown
---
id: finding-sc-needs-scale
type: finding
status: alive
tags: [decoding]
created: 2026-09-06
links:
  - {rel: npx:supersedes, to: finding-sc-small-models}
  - {rel: npx:supersedes, to: finding-sc-large-models}
  - {rel: kn:survivedGate, to: gate-compute-matched-baseline}
---

# Self-consistency pays only above a size the budget can afford

Compute-matched, sampling more chains buys nothing below ~7B and about two points above it.

## Covers
- finding-sc-small-models: the within-noise result; what it drops is the per-size table
- finding-sc-large-models: the two-point gain; what it drops is the exact token count
````

The general node must face every gate its specifics faced and name each of them in
`## Covers`; they flip to `superseded` in the same commit and `knoten index` stops listing
them. `knoten frontier` opens with the shape of the graph and lists `COMPRESSIBLE`
clusters (three or more alive findings sharing a gate or a tag). The commit says what it
bought:

```
  + nodes/finding-sc-needs-scale.md  (8 nodes)

  ! This resembles 2 settled claim(s). If it is the same question, supersede or retract that node (npx:supersedes / npx:retracts) rather than leaving two answers in the graph.
    exp-self-consistency-budget  [✓ ALIVE]  Self-consistency against a compute-matched baseline, at three model sizes
    hyp-self-consistency  [✗ DEAD]  Self-consistency (sample 5, majority vote) beats greedy decoding
    compressed 2 findings into 1 under question-what-improves-reasoning
    survived the 1 gate they faced
    this graph now stands on 1 rule and 0 specifics
```

</details>

<details>
<summary><b>A shared graph</b></summary>

`knoten init` makes the graph its own git repository, whatever it sits inside: a project
repo that holds the graph folder ignores it, so the knowledge is shared without the code
and the code is pushed without the knowledge. Sharing it is one line:

```bash
gh repo create my-org/my-topic --private --source my-topic --push   # once
```

Collaborators are the repository's collaborators, on GitHub. Each of them:

```bash
git clone git@github.com:my-org/my-topic && cd my-topic && knoten hook
git pull                                              # every session: what arrived
knoten commit ...; git add -A && git commit -m "hyp-14: ..."; git push
```

`git pull` rebases here, so your commits go on top of what arrived and there is never a
merge to do; a new node is a new file, so two people filing nodes never conflict. The
gate runs on every clone before a commit, and the graph carries a GitHub Actions workflow
that runs `knoten validate` on every push, so a broken graph shows up on the commit for
everyone.

</details>

## For agents

[`SKILL.md`](SKILL.md) teaches the loop; [`src/knoten/prompts/`](src/knoten/prompts)
says how to write each kind of node, and where to look when there are no ideas left.
[`examples/rl-reward/`](examples/rl-reward) is the graph in the screenshots (a return
climbing 2 to 15 over five experiments); [`examples/llm-research/`](examples/llm-research)
a second one; [SPEC.md](SPEC.md) the design.

MIT. One dependency: PyYAML. No framework, no database, no build step, and no server.