OWNED SKILL · PUBLISH-MARKDOWN-TO-CONFLUENCE

Markdown in.
Confluence out.

Confluence doesn't speak markdown — it speaks ADF. So "publishing" rots into hand-repasting and watching the tables break. One command converts and replaces the page body instead.

say "publish this to Confluence"
BEFORE · MARKDOWNrollup.md
# Weekly rollup | Item | Shipped | |----------|------------| | ADF push | 2026-07-15 | | Date pills | 2026-07-16 | | Source | ~/notes/raw.md | See the [spec](<url>) for detail.
md-to-adf.py
→ ADF JSON
AFTER · CONFLUENCE PAGEv+1

Weekly rollup

ITEMSHIPPED
ADF push15 Jul 2026
Date pills16 Jul 2026
✓ --drop-row "Source" — provenance stays local

See the spec for detail.

↓  WHY IT EXISTS

The problem

Your doc lives in markdown. Confluence stores its pages as ADF — Atlassian Document Format, a JSON tree. Nothing bridges those two by hand, so "publish it to the wiki" degenerates into pasting into the editor and watching tables, links and headings come out wrong. Every republish, again.

The turn

One command converts markdown → ADF and replaces the page body. The doc stays the source of truth on disk; Confluence becomes a render target. Point it at a page URL (or a raw page ID), name any rows to strip, push.

What survives the conversion

The receipt

Fetch the current page (and keep it — the preserve step needs it), convert, splice, push:

# 1. getConfluencePage with contentFormat:"adf" → save to /tmp/current-page.json
# 2. convert + preserve
python3 <skill-dir>/md-to-adf.py <source> --drop-row "Source" > /tmp/adf-full-raw.json
python3 <skill-dir>/preserve-extensions.py /tmp/current-page.json /tmp/adf-full-raw.json > /tmp/adf-full.json

# 3. push from disk — never inline the body
python3 <skill-dir>/mcp-http-call.py updateConfluencePage /tmp/update-args.json

The push goes through a direct Atlassian MCP HTTP client that reads its bearer token from the keychain and loads args from a file. Inline tool-call args flow through the model's output tokens — fragile above a few KB (character-level corruption, escaping bugs, hard size limits). The disk path is tested at ≥600KB of args.

The catch: the converter is deliberately narrow. No bullet or numbered lists, no fenced code blocks, no multi-paragraph blockquotes, no nested tables, no **bold**. Hit one of those and you extend md-to-adf.py — you do not hand-edit the ADF output.

The limits

Part of Rascal's AI setup · one atom, justifying its own existence.