Files
ar9av 5c2b92587c feat(staging): port staged-write promotion into Python
`_staging/` promotion existed only as agent instructions in
.skills/wiki-stage-commit/SKILL.md — `grep -rn "_staging" obsidian_wiki/`
returned nothing. That was fine while an agent was the only actor, but it
meant a UI or a CLI could not promote a staged page without writing a second
copy of the rules, which is exactly what #208 was trying to avoid.

obsidian_wiki/staging.py now owns the parts with one correct answer:

  list_staged(vault)  inventory, classified new / update / patch, with both
                      sides' content revisions pinned
  promote(...)        atomic os.replace to the live path
  discard(...)        move back to _raw/rejected-…, never clobbering an
                      earlier rejection of the same page

Promotion is a rename, so the page that lands is byte-for-byte the one that
was reviewed: arbitrary frontmatter, prose and wikilinks survive untouched.

Judgment stays with the skill. `.patch.md` files are listed but refused by
promote() — merging a human-readable diff into a page whose surrounding text
may have moved is not mechanical.

The revision check is the reason this exists. An agent can write to _staging/
or to the live page while a human is mid-review, so callers pass the hashes
they reviewed and get StagingConflict instead of a silent overwrite.
pin_live_absent covers the case a bare None cannot express: "there was no
live page when I looked."

Also:
- `obsidian-wiki staging list|promote|discard`, exit 9 on conflict so a
  caller can tell "re-read and ask again" from "bad input"
- server._resolve and the /v1/staging listing now use the shared module
  instead of their own copies
- SKILL.md calls the commands rather than restating the steps

Closes #212.
2026-09-10 23:16:10 -07:00
..