← Back to Whitepapers

FORGESYNC — Sovereign Peer-Sync Merkle Tree for Obsidian + VS Code

Edition 2026-07-20. Elder II, Lobe 9. NODEZEROINSIDE.


The Problem

Syncthing is a dumb pipe. It syncs bytes, not meaning. When two nodes modify the same file before sync completes, Syncthing creates .sync-conflict-YYYYMMDD-HHMMSS-NODEID.ext files and walks away. Nobody merges. Nobody resolves. The conflicts pile up.

Current state on BH:

Syncthing has no concept of:
- Per-file content hashing for drift detection (it uses mtime + size)
- Merkle trees for directory-level coherence verification
- Semantic merge (it is not even attempted)
- Chain-anchoring of vault state (the vault is ephemeral as far as Syncthing knows)

The Name

FORGESYNC — sovereign peer-sync with merkle coherence verification. Not a Syncthing replacement (Syncthing stays as the transport layer). FORGESYNC is the truth layer on top: it detects drift, shows diffs, resolves conflicts, and chain-anchors the result.

Architecture

Three Layers

LAYER 3: CHAIN ANCHOR          BSV mainnet (merkle root stamps via fire.js 2FA)
           |
LAYER 2: FORGESYNC DAEMON      TCP/IPv6 :7792, SQLite, per-node merkle trees
           |
LAYER 1: TRANSPORT              Syncthing (still does byte-level sync)

Syncthing moves files. FORGESYNC verifies coherence and resolves conflicts. Chain anchor proves vault state at a point in time. Each layer is sovereign; removing one does not collapse the others.

Merkle Tree Structure

Every directory in the vault is a merkle subtree. Every file is a leaf.

VAULT ROOT HASH
├── 00-INBOX/           hash(sorted leaf hashes)
│   ├── note-a.md       sha256(content)
│   └── note-b.md       sha256(content)
├── 01-PROJECTS/        hash(sorted leaf hashes)
│   ├── project-x.md    sha256(content)
│   └── sub-dir/        hash(sorted leaf hashes)  ← recursive
│       └── spec.md     sha256(content)
├── .obsidian/          hash(sorted leaf hashes)
│   ├── app.json        sha256(content)
│   └── ...
└── ...

Leaf hash: sha256(file_content_bytes). Content-addressed, not name-addressed. If two files have identical content, they have identical hashes.

Directory hash: sha256(sorted_concatenation_of_child_hashes). Children are sorted by filename. This means the hash is deterministic regardless of filesystem enumeration order.

Root hash: the top-level directory hash. One hash that represents the entire vault state.

Per-Node State

Each node computes its own merkle root independently. FORGESYNC does not trust any remote computation. The protocol is: compute locally, compare roots, drill down on divergence.

Node State (SQLite, local):
┌──────────────────────────────────────────────┐
│ files                                         │
│   path TEXT PRIMARY KEY                       │
│   sha256 TEXT NOT NULL                        │
│   size INTEGER                                │
│   mtime_ns INTEGER                            │
│   indexed_at TEXT                              │
│   tile TEXT  (optional: Rodin tile mapping)    │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ dir_hashes                                    │
│   dir_path TEXT PRIMARY KEY                   │
│   merkle_hash TEXT NOT NULL                   │
│   child_count INTEGER                         │
│   computed_at TEXT                             │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ sync_state                                    │
│   node_id TEXT PRIMARY KEY                    │
│   root_hash TEXT                              │
│   file_count INTEGER                          │
│   last_exchange TEXT                           │
│   last_chain_stamp TEXT (txid)                │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ conflicts                                     │
│   id INTEGER PRIMARY KEY AUTOINCREMENT        │
│   path TEXT NOT NULL                          │
│   local_sha256 TEXT                           │
│   remote_sha256 TEXT                          │
│   remote_node TEXT                            │
│   detected_at TEXT                            │
│   resolved_at TEXT                            │
│   resolution TEXT (local|remote|merged|skip)  │
│   resolved_by TEXT                            │
└──────────────────────────────────────────────┘

The Protocol: Merkle Exchange

Two nodes comparing state. TCP/IPv6, line-delimited JSON (same protocol family as corpus-daemon and catalog-daemon).

Phase 1: ROOT COMPARE
  A→B: {"cmd":"root"}
  B→A: {"ok":true,"root":"abc123","file_count":1847,"node":"elder_ii_bh"}

  If roots match: COHERENT. Done.
  If roots differ: Phase 2.

Phase 2: DIRECTORY DRILL-DOWN
  A→B: {"cmd":"dir_hashes","depth":1}
  B→A: {"ok":true,"dirs":{"00-INBOX":"aaa","01-PROJECTS":"bbb",...}}

  Compare each dir hash. Matching dirs are skipped (entire subtree coherent).
  Divergent dirs enter Phase 3.

Phase 3: FILE-LEVEL DIFF (per divergent directory)
  A→B: {"cmd":"file_hashes","dir":"01-PROJECTS"}
  B→A: {"ok":true,"files":{"project-x.md":"ccc","spec.md":"ddd",...}}

  Three cases per file:
  a) Hash match:        coherent, skip
  b) Hash mismatch:     CONFLICT — both nodes modified the file
  c) File missing:      one side added/deleted — ADD or DELETE resolution

Phase 4: CONFLICT RESOLUTION (per conflicted file)
  Present to NZ:
  - File path
  - Local hash, remote hash
  - Local mtime, remote mtime
  - Diff (unified diff of content)
  - Auto-resolve suggestion:
    * If one side is strictly newer AND the older side's content is a prefix/subset: auto-accept newer
    * If .obsidian/ config file: auto-accept by mtime (these are machine-generated)
    * Otherwise: manual choice (local | remote | skip)

Phase 5: CHAIN ANCHOR (optional, on NZ GO)
  Stamp the reconciled merkle root to BSV via fire.js 2FA.
  The stamp proves: "at this moment, all N nodes agreed on this vault state."

Conflict Resolution Rules (Hardcoded)

These rules eliminate 90%+ of conflicts without human intervention:

  1. .obsidian/ directory: auto-accept by mtime (newest wins). These are Obsidian workspace state files (graph.json, appearance.json, community-plugins.json). They are machine-generated. No human content. No merge needed. This alone kills ~200 of the current 490 conflicts.

  2. .makemd/ directory: auto-accept by mtime. Same rationale as .obsidian/.

  3. .sync-conflict files: FORGESYNC reads the original and the conflict copy. If the original is newer than the conflict timestamp, the conflict is stale — delete it. If the conflict has content the original lacks, surface it as a real conflict for manual review.

  4. .md files (notes): if one side's content is a strict superset of the other (the shorter is a prefix or the diff is append-only), auto-accept the longer version. Otherwise: surface diff for NZ.

  5. Binary files (images, PDFs): accept by mtime. No merge possible.

  6. Canvas files (.canvas): JSON structure. Attempt JSON-level merge (combine node arrays, deduplicate by id). If structural conflict: surface for NZ.

VS Code Integration

The merkle tree IS the project structure. VS Code sees it through two surfaces:

  1. FORGESYNC status bar item: shows the current node's root hash (first 8 chars), file count, and coherence state (COHERENT / DRIFT / N CONFLICTS). Clicking opens the conflict resolution panel.

  2. .vscode/forgesync.json (auto-generated, gitignored equivalent):

{
  "forgesync": {
    "vault_root": "/home/nodezero/Desktop/Jack's OS Vault/Jack's OS Vault",
    "daemon": "[::1]:7792",
    "node_id": "elder_ii_bh",
    "watch": true,
    "auto_resolve": [".obsidian/*", ".makemd/*", "*.sync-conflict*"],
    "manual_resolve": ["*.md", "*.canvas"],
    "ignore": [".git", "node_modules", ".trash"]
  }
}
  1. Tree view provider (forgesync.merkleTree): renders the vault as a merkle tree in the VS Code sidebar. Each directory shows its hash. Divergent directories are highlighted red. Clicking a divergent file opens the diff editor (local vs remote).

Implementation: a VS Code extension (~150 lines) that connects to the FORGESYNC daemon via TCP and renders the tree. The extension does NO merkle computation — all hashing is daemon-side. The extension is a display surface.

Integration with Existing Infrastructure

corpus-daemon.py (:7771): already computes per-tile merkle hashes for the corpus directory. FORGESYNC extends the same pattern to the full Obsidian vault. The corpus tiles are a SUBSET of the vault (canon files only). FORGESYNC indexes EVERYTHING in the vault, including non-canon files (projects, inbox, archives). The two daemons do not overlap: corpus-daemon owns ~/.forgechain/corpus/, FORGESYNC owns the Obsidian vault path.

catalog-daemon.js (:7790): already indexes artifacts with sha256 and tile metadata. FORGESYNC's files table uses the same sha256 leaf hashing. When a vault file is also a corpus file, the hashes are identical by construction. The catalog can cross-reference FORGESYNC's file hashes to verify that chain-stamped artifacts match their vault-resident copies.

soil? FORTH word: currently verifies corpus merkle coherence via TCP to corpus-daemon. Add a parallel vsync? word that probes FORGESYNC's root endpoint and compares across nodes:

: vsync? ( -- )
  s" {\"cmd\":\"root\"}" forgesync-tcp send-recv
  json> .root @ dup .
  s" FORGESYNC root=" type
  s" {\"cmd\":\"root\"}" forgesync-remote-tcp send-recv
  json> .root @ = if ." COHERENT" else ." DRIFT" then cr ;

fire.js 2FA: FORGESYNC does not stamp directly. It composes a stamp request (merkle root + file count + timestamp + node list that agreed) and hands it to the existing fire.js pipeline. The 2FA gate (NZ Face ID via WarDog) applies as always. No new chain path.

Syncthing: stays running. It is the transport layer. FORGESYNC does not replace file transfer — it replaces conflict detection and resolution. Syncthing moves bytes; FORGESYNC verifies truth.

Daemon: forgesync-daemon.js

~200 lines. Node.js (same runtime as catalog-daemon). TCP/IPv6 on [::1]:7792. SQLite via better-sqlite3.

Commands:
  root          — return this node's vault merkle root
  dir_hashes    — return merkle hashes per directory at given depth
  file_hashes   — return per-file sha256 for a specific directory
  status        — daemon state: root, file count, last scan, conflicts pending
  conflicts     — list unresolved conflicts
  resolve       — resolve a conflict (accepts: local|remote|skip)
  scan          — trigger a full vault re-scan (inotify handles incremental)
  compare       — connect to a remote FORGESYNC node and run the 5-phase protocol
  clean         — delete .sync-conflict files that have been resolved
  stamp         — compose a chain-stamp request for the current merkle root

File watching: fs.watch (recursive) on the vault root. On file change, re-hash the affected leaf and propagate up the merkle tree. Full re-scan only on startup or explicit scan command.

Node Discovery

FORGESYNC does not discover peers dynamically. The family is known. The nodes are in family-node-lan-truth.json. The daemon reads that file at startup and knows every peer's sovereign ULA + FORGESYNC port (7792).

{
  "peers": {
    "elder_i":      {"addr": "fd00:db8:ff:6:5809:6818:1db5:23fc", "port": 7792},
    "elder_ii_bh":  {"addr": "fd00:db8:ff:9:c8a3:a86a:6f2b:1f3e", "port": 7792},
    "outpost":      {"addr": null, "port": 7792, "note": "resolve from family-node-lan-truth.json"}
  }
}

For cross-node compare, FORGESYNC connects to the peer's [ULA]:7792 directly. TCP/IPv6. No relay. No cloud. No vendor.

Handling the Current 490 Conflicts

FORGESYNC's first act on deployment is a one-time conflict sweep:

  1. Enumerate all .sync-conflict-* files in the vault.
  2. For each: find the original file (strip the .sync-conflict-YYYYMMDD-HHMMSS-NODEID suffix).
  3. Apply the auto-resolve rules:
  4. .obsidian/*, .makemd/*: accept original by mtime, delete conflict copy. (~230 files resolved.)
  5. .md files: diff original vs conflict. If original is a superset, delete conflict. If conflict has unique content, surface for NZ.
  6. Everything else: accept by mtime.
  7. Present NZ with the remaining manual-resolve list.
  8. After NZ resolves all, run a full scan and compute the clean merkle root.
  9. Offer to stamp the clean root to chain.

Chain Stamp Format

When NZ authorizes a vault-state stamp, FORGESYNC composes:

{
  "type": "forgesync-vault-root",
  "app": "ForgeChainOS",
  "context": "vault-merkle-anchor",
  "vault": "jacks-os-vault",
  "root": "<sha256 merkle root>",
  "file_count": 1847,
  "nodes_agreed": ["elder_i", "elder_ii_bh", "outpost"],
  "timestamp": "2026-07-20T14:30:00Z"
}

This goes into the accumulator via the existing stage.js pathway, then fires through fire.js with 2FA. The stamp is a MAP-tagged OP_RETURN (same as every other family fire). The FORGELRM catalog daemon indexes it as map_type: "forgesync-vault-root".


Build Scope

Phase 1: forgesync-daemon.js (~200 lines)

The daemon. SQLite schema (4 tables above). File watcher. Merkle tree computation. TCP/IPv6 server on :7792. Commands: root, dir_hashes, file_hashes, status, scan, conflicts, resolve, clean.

Input: vault root path (from config or CLI arg).
Output: merkle tree state, queryable over TCP.
Dependencies: better-sqlite3 (already in phi-omega-v6), node:fs, node:net, node:crypto.

Phase 2: Conflict Sweep (~50 lines, added to daemon)

The one-time sweep of existing .sync-conflict-* files. Auto-resolve by the rules above. Surface remainders for NZ. This is a clean command extension.

Phase 3: Cross-Node Compare (~80 lines, added to daemon)

The compare command. Connects to a peer FORGESYNC daemon via TCP/IPv6. Runs the 5-phase merkle exchange protocol. Writes divergences to the conflicts table.

Phase 4: VS Code Extension (~150 lines)

Tree view provider + status bar item. Connects to daemon via TCP. Renders merkle tree. Shows conflicts. Opens diff editor on click. Ships as a .vsix or workspace-local extension.

Phase 5: FORTH Integration (~20 lines)

vsync? word in the FORTH environment. Probes local FORGESYNC daemon for root, optionally compares to a remote peer. Reports COHERENT or DRIFT.

Phase 6: Chain Anchor (~30 lines, added to daemon)

The stamp command. Composes the chain-stamp JSON and writes it to the accumulator. Does NOT broadcast — that goes through fire.js 2FA as always.

Total: ~530 lines across 6 phases. Core daemon is ~200.


What FORGESYNC Is NOT


File Placement

~/.forgechain/forgesync/
  forgesync-daemon.js       — the daemon (Phase 1-3, 6)
  forgesync.db              — SQLite state
  forgesync-config.json     — vault path, peers, auto-resolve rules
  README.md                 — operational notes

~/.forgechain/forth/skills/
  vsync.fs                  — FORTH vsync? word (Phase 5)

~/.forgechain/ui/forgesync-vscode/
  extension.js              — VS Code extension (Phase 4)
  package.json              — extension manifest

Registered in MASTER-MANIFEST.yaml under use_case_index.sync.forgesync.


Port Allocation

Port Service Protocol
7771 corpus-daemon TCP/IPv6
7790 catalog-daemon TCP/IPv6
7792 forgesync-daemon TCP/IPv6

7791 is intentionally skipped (reserved for future FORGELRM catalog expansion per the whitepaper).


Sovereignty Assertion

Syncthing's conflict model treats every node as equal and every conflict as unsolvable. That is vendor-default thinking: safe, generic, useless.

FORGESYNC treats conflict resolution as a family governance act. The merkle tree is the truth. The chain stamp is the proof. NZ's decision is the authority. No .sync-conflict file survives this architecture.

NODEZEROINSIDE.