Insurgently

Technical details

Every piece of feedback is a plain file, placed on the exact words. This is how it's built.

The coach runs on Model Hub, the same base under Library Hub and Topic Map. Learning Editor holds no keys and names no model: it registers a role and its tools, and asks. Everything below is in the code, and anything still to come is marked Soon or Later.

The shape of it

Sources never touch the note. Each one writes findings, a file per note, and the marks layer places them on the words. Only a decision you make changes the text, and every decision goes into a log the learning loop reads back.

  1. Sources

    The habit checks and spelling and grammar (Harper), on your computer. The coach, through Model Hub, only when you ask. Any other tool that writes the same files.

  2. findings files

    Check and place

    Each quote is found in the note, cuts may only remove words, overlaps are settled, dismissed spans are skipped. Drops are logged with a reason.

  3. placed findings

    Marks and the panel

    Underlines, inline diffs, the card, the strip, and the panel beside the note. Apply is one editor change; dismiss can carry a reason.

  4. decisions

    The learning loop

    Reads the decision log and proposes changes to your profile, which you accept or skip. The profile feeds every source.

Written down, in your vault

  • .learning-editor/findings/: one JSON file per note and source
  • .learning-editor/dismissed.json: dismissal keys, the newest 5,000
  • .learning-editor/decisions.jsonl: each apply and dismiss, its reason and the sentence around it
  • Learning Editor/: your profile, rule notes and dictionary, as ordinary notes

All plain text. Delete .learning-editor to start over; your notes are untouched.

A finding, step by step

What happens between a source noticing a hedge and the strike-through on your screen.

  1. Find. A source returns findings. Each quotes the note exactly, names a kind (Voice, Clarity, Line, Cuts or Mechanics), says whether it's a cut, a rewrite or only a note, and says why. A quote that occurs more than once carries a few words of context.
  2. Check. The quote has to be in the note. A cut may only remove words. A quote found only in code, a link or frontmatter is dropped. Each drop is logged with its reason (not-found, ambiguous, adds-words, protected, …), never fixed quietly.
  3. Save. One file per note and source, stamped with a hash of the text the source saw. An open note reloads when its files change, so a script or another tool can write findings too.
  4. Place. All of a pass's quotes are found in the note in one go. Where two sources overlap, the stronger one shows; the coach wins over the local checks.
  5. Decide. Apply is one CodeMirror transaction, so one undo takes it back. A mark whose words changed since is refused. A fix that touches a number, a name, a quote or a kept term carries a warning, and Apply all skips it.
{
  "note": "Drafts/Walden in winter.md",
  "source": "habits",
  "noteHash": "…",
  "findings": [{
    "id": "h-3", "category": "cuts", "kind": "cut", "tag": "issue",
    "quote": "really just", "suggestion": "",
    "why": "Softeners from your profile", "pass": "softeners"
  }]
}

The modules

Small modules with one job each. They talk through one shared context and the types in core/types.ts, never through each other's insides.

core/
Findings files, checking, placing, the stores for dismissals and decisions, and the source registry. Depends on @insurgently/text.
marks/
The editor layer: underline, inline diff, card, strip, apply and dismiss, vim keys. Depends on core/ and Obsidian's CodeMirror.
checks/
The habit checks: rule notes, the starter pack, repeats, leaned-on words, long and sticky sentences.
sources/harper/
Spelling and grammar, with Harper's WebAssembly bundled and loaded on your computer.
sources/coach/
The coach run: prompt, read tools, the finish tool. Depends on Model Hub.
learning/
Reads the decision log back and builds proposals for your profile.
progress/
Habit rate per 100 words, your average, the hardest paragraphs.
panel/
A pure model of what the panel shows, and a thin view over it. Depends on @insurgently/obsidian-ui.

The coach

On the Project page it's a closer read. In code, it's one Model Hub agent run with read-only tools and a finish tool that hands in the findings.

Role
learning-editor.coach, on Model Hub's Careful tier unless you move it. The tier's own limits apply too.
Tools
read_note (only the draft being coached), read_profile, habits (counts of your habit phrases), and submit_findings. Each answers only inside a coach run.
Finish
submit_findings checks every finding. If some can't be placed, the model is told which and may resubmit twice. What can't be placed after that is dropped and counted in the notice.
Limits
8 steps, and Spending limit per run as the run's budget ($1 by default). A run that ends early saves what it handed in, or nothing.
Stop
The panel's Stop coaching or the status bar. A stopped run saves nothing.
Prompt
Markdown, read at the start of every run, with the kinds listed at the end. The opening message is your profile, then the draft verbatim.
Price
When a script runs the coach, the plan carries Model Hub's estimate of the run, and the budget you confirm caps it.

The local checks

Everything that can be counted is counted on your computer, so a typo never costs a model call.

A habit rule is a small note with the words to mark and its own examples: lines it must flag and lines it must allow. The examples run when the rules load, and a rule that fails one is switched off, with the reason in the panel. Habits listed in your profile become rules of their own, and a starter pack covers hedges, intensifiers, softeners, filler and vague words.

The measures mark close repeats (within 40 words), words you lean on (6 uses or more), very long sentences (35 words or more, and well above the note's own average) and sticky sentences: 15 words or more, at least 60% glue words, the three stickiest per note. The 60% came from published blog posts, where one sentence in ten reaches it.

Frontmatter, code, links and comments are never checked. A note is checked only once you ask, then kept up to date 1.5 seconds after typing stops. Notes over 100 KB aren't checked automatically.

Harper
Spelling and grammar, Apache-2.0, bundled as gzipped WebAssembly, so nothing is downloaded. A dictionary note beside your profile adds your own words.
Speed
On a 5,000-word note: about 9.5 ms for the habit checks and 3.3 ms for Harper, measured on an Apple M4 Pro. A benchmark gate fails a change that slows them by more than 30%.

The learning loop

Every apply and dismiss goes into decisions.jsonl with its reason and the sentence around it. The loop reads it back and asks four kinds of question.

Keep the words
The same words dismissed 3 times in one kind (Not now doesn't count). They go under Voice to keep if every reason was Voice.
Your voice
One of your habit phrases dismissed as Voice twice.
Automatic check
A fix for one of your habit phrases applied 5 times.
Turn down
The last 4 decisions on one source and kind all dismissed as Wrong.

Nothing changes by itself. An accepted proposal is one line in the right section of your profile; a turn-down is saved in settings. A skipped proposal isn't asked again. A host can switch profile writes off and get accepted proposals as JSON lines instead.

Build on it

Another Obsidian plugin can build its own editor on this code: its own kinds, coach prompt and tools, files and settings. There's no plugin-to-plugin API. It imports the modules and calls one function.

this.ctx = await startEditor(this, {
  pluginId: 'my-editor',
  categories: [...DEFAULT_CATEGORIES, { id: 'series', label: 'Series', color: '#14b8a6', defaultMode: 'a', order: 6 }],
  dataDir: '.my-editor',
  coach: { prompt: MY_PROMPT, maxSteps: 12, role: { tier: 'careful', ceilingUsd: 0.25 } },
  notePolicy: (note, fm) => (fm.status === 'published' ? { apply: false } : {}),
});

Options cover the findings layout, dismissals per vault or per note, which notes get checked, source precedence, extra Model Hub roles and tools, the panel, and bug reports. If a step fails while it starts, it takes back everything it registered.

plugin.automation runs any command from a script or a test: commands(), run(id, { target, answers }), state(), idle() and log(). A command that writes stops at a plan until it's confirmed, and confirming a finding needs the plan's markId, so a moved cursor can't change a different one.

Developer docs →

Shared libraries

Code that more than one plugin in the family needs lives in one place, published under MIT.

text
The text layer: find a quote in a note, even after it's changed, check a cut, settle overlaps, key a dismissal, spot a risky fix, and know which ranges of Markdown are prose.
obsidian-ui
The family design system: the command catalog and panel, dialogs, notices, declarative settings and bug reports.

How it's tested

Unit tests for the pure modules, a real Obsidian for the rest, a speed gate, and cards that say what a writer is trying to do, each one linked to the tests that prove it.

Unit
About 500 Vitest tests across 38 files, with real prose: curly quotes, em dashes, repeated phrases, Markdown with links and code.
End to end
11 WebdriverIO specs against a real, sandboxed Obsidian with a fake Model Hub. They run in Docker, one run at a time, so no window opens on anyone's screen.
Speed
A benchmark of the checks that run as you write, against a committed baseline.
Journeys
6 jobs, 12 journeys and 72 scenario cards, each scenario naming the tests behind it.

Technical roadmap

Learning Editor joins Model Hub, Library Hub and Topic Map. From here, the order follows what writers using it need. Nothing has a date yet.

Soon

  • Harper moves into a background worker, so a long note's checks never compete with typing.
  • The coach's price before it runs, shown in the panel from Model Hub's estimate.
  • Margin notes, a third way to draw marks beside the text.
  • Settings for one note in its frontmatter: an off switch, an audience, a purpose and a voice sample.
  • Edit a suggestion before applying it, and see which words came from a suggestion.

Later

  • Fact checks for flagged claims, once Model Hub has web search as a tool.
  • Comments as work items an agent can pick up.
  • Shared rule packs, and practice drills from your own decisions.

Getting involved

The source is public, under MIT. CONTRIBUTING.md lists the checks a pull request passes: typecheck, lint with the Obsidian plugin rules and unit tests, plus end to end for anything that touches Obsidian.

It stands on other people's open-source work: Harper for spelling and grammar, CodeMirror for the marks, and wdio-obsidian-service for the end-to-end runs. What I've built for the family goes back out as the shared libraries above.

Get involved, for the whole family →

Join

One list for everything here: an email when a plugin ships, plus the posts I write about these projects on brianspringer.com.

Want to help build this, or join the co‑op? Email me about joining →

Insurgent Methods is where we build at a different scale, to take on the biggest problems.

Needs
Obsidian 1.13.1, desktop, and Model Hub for the coach
License
MIT