Files
Trevin Chow b1773be8f3 feat(emit): --emit=html for shareable self-contained briefs (#332)
Adds a one-command shareable HTML mode to /last30days. The skill detects
HTML intent (explicit --emit=html / --emit:html / --html flag in
$ARGUMENTS, or natural-language asks like "give me a shareable brief",
"for Slack", "export as HTML"), runs the normal research + chat synthesis
flow, then saves a self-contained HTML file to
~/Documents/Last30Days/{topic}-brief.html. The synthesis appears in chat
as usual; the HTML is an additional artifact for sharing.

User experience:

  /last30days OpenClaw --emit=html
  /last30days OpenClaw, give me an HTML brief for Slack

Synthesis prints to chat. Last line of the response: "📎 Shareable brief
saved to ~/Documents/Last30Days/openclaw-brief.html". Open it, drag it
into a message, browser-print to PDF, email it.

Architecture:

  - SKILL.md gets a small detection block (triggers + early exit +
    MUST/MUST NOT rules + rationale) that points to a reference file.
  - references/save-html-brief.md owns the implementation: capture the
    synthesis verbatim into a temp file via heredoc, invoke the engine
    with --emit=html --synthesis-file, save to disk, append the
    confirmation line to chat.
  - lib/render.py exposes render_for_html(report, synthesis_md=None) and
    render_for_html_comparison(...) -- clean markdown for HTML
    conversion. Omits debug file header, model-facing safety note, and
    data quality warnings (those stay in engine stderr; recipients can't
    act on them in a shared artifact).
  - lib/html_render.py is a new module: ~200-line CSS template (dark
    mode default, prefers-color-scheme switch, print stylesheet, mobile
    breakpoint), stdlib-regex markdown-to-HTML converter, marker-based
    META + engine-footer wrapping, PROSE_LABELS registry promoting plain
    -text labels to <h2>, colophon builder.
  - last30days.py adds --emit=html argparse choice and --synthesis-file
    PATH flag (engine still callable directly without the skill in the
    loop).

Design:

  - Voice-led research brief, not corporate report. Inter + JetBrains
    Mono via Google Fonts with full system fallbacks (no FOIT, works
    offline). Brand purple #a855f7 (#7c3aed in light mode). Type ramp:
    body 17px/400/muted, bold lead-in 17px/600/fg, h2 + .prose-label
    20px/600/fg, monospace badge/meta/footer/colophon at 13-13.5px.
  - 720px max-width, generous whitespace, no card layouts or shadows.
  - Print stylesheet: light theme, A4 margins, [href]::after URL
    footnotes, page-break-inside:avoid on the engine footer.

Templated (locked) shell:

  - HTML5 boilerplate, Google Fonts <link> with preconnect, all CSS
    inline.
  - .badge / .meta / .engine-footer / .colophon containers.

Flexible (role-based):

  - <h2> rendering covers BOTH plain ## headers (comparison mode per
    LAW 4 exception) AND promoted prose labels via PROSE_LABELS
    registry. Adding a new SKILL.md prose label is a one-line tuple
    addition; no CSS or template changes.
  - Marker-based engine boundaries (<!-- META: ... -->,
    <!-- PASS-THROUGH FOOTER -->) survive the markdown converter and
    get promoted post-conversion. Robust to engine output format
    changes.
  - Generic markdown-to-HTML for body content; future SKILL.md additions
    (new sections, tables, blockquotes) render correctly without code
    changes.

Tests: 30 new tests in tests/test_html_render.py covering snapshots
(rich/thin/comparison), CLI parsing, --synthesis-file end-to-end, prose
label promotion, warning exclusion from artifact, parseability via
html.parser, no-script self-containment.

No SKILL.md voice contract changes, no LAWs 1-8 changes, no new pip
dependencies, no JavaScript anywhere.
2026-05-02 11:30:22 -07:00

5.8 KiB

Save shareable HTML brief

This reference file is loaded by the main SKILL.md when the user asked for an HTML brief (either explicitly via --emit=html / --emit:html / --html, or in natural language - "give me a shareable HTML brief", "for Slack", "for Notion", "export as HTML", etc.). The detection happens in SKILL.md so that the common no-HTML path stays short; the implementation lives here.

The contract: the synthesis still appears in chat as the primary output. The HTML is an additional artifact saved to disk for sharing. Both happen in the same turn.

When to fire this flow

  • After you have already emitted the full chat response: badge, "What I learned:" (or comparison title), bold-lead-in paragraphs with citations, KEY PATTERNS list, engine footer pass-through, invitation block.
  • BEFORE the WAIT FOR USER'S RESPONSE pause.
  • ONLY if the user asked. Do NOT save HTML when the user didn't ask for it.

How to fire it

# 1. Write your synthesis prose VERBATIM to a temp file. The synthesis is the
#    "What I learned:" prose label, the bold-lead-in paragraphs with their
#    inline citations as you wrote them in chat, and the "KEY PATTERNS from
#    the research:" numbered list. Do NOT include the badge or the engine
#    footer in the temp file - the engine adds those when it renders the HTML.
#    Use the EXACT text you just wrote in chat. Do not paraphrase, do not
#    summarize, do not reorder. The HTML must read identically to the chat
#    response in voice and citations.
SYNTHESIS_FILE="/tmp/last30days-synthesis-${CLAUDE_SESSION_ID}.md"
cat > "$SYNTHESIS_FILE" <<'SYNTHESIS_EOF'
What I learned:

**{First headline}** - {body with [name](url) inline citations}

**{Second headline}** - {body}

**{Third headline}** - {body}

KEY PATTERNS from the research:
1. {pattern} - per [@handle](url)
2. {pattern} - per [r/sub](url)
3. {pattern} - per [@handle](url)
SYNTHESIS_EOF

# 2. Convert the synthesis to a self-contained HTML file via the engine.
#    The engine reuses the cache from your earlier engine run (same topic
#    + plan), so this second invocation is typically <1s on cache hit.
SLUG=$(echo "$TOPIC" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//')
HTML_PATH="${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html"
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "${TOPIC}" \
  --emit=html \
  --synthesis-file "$SYNTHESIS_FILE" \
  > "$HTML_PATH"

# 3. Append ONE line to your already-emitted chat response, after the
#    invitation block. Use a paperclip emoji as a visible signal that an
#    artifact was produced:
echo "📎 Shareable brief saved to $HTML_PATH"

What ends up in the HTML file

The engine's --emit=html renderer combines:

  • The badge (🌐 last30days vX.Y.Z · synced YYYY-MM-DD) at the top
  • A single inline metadata line ({date range} · {active sources}) below the badge
  • Your synthesis verbatim, with prose labels promoted to <h2> and bold lead-ins preserved
  • All [name](url) citations rendered as <a> tags
  • The engine footer (✅ All agents reported back! tree) preserved verbatim in monospace
  • A colophon with the topic and a re-run hint

The renderer strips engine-internal noise that doesn't belong in a shareable artifact: the # last30days vX.Y.Z: TOPIC debug file header, the model-facing > Safety note: blockquote, and the I'm now an expert on X invitation block. Data quality warnings (degraded run, thin evidence, etc.) stay in the engine's stderr logs - they never leak into the share-ready file.

Comparison mode

Same flow when the topic is X vs Y (or X vs Y vs Z). The engine routes through render_for_html_comparison internally; you don't need to do anything special. The synthesis temp file should still contain the comparison-shaped synthesis you wrote in chat (## Quick Verdict, ## {Entity} per entity, ## Head-to-Head table, ## The Bottom Line, ## The emerging stack per LAW 4 comparison exception).

Follow-up turn

If the user runs /last30days OpenClaw normally, sees the synthesis in chat, and THEN says "save that as HTML" or "give me a shareable version" in a follow-up turn, do the same save flow on the synthesis you wrote in the previous turn. Do not re-research; the synthesis is already in the conversation history. Just write it to the temp file and call the engine with --emit=html --synthesis-file.

What NOT to do

  • Do NOT save HTML if the user didn't ask. The sparse mode (no synthesis) produces a thin file; not useful as a shareable.
  • Do NOT add content to the temp file beyond your synthesis prose. The badge / footer / colophon come from the engine.
  • Do NOT change the file path convention. ${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html is the canonical location.
  • Do NOT silently overwrite an existing file without telling the user. If $HTML_PATH already exists from a prior run, the engine will pick a date-suffixed name ({slug}-brief-YYYY-MM-DD.html) automatically; just print whichever path the redirect produced.
  • Do NOT include the data quality warning text in the temp file or in your final chat line. Warnings are an engine-stderr concern, not an artifact concern.

Edge cases

  • Topic with shell-special characters (quotes, ampersands): the temp filename uses a slugified version, but the engine receives the raw topic. The cat <<'SYNTHESIS_EOF' quoted heredoc form handles arbitrary content without expansion. Your synthesis text can include any character.
  • Very long synthesis: no upper bound. The engine handles long markdown bodies. Just paste verbatim.
  • Synthesis with images or non-ASCII: emoji and Unicode pass through. Image tags pass through as raw HTML; the renderer doesn't transform them. If you didn't include images in chat, don't add them here.
  • No ${LAST30DAYS_MEMORY_DIR} set: defaults to ~/Documents/Last30Days/ per the SKILL.md Configuration section.