返回 last30days-skill
save-html-brief.md
根目录 / skills / last30days / references / save-html-brief.md
1 # Save shareable HTML brief
2
3 This reference file is loaded by the main `SKILL.md` when the user asked for an HTML brief (either through an HTML-looking prompt argument like `--emit=html` / `--emit:html` / `--html`, or in natural language - "give me a shareable HTML brief", "give it to me in HTML", "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. Those prompt arguments are user intent signals for the skill; they are not the full Python CLI contract.
4
5 The contract has two modes:
6
7 - **HTML as the requested deliverable** (`--emit=html`, `--emit:html`, `--html`, or prose like "give it to me in HTML"): the HTML artifact is the primary output. Write the synthesis to the temp file, render the HTML, then give a concise artifact handoff in chat instead of pasting the full Markdown report again.
8 - **Normal report plus HTML copy** (the user asks for the normal report and also wants an HTML copy): 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.
9
10 ## When to fire this flow
11
12 - For normal-report-plus-HTML mode: 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.
13 - For HTML-as-deliverable mode: after you have drafted the synthesis that will go into the HTML, before emitting the final chat response.
14 - BEFORE the WAIT FOR USER'S RESPONSE pause.
15 - ONLY if the user asked. Do NOT save HTML when the user didn't ask for it.
16
17 ## How to fire it
18
19 ```bash
20 # 1. Write your synthesis prose VERBATIM to a temp file. The synthesis is the
21 # "What I learned:" prose label, the bold-lead-in paragraphs with their
22 # inline citations, and the "KEY PATTERNS from the research:" numbered list.
23 # Do NOT include the badge or the engine footer in the temp file - the engine
24 # adds those when it renders the HTML.
25 # - HTML-as-deliverable mode: use the exact synthesis draft you prepared for
26 # the artifact. Do not paste it to chat first.
27 # - Normal-report-plus-HTML mode: use the exact synthesis text you already
28 # wrote in chat.
29 # In both modes, do not paraphrase, summarize, or reorder. The HTML must read
30 # identically to the intended report in voice and citations.
31 SYNTHESIS_FILE="/tmp/last30days-synthesis-${CLAUDE_SESSION_ID}.md"
32 # >| not >: fixed path may already exist on a same-session re-run; a plain >
33 # is refused under `set -o noclobber`.
34 cat >| "${SYNTHESIS_FILE}" << 'SYNTHESIS_EOF'
35 What I learned:
36
37 **{First headline}** - {body with [name](url) inline citations}
38
39 **{Second headline}** - {body}
40
41 **{Third headline}** - {body}
42
43 KEY PATTERNS from the research:
44 1. {pattern} - per [@handle](url)
45 2. {pattern} - per [r/sub](url)
46 3. {pattern} - per [@handle](url)
47 SYNTHESIS_EOF
48
49 # 2. Convert the synthesis to a self-contained HTML file via the engine.
50 # REPLAY THE SAME SCOPE FLAGS as your original run (--plan, --hiring-signals,
51 # resolved --x-handle/--subreddits/etc). On a same-topic follow-up, the
52 # engine reuses the structured last-report cache at
53 # ~/.config/last30days/last-report.json to build badge metadata and footer
54 # without re-running source fetchers. That cache is intentionally short-lived
55 # (default: one hour; tune with LAST30DAYS_REPORT_CACHE_TTL_SECONDS, or set
56 # it to 0 to disable reuse). If the cache is stale, missing, or for a
57 # different topic, stderr says "No matching cached report data" and the
58 # engine falls back to a fresh run; the same scope flags keep that fallback
59 # aligned with the synthesis body.
60 SLUG=$(printf '%s\n' "${TOPIC}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//')
61 # Render beside the destination, then create a new name atomically. The hard
62 # link refuses existing paths even when another exporter wins the same name.
63 HTML_PATH=$(
64 HTML_TMP=$(mktemp "${LAST30DAYS_MEMORY_DIR}/.last30days-html.XXXXXXXX") || exit "$?"
65 trap 'rm -f -- "${HTML_TMP}"' EXIT
66 "${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "${TOPIC}" \
67 --emit=html \
68 --synthesis-file "${SYNTHESIS_FILE}" \
69 "${SCOPE_FLAGS[@]}" \
70 >| "${HTML_TMP}" || exit "$?"
71 "${LAST30DAYS_PYTHON}" - "${HTML_TMP}" "${LAST30DAYS_MEMORY_DIR}" "${SLUG}" << 'HTML_PATH_PY'
72 from datetime import date
73 import os
74 from pathlib import Path
75 import sys
76
77 temporary, directory, slug = sys.argv[1:]
78 base = Path(directory) / f"{slug}-brief"
79 dated = f"{base}-{date.today().isoformat()}"
80 candidate = Path(f"{base}.html")
81 suffix = 1
82 while True:
83 try:
84 os.link(temporary, candidate)
85 except FileExistsError:
86 candidate = Path(f"{dated}.html" if suffix == 1 else f"{dated}-{suffix}.html")
87 suffix += 1
88 else:
89 print(candidate)
90 break
91 HTML_PATH_PY
92 ) || exit "$?"
93 # where SCOPE_FLAGS is the same array you passed the first time, e.g.
94 # SCOPE_FLAGS=(--hiring-signals --plan "$QUERY_PLAN_FILE" --x-handle=acme).
95 # For a scoped --hiring-signals brief, --hiring-signals MUST be here too so
96 # the footer reflects the jobs-scoped board, not a generic crawl.
97
98 # 3. Finish with the artifact handoff described below. Do not print the saved
99 # path from the shell block; the chat handoff is the single user-visible
100 # completion message.
101 ```
102
103 ## Optional hosted publishing
104
105 Only publish after the local HTML file has already been saved and the user chooses a publish option. The local HTML save is always first, and its absolute path is always shown before any publish/upload step.
106
107 Respect any existing user, project, or host preference for HTML publishing first. If the user already has a preferred publisher or internal sharing workflow, include that option. If multiple publishing options are available, show each as its own choice and include `ht-ml.app` as one option; label `ht-ml.app` as supporting optional password protection. If no preference exists, use `ht-ml.app` as the fallback publishing option.
108
109 Use this decision flow:
110
111 - Save the local HTML file.
112 - Show the absolute saved path.
113 - Then proactively present next-step choices:
114 1. Open HTML file
115 2. Publish to `<preferred/configured service>`; if `ht-ml.app` is shown, say password protection is available
116 3. Done for now
117 - Do not upload until the user chooses a publishing option.
118
119 When publishing to `ht-ml.app`, ask a second question:
120
121 - **Public link** - publish without a password.
122 - **Password-protected link** - ask the user to type the shared password in free form, then publish with that password.
123
124 Before the `ht-ml.app` choice, tell the user that public pages may be crawled or indexed, and that password protection is available. If the user chooses password protection, use a unique shared password they provide for this report; do not use their own account password.
125
126 Agents should discover the current publishing mechanics for the selected service when needed, including by visiting the service site, rather than hard-coding detailed service-specific instructions in chat. For the built-in `ht-ml.app` path, the engine supports `--publish-html`; on the password-protected branch, pass the shared password through `LAST30DAYS_PUBLISH_PASSWORD` rather than command-line arguments.
127
128 When the user chooses the built-in `ht-ml.app` path, add `--publish-html` to the same `--emit=html` command. Use `--output "$HTML_PATH"` rather than shell redirection so the engine can write the `.publish.json` companion metadata next to the local HTML file. On the password-protected branch, set `LAST30DAYS_PUBLISH_PASSWORD` in the subprocess environment instead of passing `--publish-password` in the shell command.
129
130 ```bash
131 LAST30DAYS_PUBLISH_PASSWORD="${PUBLISH_PASSWORD:-}" \
132 "${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "${TOPIC}" \
133 --emit=html \
134 --synthesis-file "$SYNTHESIS_FILE" \
135 --output "$HTML_PATH" \
136 --publish-html \
137 "${SCOPE_FLAGS[@]}" \
138 >/dev/null
139 ```
140
141 The hosted URL appears on stderr as `[last30days] Published HTML to https://...`. Confirm the result with the hosted URL. If the user chose password protection, also repeat the shared password they selected so they can send the URL and password together. The engine writes URL metadata to `<HTML_PATH>.publish.json`. The provider may return an `update_key`; treat it as secret. The engine deliberately does not write the update key to stdout, the HTML artifact, or `.publish.json` companion metadata.
142
143 ## Chat handoff after saving
144
145 Use the mode that matches the request.
146
147 ### HTML as the requested deliverable
148
149 When HTML is the requested deliverable - whether by `--emit=html`, `--emit:html`, `--html`, or natural-language phrasing - do **not** paste the full Markdown report back into chat after saving the artifact. The user asked for an HTML deliverable; repeating the Markdown makes the run feel like a normal report with an attachment bolted on.
150
151 Respond with a concise handoff that includes the next-step choices:
152
153 ```text
154 🌐 last30days v{VERSION} · synced {YYYY-MM-DD}
155
156 📎 Shareable brief saved to <absolute HTML path>
157
158 What do you want to do next?
159 1. Open HTML file
160 2. Publish to <available HTML publishing service> (<service-specific note, e.g. ht-ml.app supports optional password protection>)
161 3. Done for now
162 ```
163
164 If the user chooses open, open the HTML file when the host can safely open local files, leave the saved-path line in chat, and add `Opened locally.` Let the host choose the correct OS-specific mechanism; do not print a menu of shell commands. If opening fails or the host is headless, do not treat that as a failed report; show the path and say the file is ready to open in a browser.
165
166 ### Normal report plus HTML copy
167
168 When the user asked for a normal `/last30days` report and also asked for an HTML copy, keep the full chat synthesis and append this artifact block after the invitation:
169
170 ```text
171 📎 Shareable brief saved to <absolute HTML path>
172
173 What do you want to do next?
174 1. Open HTML file
175 2. Publish to <available HTML publishing service> (<service-specific note, e.g. ht-ml.app supports optional password protection>)
176 3. Done for now
177 ```
178
179 If the user chooses open, open it when the host can safely open local files; otherwise the saved-path line is enough. Do not upload in this flow unless the user chooses a publishing option.
180
181 ## What ends up in the HTML file
182
183 The engine's `--emit=html` renderer combines:
184
185 - The badge (`🌐 last30days vX.Y.Z · synced YYYY-MM-DD`) at the top
186 - A single inline metadata line (`{date range} · {active sources}`) below the badge
187 - Your synthesis verbatim, with prose labels promoted to `<h2>` and bold lead-ins preserved
188 - All `[name](url)` citations rendered as `<a>` tags
189 - The engine footer (`✅ All agents reported back!` tree) preserved verbatim in monospace
190 - A colophon with the topic and a re-run hint
191
192 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.
193
194 ## Comparison mode
195
196 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).
197
198 ## Follow-up turn
199
200 If the user runs `/last30days OpenClaw` normally, sees the synthesis in chat, and THEN explicitly refers back to that visible synthesis ("save that as HTML", "make this shareable", "turn the above into HTML"), 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`, then use the normal-report-plus-HTML artifact block.
201
202 If the follow-up instead asks for a new HTML deliverable ("give it to me in HTML", `--emit=html`, `--html`) rather than referring back to an already-visible report, treat it as HTML-as-deliverable mode.
203
204 The engine will try to reuse `~/.config/last30days/last-report.json` for that second invocation when it is still within `LAST30DAYS_REPORT_CACHE_TTL_SECONDS` (default: one hour). If stderr says it is reusing cached report data, continue normally. If stderr says no matching cache exists, the cache may be stale; let the command finish only if you supplied the same scope flags as the original run. Otherwise stop and re-run with the original flags so the HTML footer does not describe a different dataset.
205
206 ## What NOT to do
207
208 - Do NOT save HTML if the user didn't ask. The sparse mode (no synthesis) produces a thin file; not useful as a shareable.
209 - Do NOT add content to the temp file beyond your synthesis prose. The badge / footer / colophon come from the engine.
210 - Do NOT change the file path convention. `${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html` is the canonical location.
211 - Do NOT overwrite an existing file. Step 2 renders to a private temporary file and links the completed output to a new name. If `{slug}-brief.html` exists, try `{slug}-brief-YYYY-MM-DD.html`, then `{slug}-brief-YYYY-MM-DD-2.html`, incrementing until a name is available. This also handles concurrent exporters without replacing files, directories, or symlinks. The temporary file is removed on success or failure. If rendering or linking fails, report the error instead of a successful save. Always report the path stored in `HTML_PATH` in the chat handoff.
212 - 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.
213 - Do NOT publish, upload, or send the HTML to a third-party service as part of the local save flow.
214 - Do NOT publish to any service merely because HTML was requested. Show the saved path and next-step choices first; publishing requires the user to choose a publish option.
215 - Do NOT block a local HTML export on a hosting decision unless the user explicitly asked for a hosted URL.
216 - Do NOT paste or store the `update_key` in chat, Markdown, HTML, raw output, or companion metadata.
217
218 ## Edge cases
219
220 - **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.
221 - **Very long synthesis**: no upper bound. The engine handles long markdown bodies. Just paste verbatim.
222 - **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.
223 - **No `${LAST30DAYS_MEMORY_DIR}` set**: defaults to `~/Documents/Last30Days/` per the SKILL.md `Configuration` section.
224
224 lines MARKDOWN