# HTML briefs

> Shareable offline HTML artifacts via --emit=html, --synthesis-file, save paths under LAST30DAYS_MEMORY_DIR, and SKILL.md-driven save flow.

- Repository: mvanhorn/last30days-skill
- GitHub: https://github.com/mvanhorn/last30days-skill
- Human docs: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca
- Complete Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/llms-full.txt

## Source Files

- `skills/last30days/references/save-html-brief.md`
- `skills/last30days/scripts/lib/html_render.py`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`
- `README.md`

---

---
title: "HTML briefs"
description: "Shareable offline HTML artifacts via --emit=html, --synthesis-file, save paths under LAST30DAYS_MEMORY_DIR, and SKILL.md-driven save flow."
---

Shareable HTML briefs are produced by the Python engine’s `--emit=html` mode (`html_render.py` wrapping `render.render_for_html`), with the harness agent writing model synthesis to a temp file via `--synthesis-file` and saving stdout under `LAST30DAYS_MEMORY_DIR`. Chat remains the primary surface; the `.html` file is an optional artifact for Slack, email, or Notion.

## When HTML briefs run

HTML save flow activates only when the user asks for it. `SKILL.md` gates the flow on either:

| Trigger | Examples |
|---|---|
| Explicit flag in `$ARGUMENTS` | `--emit=html`, `--emit:html`, `--html` |
| Natural language | “shareable HTML brief”, “for Slack”, “export as HTML”, “for Notion” |

If neither applies, skip HTML entirely and proceed to the normal wait-for-response step.

<Warning>
Slash commands in Agent Skills hosts do not pass shell pipes or redirects. `/last30days OpenClaw --emit=html | pbcopy` is invalid. The model must translate intent into engine flags and a real shell redirect (or use direct CLI invocation with full `python3 ...` syntax).
</Warning>

## Two save path conventions

The repo uses two related but distinct filenames under `LAST30DAYS_MEMORY_DIR` (default `~/Documents/Last30Days/`, overridable via env or `.claude/last30days.env`):

| Path pattern | Who writes it | Purpose |
|---|---|---|
| `{slug}-brief.html` | Harness agent (`references/save-html-brief.md`) | Canonical shareable brief from skill flow |
| `{slug}-raw-html[-suffix][-YYYY-MM-DD].html` | Engine `save_output()` when `--save-dir` is set | Engine-persisted HTML alongside other emit saves |

Slug rules: lowercase topic with non-alphanumeric runs collapsed to `-` (`slugify()` in `last30days.py`).

Collision handling in `save_output()`: if the target file already exists, append `-YYYY-MM-DD` before the extension. The skill reference documents the same behavior for agents saving briefs.

## End-to-end flow (skill / harness)

```mermaid
sequenceDiagram
  participant User
  participant Agent as Harness agent
  participant Engine as last30days.py
  participant Disk as LAST30DAYS_MEMORY_DIR

  User->>Agent: /last30days topic (HTML intent)
  Agent->>Engine: --emit=compact --save-dir ...
  Engine-->>Agent: compact stdout (evidence + footer)
  Agent-->>User: synthesis in chat (badge, What I learned, KEY PATTERNS, footer, invitation)
  Agent->>Agent: Write synthesis verbatim to temp .md
  Agent->>Engine: --emit=html --synthesis-file temp.md
  Engine-->>Agent: full HTML document on stdout
  Agent->>Disk: redirect stdout to {slug}-brief.html
  Agent-->>User: 📎 Shareable brief saved to path
```

<Steps>
<Step title="Run research as usual">
Invoke the engine with `--emit=compact` and `--save-dir="${LAST30DAYS_MEMORY_DIR}"` (plus plan/targeting flags). Emit the full chat synthesis before any HTML step.
</Step>
<Step title="Write synthesis to a temp file">
Persist only the narrative the user saw: `What I learned:` paragraphs, inline `[name](url)` citations, and `KEY PATTERNS from the research:` list. Exclude the badge, engine footer, invitation block, evidence scratchpad, and debug headers.
</Step>
<Step title="Render HTML">
Re-invoke the engine with the same topic and the same plan/targeting flags as the compact run, plus `--emit=html` and `--synthesis-file`. Redirect stdout to `${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html`.
</Step>
<Step title="Confirm in chat">
Append one line after the invitation: `📎 Shareable brief saved to <absolute or ~ path>`.
</Step>
</Steps>

Canonical shell pattern (from `references/save-html-brief.md`):

```bash
SYNTHESIS_FILE="/tmp/last30days-synthesis-${CLAUDE_SESSION_ID}.md"
# cat synthesis verbatim into SYNTHESIS_FILE (quoted heredoc)

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"
```

<Note>
`SKILL.md` requires reading `references/save-html-brief.md` before the wait-for-response step whenever HTML is requested. Do not improvise paths or synthesis scope from memory.
</Note>

### Follow-up turn without re-research

If the user already received a compact synthesis and later asks to “save as HTML”, reuse the synthesis from conversation history. Write it to the temp file and call `--emit=html --synthesis-file` only—do not restart WebSearch or rewrite the narrative.

## Direct CLI usage

For scripting, cron, or engine testing without the skill loop:

<ParamField body="--emit" type="string" required>
Must be `html`. Other values ignore `--synthesis-file` with a stderr warning.
</ParamField>

<ParamField body="--synthesis-file" type="path">
UTF-8 markdown file embedded as the HTML body. Only read when `--emit=html`. Missing or unreadable files exit with code 2.
</ParamField>

<ParamField body="--save-dir" type="path">
Optional. When set, the engine writes `{slug}-raw-html[-suffix][-date].html` via `save_output()` and still prints HTML to stdout.
</ParamField>

<CodeGroup>
```bash title="Stdout only"
python3 skills/last30days/scripts/last30days.py "OpenClaw" \
  --emit=html \
  --synthesis-file /tmp/synthesis.md
```

```bash title="Engine save + stdout"
python3 skills/last30days/scripts/last30days.py "OpenClaw" \
  --emit=html \
  --synthesis-file /tmp/synthesis.md \
  --save-dir "$HOME/Documents/Last30Days" \
  --save-suffix=v3
```
</CodeGroup>

Without `--synthesis-file`, `render_for_html()` still emits badge, date/source metadata, and the pass-through footer, but the body stays sparse—unsuitable as a shareable brief.

## What the HTML document contains

Rendering pipeline: `render_for_html()` → markdown intermediate → `html_render.render_html()` → single self-contained file.

| Section | Source | Notes |
|---|---|---|
| Badge | Engine `_render_badge()` | `🌐 last30days vX.Y.Z · synced YYYY-MM-DD` as styled pill |
| Metadata line | `<!-- META: range · sources -->` | Promoted to `<div class="meta">` after conversion |
| Synthesis body | `--synthesis-file` content | Verbatim; `What I learned:` / `KEY PATTERNS from the research:` promoted to `<h2>` |
| Citations | Inline markdown links | Converted to `<a href="...">` |
| Engine footer | Pass-through footer markers | Wrapped in `<div class="engine-footer"><pre>…</pre></div>`; tree preserved |
| Colophon | `_build_colophon()` | Generation date, skill version, topic, `/last30days {topic}` rerun hint |

Stripped before share (not in the artifact):

- `# last30days vX.Y.Z: TOPIC` debug file header (`<h1>` suppressed)
- `> Safety note:` blockquote
- `<!-- EVIDENCE FOR SYNTHESIS -->` blocks
- Invitation block (`I'm now an expert… Just ask.`)
- `---` / `# END OF last30days CANONICAL OUTPUT` boundary
- Data quality warnings (degraded run, thin evidence, quota messages)—by design these stay in stderr for the generator, not recipients

Presentation details:

- Inline CSS in `<style>`; no JavaScript (`test_self_containedness`)
- Google Fonts link for Inter and JetBrains Mono (offline-friendly content except font fetch)
- `prefers-color-scheme: light` and `@media print` rules (A4, link URLs in print)

## Comparison mode

For `X vs Y` topics, the engine routes to `render_for_html_comparison()` / `html_render.render_html_comparison()`. The agent uses the same save flow; the synthesis temp file should match the comparison template from chat (`## Quick Verdict`, per-entity sections, `## Head-to-Head`, `## The Bottom Line`, etc.). Saved comparison HTML uses topic slug `entity-a-vs-entity-b` when `--save-dir` persists via `comparison_topic()`.

## Synthesis file contract

| Include | Exclude |
|---|---|
| `What I learned:` bold-lead-in paragraphs with `[name](url)` | Badge line |
| `KEY PATTERNS from the research:` numbered list | `✅ All agents reported back!` footer tree |
| Comparison `##` sections when in comparison mode | Invitation block |
| Unicode and emoji as in chat | Evidence clusters, stats, source coverage |
| | Data quality warning text |

Use a quoted heredoc (`<<'SYNTHESIS_EOF'`) so shell metacharacters in the topic or synthesis do not expand. Match chat text exactly—no paraphrase or reorder.

## Configuration

| Variable / knob | Role |
|---|---|
| `LAST30DAYS_MEMORY_DIR` | Root for `{slug}-brief.html` and `{slug}-raw*.md` / `raw-html` saves; defaults per platform in `CONFIGURATION.md` |
| `--save-dir` | Per-run override passed on engine invocations |
| `--save-suffix` | Inserts `-{suffix}` before extension on engine saves (e.g. per-client `v3`) |

Project-scoped `.claude/last30days.env` can set `LAST30DAYS_MEMORY_DIR` per client without wrapper scripts.

## Troubleshooting

| Symptom | Likely cause | Mitigation |
|---|---|---|
| Thin HTML with no narrative | HTML run without `--synthesis-file` or empty temp file | Ensure synthesis file matches chat output scope |
| `[last30days] Warning: --synthesis-file is only used with --emit=html` | `--synthesis-file` on non-html emit | Add `--emit=html` |
| `Cannot read --synthesis-file` (exit 2) | Bad path or permissions | Fix path; use absolute paths in scripts |
| Warnings missing from file but in stderr | Expected—warnings excluded from artifact | Read stderr when debugging run quality |
| Slash command with pipe fails | Harness does not pass shell mechanics | Use agent redirect or direct CLI |
| Duplicate briefs | Same slug, same day | Engine adds date suffix on `--save-dir` saves; agent should report actual path after redirect |

## Related pages

<CardGroup>
<Card title="Output contract" href="/output-contract">
Badge, LAWs voice, all `--emit` modes, evidence blocks, and footer pass-through rules that define what goes into synthesis vs HTML.
</Card>
<Card title="Skill contract" href="/skill-contract">
SKILL.md HTML trigger section, `save-html-brief.md` reference load, and wait-for-response ordering.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `last30days.py` flags including `--emit` choices and direct CLI vs slash-command constraints.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`LAST30DAYS_MEMORY_DIR`, `--save-dir`, `--save-suffix`, and per-client `.env` patterns.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
Comparison synthesis shape and `render_for_html_comparison` behavior.
</Card>
</CardGroup>
