agentleFS
tboome33tboome33/obsidian-mcp-router/skills/write-bundle/SKILL.md

write-bundle

Skill1 starsChanged 7 months agoApache-2.0View on GitHub

Run several vault writes as ONE logical operation with rollback — the note + the index + the journal + hot.md either all land, or none do. Use whenever a single user-visible action touches 2+ files and a half-written result would be wrong. Also the recovery entry point for bundles a crash left unfinished.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Call the obsidian-router write_bundle MCP tool.

    line 16
  2. Very specific:

    If the call fails entirely (not a rolled-back bundle — an actual throw), do NOT redo it with Read/Edit/Write on the vault's real path:

    line 103

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

write-bundle

Call the obsidian-router write_bundle MCP tool. It runs a list of writes as one journaled operation: every target's content is captured before the first write, and if any step fails, every file the bundle touched is put back byte-for-byte.

When to use

Whenever one user-visible action produces several files whose half-written state would be wrong:

  • save — the note + the journal line + hot.md
  • wiki-ingest — the source page + entity pages + the journal
  • wiki-fold — the fold page + the log rewrite
  • any multi-key frontmatter update that must not land halfway (this is what makes merge_frontmatter all-or-nothing — see below)

For a single file, use the ordinary write-append / write-create-or-replace / write-patch. A bundle around one write buys nothing but a journal round-trip.

Arguments

Required: steps — an ordered array, at most 25. Each entry is { op, path, ...that op's own arguments }.

oprunsneeds
writewrite_filecontent
appendappend_to_filecontent
patchpatch_fileoperation, targetType, target, content
set_frontmatterset_frontmatterkey, value
merge_frontmattermerge_frontmattervalues
deletedelete_fileconfirm: true

Optional: vault (omit for default — a bundle is single-vault, steps may not override it) · preview: true (sealed plan, writes nothing) · approvedPlanSha256 (the seal from a preview) · recover (see below) · per-step ifMatch.

move is deliberately not a bundle step: a half-rolled-back move is worse than no rollback. Express it as a delete plus a write.

What it guarantees — and what it does not

✅ No silent in-between. outcome says exactly where you landed (four values, below). Read it; do not assume success. ✅ Crash survival. The before-images are written to wiki-meta/write-journal/<operationId>.json before the first mutation, so a rollback is still possible after the process dies. A journal is deleted — or, if that fails, stamped terminal — once the operation reaches a proven end, so it can never be replayed against a bundle that already succeeded. It stays pending (and recoverable) whenever the end was not proven. ✅ Nothing is destroyed without a copy. If a rollback has to overwrite content it cannot attribute, the exact bytes it is about to overwrite are saved into the journal first — and if that copy cannot be saved, the overwrite does not happen at all. ✅ Group-wide C1. Put ifMatch on the steps that matter: every precondition is checked up front, so a bundle whose targets moved refuses entirely, before writing anything. ✅ Concurrent writers are detected, not assumed away. For write and delete steps the bundle knows the exact result it produced, so a read-back that disagrees is proof someone else wrote — that file is then left alone for the rest of the operation, rollback included.

❌ Not isolation. A concurrent reader can still see an intermediate state while the bundle runs. What disappears is the durable half-applied state, not the transient one. ❌ Not a lock. A file that someone else changed after a step the bundle can attribute is never restored over — that would be the exact clobber ifMatch exists to prevent. It is left alone, named in rollback.paths with left-modified / left-deleted / left-created, and the journal is kept. Where nothing can be attributed — a recovery run, or a step that failed before any post-image existed — the restore goes ahead as unverified and the journal keeps a copy of what it overwrote; that is why a recovery on a shared vault requires expect (below). ❌ Not byte-level. Restoration puts back the content the router read. The read path strips a leading UTF-8 BOM (the same normalisation C1's hash depends on), so a BOM-prefixed file comes back without its BOM. Everything else — CRLF, accents, emoji, trailing spaces — is exact.

Reading the result

outcomewhat happenedwhat to say
appliedevery step landeddone; operationId is the trace. If skipped is set, some steps were idempotent no-ops — say so rather than "all applied"
rolled-backa step failed; every file is back, provablyreport failedStep + error. Do not re-run blindly: fix the cause
rolled-back-unverifiedevery file is back, but the undo could not be provensay so. The failing step never confirmed a write, so what was overwritten could not be attributed — it is saved under salvage in the retained journal
rolled-back-partialsome files are still dirtyname them from rollback.paths with their reason. journalPath holds the pre-bundle content. Never summarise this as "it failed but I rolled it back"

Also worth surfacing:

  • rollback.paths[].attribution — ours (the bundle knew the exact bytes it wrote), observed (read back after the step, so a write inside that one round trip would have been adopted as the bundle's own), unverified.
  • warnings — carries concurrent-writer detections and journal problems. Both, never one silently replacing the other.
  • journalUnsafe: true — rare and serious: the operation finished, but its journal could be neither removed nor closed, so it still reads as a live recovery instruction. Delete the file at journalPath by hand before running any recovery.

If a step is refused because the file "was changed by something other than this bundle", that is not a transient error: the bundle proved a concurrent writer is working on that file. Re-read it and rebuild on the current content — do not retry the same bundle.

What a bundle costs

Every target's full content is copied into memory and into the journal, so the price is the size of the files you touch, not the size of your edit. Measured on a real vault: a three-step bundle (new page + append to journal.md + patch hot.md) carried 401 KB of before-images, almost all of it the journal. That is well inside the 5 MB bound and it is the honest cost of being able to undo — but it is why the bound exists, and why you should not casually add a large file to a bundle that does not need it.

Steps that must not be inside a bundle

A step whose failure is a normal branch of your logic will roll the whole bundle back. The classic case is the backlink dance in save: patch_file on ## Backlinks, falling back to append_to_file when the heading is absent. Resolve the branch before building the bundle (read the target, decide which op to use), or leave that write outside the bundle. Never rely on a step failing.

Recovery

write_bundle({ recover: true })                                            # read-only listing
write_bundle({ recover: "op-<16 hex>", confirm: true })                    # replay one rollback
write_bundle({ recover: "op-<16 hex>", confirm: true, only: ["a.md"] })     # …just these files
write_bundle({ recover: "op-<16 hex>", confirm: true,
               expect: { "a.md": "<currentSha256 from the listing>", "new.md": null } })  # guarded

recover: true lists the journals left by bundles that never finished, with a per-file wouldChange verdict, its currentSha256 / beforeSha256, the journalPath, and a recoverable flag (a journal already stamped terminal is listed but refused — replaying it would undo an operation that succeeded).

expect is the recovery's own ifMatch: { "<path>": "<currentSha256>" | null } for every path the run will restore (narrow with only first), copied from the listing. The run is refused before any write if a file no longer matches. It is required on a vault list_vaults reports as writesRequireIfMatch: true — a recovery restores over whatever is there, and on a shared vault that could be another workspace's edit — and honoured everywhere else.

Run the listing first and look at those files. A recovery cannot prove two things: who wrote the content that is there now, and how far the crashed bundle actually got — a crash leaves no per-step record. So wouldChange:true mixes files the bundle wrote with files you may have edited since. That is why the run form demands confirm: true, reports every restore as unverified, and accepts only so you can restore just the files you recognise as the bundle's. A partial recovery keeps the journal so the rest stays recoverable.

A leftover journal is a signal, not garbage: it means a vault may still be carrying half an operation.

On failure — remediate, NEVER fall back to filesystem writes

If the call fails entirely (not a rolled-back bundle — an actual throw), do NOT redo it with Read/Edit/Write on the vault's real path:

  • Connection error (ECONNREFUSED, timeout) → vault closed; list_vaults, then ask the user to open it via the openUri link and wait.
  • kind: "conflict" → a step's ifMatch is stale. Re-read the files, rebuild the bundle on current content, retry.
  • kind: "plan_drift" → a target moved since your preview. Re-preview, review, pass the fresh seal.

Rationale + message template: the default-vault-health-check convention (canonical source).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.