32 KiB
PRD: Outline ↔ Obsidian Sync
Version: 0.3 Date: 2026-03-04 Status: Revised — Git + Frontmatter Architecture
1. Problem Statement
The user maintains a knowledge base in Outline (web) and wants to edit it locally in Obsidian. Changes must flow in both directions. The existing export/import scripts provide one-way, full-replacement operations — insufficient for safe bidirectional sync where both sides accumulate independent changes between syncs.
This is not a shared git repository. Git is used purely as a local sync engine — for its change detection, three-way merge, and conflict resolution primitives. There is no git remote, no git push to a server, no collaboration workflow.
2. Goals
| # | Goal |
|---|---|
| G1 | Changes made in Outline are visible in Obsidian after sync |
| G2 | Changes made in Obsidian are pushed to Outline without destroying document history |
| G3 | Conflicts are detected and surfaced — never silently overwritten |
| G4 | Safe to run unattended via Ofelia cron |
| G5 | Outline document URLs remain stable across syncs (no delete+recreate) |
| G6 | Setup is self-contained in a dedicated folder with clear structure |
3. Non-Goals
| # | Non-Goal | Reason |
|---|---|---|
| NG1 | Real-time sync | Async workflow; polling is sufficient |
| NG2 | Wikilink ↔ Outline link translation | Lossy and fragile |
| NG3 | Syncing attachments/images | Separate concern |
| NG4 | Obsidian-only content (Daily Notes, templates) | No Outline equivalent |
| NG5 | Multi-user conflict resolution | Single-user use case |
| NG6 | A shared/remote git repository | Git is local tooling only |
4. Folder Setup
4.1 Directory Structure
A dedicated folder is created for the Obsidian vault and sync tooling. It is separate from outline-tools/ (which holds export/import scripts).
/home/crabix/docker_authentik/
├── outline-tools/ ← existing: export + import scripts (unchanged)
└── outline-vault/ ← NEW: Obsidian vault + git sync engine
├── .git/ ← local git repo (never pushed to any remote)
├── .gitignore
├── .gitattributes
├── sync.sh ← main sync entrypoint (bash wrapper)
├── outline_sync.py ← core sync logic
├── settings.json ← API token + config (gitignored)
├── _sync_log.md ← human-readable sync log (readable in Obsidian)
├── .obsidian/ ← Obsidian config (gitignored)
├── Bewerbungen/ ← Outline collection → top-level folder
│ ├── CV.md
│ └── Tipico/ ← nested document → subfolder
│ ├── Tipico.md
│ └── Pitch Tipico.md
└── Projekte/
└── ...
Obsidian is pointed at /home/crabix/docker_authentik/outline-vault/ as its vault root.
4.2 Git Initialization
Git is initialized locally with two branches. There is no remote.
cd /home/crabix/docker_authentik/outline-vault
git init
git checkout -b outline # branch: tracks last known Outline state
git checkout -b main # branch: Obsidian edits live here
Branch semantics:
| Branch | Purpose |
|---|---|
outline |
Snapshot of Outline at last successful sync. Committed by sync script only. Never edited by hand. |
main |
Working branch. Obsidian edits land here. Obsidian Git plugin auto-commits. |
The outline branch tip is the common ancestor for all merges. It replaces any custom state file — no sync_state.json needed.
4.3 .gitignore
# Obsidian internals
.obsidian/
# Sync config (contains API token)
settings.json
# Conflict sidecars (resolved manually, not tracked)
*.conflict.md
# OS
.DS_Store
Thumbs.db
4.4 .gitattributes
# Treat all markdown as text, normalize line endings
*.md text eol=lf
# Tell git to always use union merge on the sync log
# (append-only, never conflicts)
_sync_log.md merge=union
4.5 settings.json (not committed)
{
"source": {
"url": "http://outline:3000",
"token": "ol_api_YOUR_TOKEN_HERE"
},
"sync": {
"vault_dir": "/home/crabix/docker_authentik/outline-vault",
"enable_deletions": false,
"auto_push": false
}
}
5. Frontmatter as the ID Layer
Every synced file carries its Outline identity in YAML frontmatter. This is the bridge between git paths and Outline document IDs.
---
outline_id: abc-123
outline_collection_id: col-456
outline_parent_id: def-789 # omitted if root document
outline_updated_at: 2026-03-03T10:30:00Z
---
# CV
Actual document content here...
Rules:
- Frontmatter is stripped before sending content to Outline API
- Frontmatter is written back after each successful API call with updated
outline_updated_at - Frontmatter travels with the file on rename/move — the Outline ID is never lost
- New files created in Obsidian have no frontmatter until first push
- The sync script identifies new files by the absence of
outline_idin frontmatter
6. Architecture: Outline as a "Dumb Remote"
Git handles change detection, merging, and conflict marking. The sync script is purely an API translation layer.
git pull equivalent:
Export Outline → temp dir → convert to frontmatter format
→ git commit to `outline` branch
→ git merge outline into main
git push equivalent:
git diff outline..main → list of changed files
→ for each file: call appropriate Outline API endpoint
→ write updated frontmatter back
→ advance outline branch tip
6.1 Change Detection Matrix
git diff outline..main --name-status --find-renames produces all cases:
| Git status | Outline API call |
|---|---|
M modified |
documents.update(id, title, text) |
A added (with outline_id in frontmatter) |
Already synced — skip |
A added (no outline_id) |
documents.create(collectionId, title, text) → write frontmatter |
D deleted |
documents.delete(id) — only if enable_deletions: true |
R renamed/moved |
documents.update(id, title) + documents.move(id, newParentId) if folder changed |
6.2 New Outline API Calls Required
Existing scripts only use documents.create. Sync additionally requires:
| Endpoint | Used for |
|---|---|
documents.update |
Core push operation — preserves history |
documents.move |
Reparent when file moved between folders |
documents.delete |
Deletion (flag-gated) |
documents.list |
Detect remote changes during pull |
collections.list |
Detect new/deleted collections during pull |
collections.create |
Create collection for new top-level folder |
collections.delete |
Delete collection (flag-gated) |
7. Sync Commands
sync.sh init
First-time setup. Creates vault from current Outline state.
./sync.sh init
Steps:
- Run existing
outline_export_fixed.pyinto temp dir git init+ createoutlineandmainbranches- Convert export metadata headers → YAML frontmatter in each
.mdfile - Commit to
outlinebranch:git commit -m "sync: initial import" - Checkout
main, mergeoutline - Write
.gitignore,.gitattributes
sync.sh pull
Pull latest Outline state into Obsidian.
./sync.sh pull [--auto]
Steps:
- Export Outline → temp dir
- Convert to frontmatter format
- Checkout
outlinebranch, apply changes, commit:git commit -m "sync: outline@TIMESTAMP" - Checkout
main git merge outline- If conflict: write
*.conflict.mdsidecars, append to_sync_log.md, exit non-zero - If clean: append success to
_sync_log.md
sync.sh push
Push local Obsidian changes to Outline.
./sync.sh push [--dry-run]
Steps:
- Check for unresolved merge conflicts — abort if any
git diff outline..main --name-status --find-renames- For each changed file: call Outline API (topological order for new docs)
- Write updated frontmatter,
git addthe file - On full success:
git checkout outline && git merge main - On partial failure: log errors, do not advance
outlinebranch — next push retries failed files
sync.sh (bidirectional)
Pull, then push if pull was clean.
./sync.sh [--dry-run] [--auto]
sync.sh status
Show pending local changes not yet in Outline.
./sync.sh status
Output: git diff outline..main --name-status formatted as human-readable table.
sync.sh resolve FILE --accept [local|remote]
Resolve a merge conflict.
./sync.sh resolve Bewerbungen/CV.md --accept local
./sync.sh resolve Bewerbungen/CV.md --accept remote
8. Conflict Handling
A conflict occurs when git merge outline produces conflict markers (same document edited on both sides since last sync).
Bewerbungen/CV.md ← contains git conflict markers, NOT pushed
Bewerbungen/CV.conflict.md ← human-readable: what changed locally vs remotely
_sync_log.md always reflects current sync state (readable in Obsidian):
## 2026-03-04 08:00
- pull: 3 updated, 1 CONFLICT (Bewerbungen/CV.md) — push blocked
- push: blocked pending conflict resolution
9. Scheduled Sync via Ofelia
[job-exec "outline-pull"]
schedule = @every 1h
container = outline-sync
command = /work/sync.sh pull --auto
# Push is manual by default. Enable only after P6 (conflict detection) is complete.
# [job-exec "outline-push"]
# schedule = @daily
# container = outline-sync
# command = /work/sync.sh push --auto
--auto flag: suppresses interactive prompts, writes to _sync_log.md, exits non-zero on conflict so Ofelia can detect failures.
10. Implementation Phases + User Stories
Phase 1 — sync init
Scope: Create vault from current Outline state. Git initialized. Frontmatter injected.
User Stories
US-1.1 — As a user, I want to run a single command that sets up the vault from scratch, so that I don't need to manually configure git or copy files.
- Given:
outline-vault/does not exist (or is empty) - When:
./sync.sh initis run - Then: The vault folder is created, git is initialized with
outlineandmainbranches, all Outline documents are exported as.mdfiles with frontmatter, andmainis checked out
US-1.2 — As a user, I want every exported document to have frontmatter with its Outline ID, so that future syncs can identify documents without an external map.
- Given: A document exists in Outline with id
abc-123 - When:
sync initcompletes - Then: The corresponding
.mdfile containsoutline_id: abc-123in its YAML frontmatter
US-1.3 — As a user, I want the vault folder structure to mirror Outline's collection/document hierarchy, so that navigation in Obsidian feels natural.
- Given: Outline has collection "Bewerbungen" with nested document "Tipico > Pitch"
- When:
sync initcompletes - Then: Files exist at
Bewerbungen/Tipico/Tipico.mdandBewerbungen/Tipico/Pitch.md
US-1.4 — As a user, I want settings.json to be gitignored automatically, so that my API token is never accidentally committed.
- Given:
sync initruns - Then:
.gitignoreis created containingsettings.jsonand*.conflict.md
Phase 2 — sync pull
Scope: Export Outline → commit to outline branch → merge into main.
User Stories
US-2.1 — As a user, I want to run sync pull to get the latest Outline changes into Obsidian, so that edits made on the web are reflected locally.
- Given: A document was updated in Outline since last sync
- When:
./sync.sh pullis run - Then: The corresponding
.mdfile is updated in the vault with the new content and updatedoutline_updated_atfrontmatter
US-2.2 — As a user, I want new Outline documents to appear as new files after pull, so that I don't miss newly created content.
- Given: A new document was created in Outline since last sync
- When:
./sync.sh pullis run - Then: A new
.mdfile exists with correct frontmatter, in the correct folder
US-2.3 — As a user, I want deleted Outline documents to be removed locally after pull, so that my vault stays in sync with Outline.
- Given: A document was deleted in Outline since last sync
- When:
./sync.sh pullis run - Then: The corresponding local
.mdfile is removed
US-2.4 — As a user, I want to be informed if a pull produces a conflict (same document edited both locally and in Outline), so that I can resolve it manually.
- Given:
CV.mdwas edited locally AND in Outline since last sync - When:
./sync.sh pullis run - Then: A
CV.conflict.mdsidecar is created,_sync_log.mdrecords the conflict, the command exits non-zero, andCV.mdis left untouched
US-2.5 — As a user running pull in --auto mode (cron), I want the sync log to be updated even on failure, so that I can check _sync_log.md in Obsidian to understand what happened.
- Given:
./sync.sh pull --autoruns via Ofelia and encounters a conflict - Then:
_sync_log.mdis updated with timestamp and conflict details, exit code is non-zero
Phase 3 — sync status
Scope: Show pending local changes not yet pushed to Outline.
User Stories
US-3.1 — As a user, I want to see which local files have changed since last sync, so that I know what will be pushed before running push.
- Given:
CV.mdwas edited locally and not yet pushed - When:
./sync.sh statusis run - Then: Output shows
M Bewerbungen/CV.md(modified)
US-3.2 — As a user, I want status to also show new files that will be created in Outline on push, so that I can review before committing.
- Given:
NewNote.mdwas created in Obsidian with no frontmatter - When:
./sync.sh statusis run - Then: Output shows
A Bewerbungen/NewNote.md(new → will create in Outline)
US-3.3 — As a user, I want status to exit cleanly when there are no pending changes, so that cron jobs don't produce false alarms.
- Given: No local changes since last sync
- When:
./sync.sh statusis run - Then: Output is "Nothing to push. Outline is up to date." and exit code is 0
Phase 4 — sync push (modify + rename)
Scope: Push modified and renamed/moved files to Outline. No new doc creation yet.
User Stories
US-4.1 — As a user, I want editing a file in Obsidian and running push to update the document in Outline, so that my local edits appear on the web.
- Given:
CV.mdwas edited in Obsidian (hasoutline_idin frontmatter) - When:
./sync.sh pushis run - Then: Outline document
abc-123is updated with new content, document history is preserved (not recreated),outline_updated_atin frontmatter is updated
US-4.2 — As a user, I want renaming a file in Obsidian to update the document title in Outline, so that both sides stay consistent.
- Given:
CV.mdis renamed toLebenslauf.mdin Obsidian (frontmatteroutline_idintact) - When:
./sync.sh pushis run - Then: Outline document title is updated to "Lebenslauf", document URL is preserved
US-4.3 — As a user, I want moving a file to a different folder in Obsidian to reparent the document in Outline, so that the hierarchy stays consistent.
- Given:
Bewerbungen/CV.mdis moved toProjekte/CV.mdin Obsidian - When:
./sync.sh pushis run - Then: In Outline, the document is moved to the "Projekte" collection with the new parent
US-4.4 — As a user, I want push to be atomic per-document — a failure on one file should not block others.
- Given: Push is run with 5 changed files, one API call fails
- When:
./sync.sh pushcompletes - Then: 4 documents are updated in Outline, 1 failure is logged, the
outlinebranch is NOT advanced (failed file will retry on next push)
US-4.5 — As a user, I want --dry-run to show me exactly what API calls would be made without touching Outline.
- Given: 3 files modified locally
- When:
./sync.sh push --dry-run - Then: Output lists each file and the API call that would be made, no changes to Outline or git
Phase 5 — sync push (new documents)
Scope: Create new Outline documents from new Obsidian files.
User Stories
US-5.1 — As a user, I want creating a new .md file in an existing collection folder to create a new document in Outline on push.
- Given:
Bewerbungen/NewApplication.mdcreated in Obsidian, no frontmatter - When:
./sync.sh pushis run - Then: Document created in Outline under "Bewerbungen" collection, frontmatter written back with new
outline_id
US-5.2 — As a user, I want creating a file in a subfolder to correctly set the parent document in Outline, preserving hierarchy.
- Given:
Bewerbungen/Tipico/NewDoc.mdcreated,Tipico.mdhasoutline_id: def-789 - When:
./sync.sh pushis run - Then: Outline document created with
parentDocumentId: def-789
US-5.3 — As a user, I want creating a new top-level folder to create a new Outline collection on push.
- Given:
NewCollection/FirstDoc.mdcreated in Obsidian - When:
./sync.sh pushis run - Then: New collection "NewCollection" is created in Outline,
FirstDocis created inside it
US-5.4 — As a user, I want parent documents to always be created before their children, even if I created them in reverse order.
- Given:
Bewerbungen/Tipico/NewChild.mdcreated, butBewerbungen/Tipico/Tipico.mdis also new - When:
./sync.sh pushis run - Then:
Tipico.mdis created first, thenNewChild.mdwith correctparentDocumentId
Phase 6 — Conflict Detection
Scope: Block push when unresolved conflicts exist. Generate conflict sidecars.
User Stories
US-6.1 — As a user, I want push to be blocked if there are unresolved merge conflicts, so that I never push broken content.
- Given:
CV.mdcontains git conflict markers from a previous pull - When:
./sync.sh pushis run - Then: Push aborts with "Resolve conflicts before pushing", lists conflicted files, exits non-zero
US-6.2 — As a user, I want a human-readable conflict sidecar file, so that I can understand what changed on each side.
- Given:
CV.mdconflicts during pull - When: Pull completes
- Then:
CV.conflict.mdexists containing: last synced content, local changes, Outline changes, and instructions for resolution
US-6.3 — As a user, I want non-conflicted files to be pushed successfully even when other files have conflicts.
- Given:
CV.mdhas conflict,Projekte/Roadmap.mdis cleanly modified - When:
./sync.sh pushis run (after resolving or acknowledging the conflict situation) - Then: Only
Roadmap.mdis pushed;CV.mdis skipped with a warning
Phase 7 — sync resolve
Scope: Command to resolve conflicts without needing raw git knowledge.
User Stories
US-7.1 — As a user, I want to resolve a conflict by accepting the local version with a single command.
- Given:
CV.mdis in conflict - When:
./sync.sh resolve Bewerbungen/CV.md --accept local - Then: Conflict markers removed, local content kept, file staged,
CV.conflict.mddeleted
US-7.2 — As a user, I want to resolve a conflict by accepting the remote (Outline) version with a single command.
- Given:
CV.mdis in conflict - When:
./sync.sh resolve Bewerbungen/CV.md --accept remote - Then: Conflict markers removed, Outline's content kept, file staged,
CV.conflict.mddeleted
US-7.3 — As a user, I want sync status to clearly show which files are in conflict, so that I know what needs resolving.
- Given:
CV.mdandNotes.mdare conflicted - When:
./sync.sh status - Then: Output shows both files marked as
CONFLICTwith instructions to runsync resolve
Phase 8 — Ofelia + _sync_log.md
Scope: Unattended scheduled pull. Human-readable log in vault.
User Stories
US-8.1 — As a user, I want hourly automatic pulls from Outline so that Obsidian stays current without manual intervention.
- Given: Ofelia runs
sync.sh pull --autoevery hour - When: New documents were created in Outline during the hour
- Then: New
.mdfiles appear in vault,_sync_log.mdrecords the event
US-8.2 — As a user, I want to check _sync_log.md in Obsidian to see the history of syncs, so that I can diagnose issues without accessing a terminal.
- Given: Multiple sync runs have occurred
- When: Opening
_sync_log.mdin Obsidian - Then: A chronological log of pull/push operations, counts of changes, and any errors or conflicts is visible
US-8.3 — As a user, I want failed syncs to be visible in Ofelia's output so that I can be alerted.
- Given: A pull encounters a conflict
- When: Ofelia's job completes
- Then: Exit code is non-zero, Ofelia marks the job as failed
Phase 9 — Deletions (flag-gated)
Scope: Handle deleted files/documents. Off by default.
User Stories
US-9.1 — As a user, I want to opt into deletion sync so that documents I delete locally are also removed from Outline.
- Given:
enable_deletions: trueinsettings.json,OldNote.mddeleted in Obsidian - When:
./sync.sh push - Then: Outline document is deleted, frontmatter entry removed
US-9.2 — As a user, I want deletions to be off by default so that accidental file deletion never cascades to Outline without explicit opt-in.
- Given:
enable_deletions: false(default),OldNote.mddeleted in Obsidian - When:
./sync.sh push - Then: Deletion is logged as a warning, Outline document is untouched
US-9.3 — As a user, I want --dry-run to show pending deletions so that I can review before enabling.
- Given:
enable_deletions: true, 2 files deleted locally - When:
./sync.sh push --dry-run - Then: Output shows 2 documents marked for deletion, no Outline changes made
11. Automated Testing
All tests run inside Docker (same python:3.11-slim + domnet network as existing scripts). A dedicated test collection _sync_test_TIMESTAMP is created in Outline at test start and deleted at teardown.
Test Runner
./sync_tests.sh # run all tests
./sync_tests.sh --phase 1 # run tests for specific phase
./sync_tests.sh --keep # don't delete test collection on failure (debug)
./sync_tests.sh --verbose # show full API request/response
11.1 Phase 1 Tests — Init
TEST-1.1 init creates vault directory if not exists
TEST-1.2 init creates git repo with outline and main branches
TEST-1.3 init exports all Outline collections as top-level folders
TEST-1.4 init injects frontmatter into every .md file
TEST-1.5 frontmatter contains: outline_id, outline_collection_id, outline_updated_at
TEST-1.6 outline_id in frontmatter matches actual Outline document ID (API verified)
TEST-1.7 nested documents are in correct subfolder matching hierarchy
TEST-1.8 settings.json is listed in .gitignore
TEST-1.9 .obsidian/ is listed in .gitignore
TEST-1.10 outline branch and main branch are at same commit after init
TEST-1.11 re-running init on existing vault aborts with clear error message
11.2 Phase 2 Tests — Pull (Outline → Local)
Read direction:
TEST-2.1 pull: modified Outline document updates local .md content
TEST-2.2 pull: modified Outline document updates outline_updated_at in frontmatter
TEST-2.3 pull: outline_id in frontmatter unchanged after content update
TEST-2.4 pull: new Outline document appears as new .md file in correct folder
TEST-2.5 pull: new nested Outline document appears in correct subfolder
TEST-2.6 pull: new Outline collection appears as new top-level folder
TEST-2.7 pull: deleted Outline document removes local .md file
TEST-2.8 pull: deleted Outline collection removes local folder
TEST-2.9 pull: unchanged documents are not modified (mtime unchanged)
TEST-2.10 pull: git commits to outline branch, not main
TEST-2.11 pull: outline branch advances, main branch merges cleanly
TEST-2.12 pull: _sync_log.md updated with counts and timestamp
Conflict detection:
TEST-2.13 pull: same document edited locally + in Outline → conflict detected
TEST-2.14 pull: conflict produces *.conflict.md sidecar file
TEST-2.15 pull: conflict sidecar contains local diff and remote diff sections
TEST-2.16 pull: conflicted file is left untouched (not overwritten)
TEST-2.17 pull: non-conflicted files merge successfully despite other conflicts
TEST-2.18 pull: --auto mode exits non-zero on conflict
TEST-2.19 pull: _sync_log.md records conflict with filename and timestamp
11.3 Phase 3 Tests — Status
TEST-3.1 status: shows M for locally modified file
TEST-3.2 status: shows A for new local file without outline_id
TEST-3.3 status: shows D for locally deleted file (even if deletions off)
TEST-3.4 status: shows R for renamed/moved file
TEST-3.5 status: shows CONFLICT for files with unresolved merge conflicts
TEST-3.6 status: shows nothing when vault is clean
TEST-3.7 status: exit code 0 in all cases (informational only)
TEST-3.8 status: does not modify any files or git state
11.4 Phase 4 Tests — Push Modified + Renamed
Update direction:
TEST-4.1 push: modified local file calls documents.update (not create)
TEST-4.2 push: Outline document content matches local file content after push (strip frontmatter)
TEST-4.3 push: documents.update preserves document history (version count increases)
TEST-4.4 push: outline_updated_at in frontmatter updated to API response timestamp
TEST-4.5 push: outline_id unchanged after update
TEST-4.6 push: Outline document URL unchanged after update (ID preserved)
TEST-4.7 push: renamed file → Outline document title updated
TEST-4.8 push: renamed file → Outline document ID unchanged
TEST-4.9 push: file moved to different folder → documents.move called
TEST-4.10 push: moved file → document appears under new parent in Outline
TEST-4.11 push: moved file → document removed from old parent in Outline
TEST-4.12 push: file moved to different top-level folder → document moved to different collection
TEST-4.13 push: API failure on one file → other files still pushed
TEST-4.14 push: failed file → outline branch NOT advanced
TEST-4.15 push: failed file → retried on next push
TEST-4.16 push: --dry-run → no Outline changes, no git state changes
TEST-4.17 push: --dry-run → lists all API calls that would be made
TEST-4.18 push: frontmatter stripped from content sent to Outline API
TEST-4.19 push: Outline document does NOT contain frontmatter YAML in its body
11.5 Phase 5 Tests — Push New Documents
TEST-5.1 push: new file in existing collection folder → documents.create called
TEST-5.2 push: new document appears in correct Outline collection
TEST-5.3 push: outline_id written back to frontmatter after create
TEST-5.4 push: outline_collection_id written back to frontmatter after create
TEST-5.5 push: new file in subfolder → parentDocumentId set correctly
TEST-5.6 push: parent document outline_id used as parentDocumentId
TEST-5.7 push: new file in new top-level folder → collection created first
TEST-5.8 push: new file in new top-level folder → document created after collection
TEST-5.9 push: two new files parent+child → parent created before child (topological order)
TEST-5.10 push: three-level new hierarchy → correct creation order preserved
TEST-5.11 push: new file with no frontmatter → document still created (title from filename)
TEST-5.12 push: created document is published (not draft)
11.6 Phase 6 Tests — Conflict Detection
TEST-6.1 push blocked when any file contains git conflict markers
TEST-6.2 push blocked lists all conflicted files
TEST-6.3 push blocked exits non-zero
TEST-6.4 non-conflicted files can still be pushed despite other files in conflict
TEST-6.5 conflict sidecar .conflict.md contains "LOCAL" and "REMOTE" sections
TEST-6.6 conflict sidecar contains instructions for resolution commands
TEST-6.7 outline branch not advanced when push is blocked by conflicts
11.7 Phase 7 Tests — Resolve
TEST-7.1 resolve --accept local → file contains local content
TEST-7.2 resolve --accept local → no conflict markers remain
TEST-7.3 resolve --accept local → .conflict.md sidecar deleted
TEST-7.4 resolve --accept local → file is staged (git add)
TEST-7.5 resolve --accept remote → file contains Outline content
TEST-7.6 resolve --accept remote → no conflict markers remain
TEST-7.7 resolve --accept remote → .conflict.md sidecar deleted
TEST-7.8 resolve --accept remote → file is staged (git add)
TEST-7.9 resolve on non-conflicted file → error message, no changes
TEST-7.10 after resolve, push succeeds for that file
11.8 Phase 8 Tests — Ofelia / Auto mode
TEST-8.1 --auto flag: no interactive prompts, completes without stdin
TEST-8.2 --auto flag: _sync_log.md updated on success
TEST-8.3 --auto flag: _sync_log.md updated on conflict
TEST-8.4 --auto flag: exit code 0 on clean pull
TEST-8.5 --auto flag: exit code non-zero on conflict
TEST-8.6 _sync_log.md uses union merge strategy (no conflicts on the log itself)
TEST-8.7 _sync_log.md entries are append-only (history preserved)
11.9 Phase 9 Tests — Deletions
TEST-9.1 enable_deletions false: locally deleted file → no Outline change
TEST-9.2 enable_deletions false: warning logged in _sync_log.md
TEST-9.3 enable_deletions true: locally deleted file → documents.delete called
TEST-9.4 enable_deletions true: Outline document is gone after push
TEST-9.5 enable_deletions true: deleted folder → collection deleted after all docs deleted
TEST-9.6 enable_deletions true: --dry-run lists pending deletions without executing
TEST-9.7 Outline-side deletion during pull: local file removed (regardless of enable_deletions)
TEST-9.8 Outline-side deletion during pull: deletion logged in _sync_log.md
11.10 Full Round-Trip Tests
These run the complete cycle and verify end-to-end consistency.
TEST-RT.1 Create in Outline → pull → verify local file content + frontmatter
TEST-RT.2 Create in Obsidian → push → verify Outline document content + ID in frontmatter
TEST-RT.3 Edit in Outline → pull → edit locally → push → verify Outline has latest content
TEST-RT.4 Edit locally → push → edit in Outline → pull → verify local has Outline content
TEST-RT.5 Edit same doc in Outline AND locally → pull → verify conflict detected → resolve local → push → verify Outline updated
TEST-RT.6 Edit same doc in Outline AND locally → pull → verify conflict detected → resolve remote → push → verify Outline unchanged, local matches Outline
TEST-RT.7 Rename locally → push → pull (clean) → verify no duplicate documents
TEST-RT.8 Move locally → push → pull (clean) → verify hierarchy correct on both sides
TEST-RT.9 Delete locally (deletions on) → push → verify gone from Outline
TEST-RT.10 Delete in Outline → pull → verify gone locally
TEST-RT.11 Create parent+child locally → push → verify parent-child relationship in Outline
TEST-RT.12 Full CRUD cycle: create → edit → rename → move → delete, verify Outline matches at each step
12. What Is NOT Written from Scratch
| Problem | Solution |
|---|---|
| Change detection | git diff |
| Three-way merge | git merge |
| Conflict markers | git native |
| History + rollback | git log / git reset |
| Local auto-commit | Obsidian Git plugin |
| API retry + rate limiting | Reuse OutlineImporter._api_request() from outline_import.py |
| Docker network execution | Reuse pattern from import_to_outline.sh |
| Export logic | Reuse outline_export_fixed.py unchanged |
New code only: outline_sync.py (~500 lines) + sync.sh wrapper (~100 lines).
13. Risks
| Risk | Mitigation |
|---|---|
git merge fails in unattended cron |
Pull conflicts block push only; clean files still merged; logged to _sync_log.md |
| Frontmatter stripped incorrectly before push | Unit test the strip/restore; verify Outline body does not contain YAML |
outline_updated_at clock skew |
Use API response timestamp, not local clock; normalize to UTC |
| New file in unknown folder (not an Outline collection) | Warn and skip; require top-level folder to match existing collection or be brand new |
| Outline deletes document between pull and push | documents.update returns 404 → log as conflict, skip |
| Obsidian Git plugin commits during sync | Sync script checks for clean working tree before starting; aborts if dirty |
outline branch diverges from main after long conflict |
sync status shows divergence; sync pull always catches up |