assets/code-pane.html
<!--
readout code pane (bundled asset from the readout skill).
Inline this ENTIRE block verbatim immediately before </body>.
It enhances every GitHub blob link (https://github.com/<org>/<repo>/blob/<commit>/<path>#L10-L20)
with an in-document source viewer: click opens the code in a split pane on the right (the
document content is pushed aside, not overlaid), clicking another reference reuses the pane,
and a close button (or Escape) dismisses it. The pane is resizable by dragging its left edge
(min width, persisted; double-click the edge to reset). Code is syntax-highlighted via
highlight.js, lazily loaded from a CDN only when the pane opens; offline it degrades to plain text.
Progressive enhancement: with JS disabled, or on modified clicks, links open GitHub normally.
Source resolution order:
1. snippets embedded at generation time in a script[type="application/json"][data-code-snippets]
block, keyed "org/repo@commit:path" -> {"start": first line number, "text": "lines", "full": bool}
or, for whole files compressed at generation time, {"gz": "base64 gzip", "full": true}
(inflated in the browser via DecompressionStream). Use scripts/embed_snippets.py to generate.
2. runtime fetch of raw.githubusercontent.com (user-initiated; works for public repos)
3. reader-supplied GitHub token: on an HTTP error the pane offers a paste-a-token form and
retries via api.github.com (fine-grained PAT, read-only Contents). The token lives in
sessionStorage for this tab only and is never written into the document.
4. fallback message with an "open on GitHub" link
-->
<style data-code-pane>
:root { --crp-w: min(44rem, 46vw); }
body { transition: margin-right 0.18s ease; }
html.crp-open body { margin-right: var(--crp-w); }
@media (max-width: 900px) {
:root { --crp-w: 100vw; }
html.crp-open body { margin-right: 0; }
}
.crp-pane {
position: fixed; top: 0; right: 0; bottom: 0;
width: var(--crp-w);
display: flex; flex-direction: column;
background: #f5f2eb; color: #2a2622;
border-left: 1px solid #d8d2c6;
z-index: 1000;
font-size: 0.8rem;
}
@media (prefers-color-scheme: dark) {
.crp-pane { background: #211f1c; color: #e8e4dc; border-left-color: #3a3733; }
}
.crp-head {
display: flex; align-items: center; gap: 0.7rem;
padding: 0.6rem 1rem;
border-bottom: 1px solid rgba(128, 118, 100, 0.3);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.76rem;
}
.crp-title { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: rtl; text-align: left; }
.crp-lines { flex: none; opacity: 0.6; }
.crp-gh { flex: none; color: inherit; opacity: 0.75; text-decoration: underline; white-space: nowrap; }
.crp-close {
flex: none; width: 1.7rem; height: 1.7rem;
border: 1px solid rgba(128, 118, 100, 0.4); border-radius: 6px;
background: transparent; color: inherit;
font-size: 1rem; line-height: 1; cursor: pointer;
}
.crp-close:hover { background: rgba(128, 118, 100, 0.15); }
.crp-resize {
position: absolute; left: -3px; top: 0; bottom: 0; width: 7px;
cursor: col-resize; z-index: 1001;
}
.crp-resize:hover, html.crp-resizing .crp-resize { background: rgba(128, 118, 100, 0.3); }
html.crp-resizing body { transition: none; }
html.crp-resizing, html.crp-resizing * { user-select: none; cursor: col-resize !important; }
.crp-body {
flex: 1 1 auto; overflow: auto;
padding: 0.5rem 0 2rem;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
line-height: 1.5; tab-size: 4;
}
.crp-line { display: flex; }
.crp-ln { flex: none; width: 3.4rem; padding-right: 0.9rem; text-align: right; opacity: 0.45; user-select: none; }
.crp-code { flex: 1 1 auto; white-space: pre; padding-right: 1rem; }
.crp-line.crp-hit { background: rgba(163, 148, 84, 0.22); }
.crp-msg { padding: 1.1rem 1.2rem; font-family: inherit; }
.crp-msg a { color: inherit; }
.crp-note { padding: 0.5rem 1.2rem 0.7rem; opacity: 0.65; font-size: 0.72rem; font-family: inherit; }
.crp-token { display: flex; gap: 0.5rem; margin: 0.8rem 0 0.4rem; }
.crp-token input {
flex: 1 1 auto; min-width: 0;
font: inherit; color: inherit;
background: rgba(128, 118, 100, 0.08);
border: 1px solid rgba(128, 118, 100, 0.4); border-radius: 6px;
padding: 0.35rem 0.6rem;
}
.crp-token button {
flex: none;
font: inherit; color: inherit; cursor: pointer;
background: rgba(128, 118, 100, 0.15);
border: 1px solid rgba(128, 118, 100, 0.4); border-radius: 6px;
padding: 0.35rem 0.8rem;
}
.crp-token button:hover { background: rgba(128, 118, 100, 0.25); }
.crp-token-note { opacity: 0.65; font-size: 0.72rem; margin: 0.4rem 0 0.8rem; }
/* muted-primaries highlight.js theme on warm paper, scoped to the pane (light + dark) */
.crp-pane .hljs-comment, .crp-pane .hljs-doctag, .crp-pane .hljs-quote { color: #91887a; font-style: italic; }
.crp-pane .hljs-string, .crp-pane .hljs-regexp, .crp-pane .hljs-addition { color: #3d6247; }
.crp-pane .hljs-keyword, .crp-pane .hljs-literal, .crp-pane .hljs-selector-tag, .crp-pane .hljs-section { color: #31567d; }
.crp-pane .hljs-number, .crp-pane .hljs-symbol, .crp-pane .hljs-bullet { color: #7d6a2e; }
.crp-pane .hljs-title, .crp-pane .hljs-name { color: #794046; }
.crp-pane .hljs-type, .crp-pane .hljs-built_in, .crp-pane .hljs-selector-class, .crp-pane .hljs-selector-id { color: #6e5540; }
.crp-pane .hljs-attr, .crp-pane .hljs-property, .crp-pane .hljs-variable, .crp-pane .hljs-template-variable, .crp-pane .hljs-attribute { color: #5a6b3f; }
.crp-pane .hljs-meta, .crp-pane .hljs-params, .crp-pane .hljs-operator, .crp-pane .hljs-punctuation { color: #6d675c; }
.crp-pane .hljs-deletion { color: #8e3b3b; }
.crp-pane .hljs-emphasis { font-style: italic; }
.crp-pane .hljs-strong { font-weight: 600; }
@media (prefers-color-scheme: dark) {
.crp-pane .hljs-comment, .crp-pane .hljs-doctag, .crp-pane .hljs-quote { color: #837a6b; }
.crp-pane .hljs-string, .crp-pane .hljs-regexp, .crp-pane .hljs-addition { color: #9ab894; }
.crp-pane .hljs-keyword, .crp-pane .hljs-literal, .crp-pane .hljs-selector-tag, .crp-pane .hljs-section { color: #8caec9; }
.crp-pane .hljs-number, .crp-pane .hljs-symbol, .crp-pane .hljs-bullet { color: #c2ae76; }
.crp-pane .hljs-title, .crp-pane .hljs-name { color: #c99a94; }
.crp-pane .hljs-type, .crp-pane .hljs-built_in, .crp-pane .hljs-selector-class, .crp-pane .hljs-selector-id { color: #b39a7e; }
.crp-pane .hljs-attr, .crp-pane .hljs-property, .crp-pane .hljs-variable, .crp-pane .hljs-template-variable, .crp-pane .hljs-attribute { color: #a9b98a; }
.crp-pane .hljs-meta, .crp-pane .hljs-params, .crp-pane .hljs-operator, .crp-pane .hljs-punctuation { color: #9a9284; }
.crp-pane .hljs-deletion { color: #c98f8f; }
}
</style>
<script data-code-pane>
(function () {
"use strict";
var BLOB = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/([^#?]+)(?:#L(\d+)(?:-L(\d+))?)?$/;
var TOKEN_KEY = "crp-gh-token";
var WIDTH_KEY = "crp-w-px";
var MIN_W = 320; // px — minimum pane width when dragging
var HLJS_CDN = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js";
var EXT_LANG = {
go: "go", rs: "rust", py: "python", rb: "ruby", sql: "sql", lua: "lua", pl: "perl",
js: "javascript", mjs: "javascript", cjs: "javascript", jsx: "javascript",
ts: "typescript", tsx: "typescript",
sh: "bash", bash: "bash", zsh: "bash",
c: "c", h: "c", cpp: "cpp", cc: "cpp", cxx: "cpp", hpp: "cpp", hh: "cpp",
java: "java", kt: "kotlin", kts: "kotlin", swift: "swift", cs: "csharp",
m: "objectivec", mm: "objectivec", php: "php", r: "r",
yaml: "yaml", yml: "yaml", toml: "ini", ini: "ini", conf: "ini",
json: "json", jsonc: "json", xml: "xml", html: "xml", htm: "xml", svg: "xml",
css: "css", scss: "scss", less: "less", md: "markdown", markdown: "markdown",
makefile: "makefile", diff: "diff", patch: "diff"
};
var snippets = {};
var snipEl = document.querySelector('script[type="application/json"][data-code-snippets]');
if (snipEl) { try { snippets = JSON.parse(snipEl.textContent) || {}; } catch (e) { snippets = {}; } }
var cache = Object.create(null); // rawUrl -> file text
var reqId = 0;
var memToken = "";
var hljsPromise = null;
var pane = null, titleEl, linesEl, ghEl, bodyEl;
function getToken() {
if (memToken) return memToken;
try { return window.sessionStorage.getItem(TOKEN_KEY) || ""; } catch (e) { return ""; }
}
function setToken(t) {
memToken = t;
try { window.sessionStorage.setItem(TOKEN_KEY, t); } catch (e) {}
}
function storedWidth() {
try { return parseInt(window.localStorage.getItem(WIDTH_KEY), 10) || 0; } catch (e) { return 0; }
}
function storeWidth(px) {
try { window.localStorage.setItem(WIDTH_KEY, String(px)); } catch (e) {}
}
function clampWidth(px) {
var max = Math.max(MIN_W, window.innerWidth - 280); // keep some document visible
return Math.min(Math.max(px, MIN_W), max);
}
function applyWidth(px) {
document.documentElement.style.setProperty("--crp-w", px + "px");
}
function resetWidth() {
try { window.localStorage.removeItem(WIDTH_KEY); } catch (e) {}
document.documentElement.style.removeProperty("--crp-w");
}
// Applies the remembered width, and defers to the stylesheet (full-width pane) on
// narrow screens — an inline --crp-w would otherwise override the media query.
function syncWidth() {
if (window.innerWidth <= 900) { document.documentElement.style.removeProperty("--crp-w"); return; }
var w = storedWidth();
if (w) applyWidth(clampWidth(w));
}
function langFor(path) {
var base = (path || "").split("/").pop().toLowerCase();
if (base === "makefile") return "makefile";
if (base === "dockerfile") return "dockerfile";
var m = /\.([a-z0-9_]+)$/.exec(base);
return m ? EXT_LANG[m[1]] || null : null;
}
function loadHljs() {
if (window.hljs) return Promise.resolve(window.hljs);
if (hljsPromise) return hljsPromise;
hljsPromise = new Promise(function (resolve) {
var s = document.createElement("script");
s.src = HLJS_CDN;
s.onload = function () { resolve(window.hljs || null); };
s.onerror = function () { resolve(null); };
document.head.appendChild(s);
});
return hljsPromise;
}
// Split highlight.js output HTML into per-line HTML, re-opening spans that cross newlines.
function splitHighlighted(html) {
var lines = html.split("\n");
var open = [];
var out = [];
var tagRe = /<span[^>]*>|<\/span>/g;
for (var i = 0; i < lines.length; i++) {
var prefix = open.join("");
var line = lines[i];
var m;
tagRe.lastIndex = 0;
while ((m = tagRe.exec(line))) {
if (m[0] === "</span>") open.pop(); else open.push(m[0]);
}
var suffix = "";
for (var k = 0; k < open.length; k++) suffix += "</span>";
out.push(prefix + line + suffix);
}
return out;
}
function inflate(b64) {
return new Promise(function (resolve, reject) {
if (typeof DecompressionStream === "undefined") { reject(new Error("DecompressionStream unavailable")); return; }
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
var stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
new Response(stream).text().then(resolve, reject);
});
}
// Resolves an embedded snippet entry to {text, start, full} (inflating "gz" once), or null.
function resolveSnippet(key) {
var snip = snippets[key];
if (!snip) return Promise.resolve(null);
if (typeof snip.text === "string") {
return Promise.resolve({ text: snip.text, start: snip.start || 1, full: !!snip.full });
}
if (typeof snip.gz === "string") {
return inflate(snip.gz).then(function (text) {
snippets[key] = { text: text, start: snip.start || 1, full: !!snip.full };
return snippets[key];
}, function () { return null; });
}
return Promise.resolve(null);
}
function ensurePane() {
if (pane) return;
pane = document.createElement("aside");
pane.className = "crp-pane";
pane.setAttribute("role", "complementary");
pane.setAttribute("aria-label", "Source viewer");
pane.tabIndex = -1;
var head = document.createElement("div");
head.className = "crp-head";
titleEl = document.createElement("span"); titleEl.className = "crp-title";
linesEl = document.createElement("span"); linesEl.className = "crp-lines";
ghEl = document.createElement("a"); ghEl.className = "crp-gh"; ghEl.textContent = "open on GitHub";
ghEl.target = "_blank"; ghEl.rel = "noopener";
var close = document.createElement("button");
close.className = "crp-close"; close.type = "button";
close.setAttribute("aria-label", "Close source viewer");
close.textContent = "\u00d7";
close.addEventListener("click", closePane);
head.appendChild(titleEl); head.appendChild(linesEl); head.appendChild(ghEl); head.appendChild(close);
bodyEl = document.createElement("div");
bodyEl.className = "crp-body";
var grip = document.createElement("div");
grip.className = "crp-resize";
grip.setAttribute("aria-hidden", "true");
grip.title = "Drag to resize · double-click to reset";
grip.addEventListener("pointerdown", function (ev) {
if (ev.button !== 0) return;
ev.preventDefault();
grip.setPointerCapture(ev.pointerId);
document.documentElement.classList.add("crp-resizing");
function move(e) {
applyWidth(clampWidth(Math.round(window.innerWidth - e.clientX)));
}
function up(e) {
document.documentElement.classList.remove("crp-resizing");
storeWidth(clampWidth(Math.round(window.innerWidth - e.clientX)));
grip.removeEventListener("pointermove", move);
grip.removeEventListener("pointerup", up);
grip.removeEventListener("pointercancel", up);
}
grip.addEventListener("pointermove", move);
grip.addEventListener("pointerup", up);
grip.addEventListener("pointercancel", up);
});
grip.addEventListener("dblclick", resetWidth);
pane.appendChild(grip);
pane.appendChild(head);
pane.appendChild(bodyEl);
document.body.appendChild(pane);
document.documentElement.classList.add("crp-open");
syncWidth();
}
function closePane() {
if (pane && pane.parentNode) pane.parentNode.removeChild(pane);
pane = null;
document.documentElement.classList.remove("crp-open");
}
function showMessage(text, href) {
bodyEl.textContent = "";
var div = document.createElement("div");
div.className = "crp-msg";
var p = document.createElement("p");
p.textContent = text;
div.appendChild(p);
if (href) {
var a = document.createElement("a");
a.href = href; a.target = "_blank"; a.rel = "noopener";
a.textContent = "Open on GitHub instead";
div.appendChild(a);
}
bodyEl.appendChild(div);
}
function showTokenForm(ref, notice) {
bodyEl.textContent = "";
var div = document.createElement("div");
div.className = "crp-msg";
var p = document.createElement("p");
p.textContent = notice;
div.appendChild(p);
var form = document.createElement("form");
form.className = "crp-token";
var input = document.createElement("input");
input.type = "password";
input.placeholder = "github_pat_\u2026";
input.setAttribute("aria-label", "GitHub token");
input.autocomplete = "off";
var btn = document.createElement("button");
btn.type = "submit";
btn.textContent = "View with token";
form.appendChild(input); form.appendChild(btn);
form.addEventListener("submit", function (ev) {
ev.preventDefault();
var t = input.value.trim();
if (!t) return;
setToken(t);
show(ref);
});
div.appendChild(form);
var note = document.createElement("p");
note.className = "crp-token-note";
note.textContent = "Use a fine-grained personal access token with read-only Contents permission, scoped to this repo. It is kept in sessionStorage for this tab only and never written into the document.";
div.appendChild(note);
var a = document.createElement("a");
a.href = ref.href; a.target = "_blank"; a.rel = "noopener";
a.textContent = "Open on GitHub instead";
div.appendChild(a);
bodyEl.appendChild(div);
input.focus();
}
function renderLines(text, startNum, hitFrom, hitTo, note, path, live) {
bodyEl.textContent = "";
if (note) {
var n = document.createElement("div");
n.className = "crp-note";
n.textContent = note;
bodyEl.appendChild(n);
}
var lines = text.split("\n");
if (lines.length && lines[lines.length - 1] === "") lines.pop();
var frag = document.createDocumentFragment();
var target = null;
var codeEls = [];
for (var i = 0; i < lines.length; i++) {
var num = startNum + i;
var row = document.createElement("div");
row.className = "crp-line" + (hitFrom && num >= hitFrom && num <= hitTo ? " crp-hit" : "");
if (!target && hitFrom && num >= hitFrom) target = row;
var ln = document.createElement("span"); ln.className = "crp-ln"; ln.textContent = String(num);
var code = document.createElement("span"); code.className = "crp-code"; code.textContent = lines[i];
codeEls.push(code);
row.appendChild(ln); row.appendChild(code);
frag.appendChild(row);
}
bodyEl.appendChild(frag);
if (target) {
var y = target.offsetTop - bodyEl.clientHeight * 0.33;
bodyEl.scrollTop = y > 0 ? y : 0;
} else {
bodyEl.scrollTop = 0;
}
// Upgrade to syntax-highlighted lines in place once highlight.js is available.
var lang = langFor(path);
if (!lang) return;
loadHljs().then(function (hljs) {
if (!hljs || !live()) return;
if (!hljs.getLanguage(lang)) return;
var highlighted;
try { highlighted = hljs.highlight(text, { language: lang, ignoreIllegals: true }).value; }
catch (e) { return; }
var htmlLines = splitHighlighted(highlighted);
for (var i = 0; i < codeEls.length && i < htmlLines.length; i++) {
codeEls[i].innerHTML = htmlLines[i];
}
});
}
function apiFetch(ref, token) {
var url = "https://api.github.com/repos/" + ref.org + "/" + ref.repo + "/contents/" +
encodeURIComponent(ref.path).replace(/%2F/gi, "/") + "?ref=" + encodeURIComponent(ref.commit);
return fetch(url, {
headers: { "Accept": "application/vnd.github.raw+json", "Authorization": "Bearer " + token }
}).then(function (res) {
if (!res.ok) { var e = new Error("HTTP " + res.status); e.httpStatus = res.status; throw e; }
return res.text();
});
}
function show(ref) {
ensurePane();
var my = ++reqId;
titleEl.textContent = "\u200e" + ref.path;
titleEl.title = ref.org + "/" + ref.repo + "@" + ref.commit + ":" + ref.path;
linesEl.textContent = ref.from ? (ref.to && ref.to !== ref.from ? "L" + ref.from + "\u2013L" + ref.to : "L" + ref.from) : "";
ghEl.href = ref.href;
var hitFrom = ref.from || 0;
var hitTo = ref.to || ref.from || 0;
var key = ref.org + "/" + ref.repo + "@" + ref.commit + ":" + ref.path;
var rawUrl = "https://raw.githubusercontent.com/" + ref.org + "/" + ref.repo + "/" + ref.commit + "/" + ref.path;
var live = function () { return my === reqId && !!pane; };
var resolvedSnip = null;
function done(text, startNum, note) {
if (!live()) return;
renderLines(text, startNum, hitFrom, hitTo, note, ref.path, live);
}
function snippetCovers(s) {
if (!s) return false;
if (!ref.from || s.full) return true;
var end = s.start + s.text.split("\n").length - 1;
return ref.from >= s.start && hitTo <= end;
}
function fallbackAfterHttp(status) {
if (!live()) return;
if (resolvedSnip) {
done(resolvedSnip.text, resolvedSnip.start, "Excerpt embedded at generation time \u2014 couldn't fetch the full file (HTTP " + status + ").");
} else {
showTokenForm(ref, "GitHub returned HTTP " + status + " for this file \u2014 it may be private (paste a token to view it here), or the file/commit may not exist.");
}
}
function fallbackOffline() {
if (!live()) return;
if (resolvedSnip) {
done(resolvedSnip.text, resolvedSnip.start, "Excerpt embedded at generation time \u2014 couldn't reach GitHub.");
} else {
showMessage("Couldn't reach GitHub \u2014 you may be offline.", ref.href);
}
}
function fetchChain() {
if (typeof cache[rawUrl] === "string") { done(cache[rawUrl], 1, null); return; }
showMessage("Loading " + ref.path + " @ " + ref.commit.slice(0, 10) + "\u2026", null);
fetch(rawUrl).then(function (res) {
if (!res.ok) { var e = new Error("HTTP " + res.status); e.httpStatus = res.status; throw e; }
return res.text();
}).then(function (text) {
cache[rawUrl] = text;
done(text, 1, null);
}).catch(function (err) {
if (!live()) return;
if (!(err && err.httpStatus)) { fallbackOffline(); return; }
var token = getToken();
if (!token) { fallbackAfterHttp(err.httpStatus); return; }
apiFetch(ref, token).then(function (text) {
cache[rawUrl] = text;
done(text, 1, null);
}).catch(function (err2) {
if (!live()) return;
if (err2 && (err2.httpStatus === 401 || err2.httpStatus === 403)) {
showTokenForm(ref, "GitHub rejected the saved token (HTTP " + err2.httpStatus + "). Paste a token with read access to " + ref.org + "/" + ref.repo + ":");
} else if (err2 && err2.httpStatus) {
fallbackAfterHttp(err2.httpStatus);
} else {
fallbackOffline();
}
});
});
}
resolveSnippet(key).then(function (s) {
if (!live()) return;
resolvedSnip = s;
if (snippetCovers(s)) {
done(s.text, s.start, s.full ? null : "Excerpt embedded at generation time \u2014 full file on GitHub.");
return;
}
fetchChain();
});
pane.focus();
}
document.addEventListener("click", function (ev) {
if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
var el = ev.target && ev.target.closest ? ev.target.closest("a[href]") : null;
if (!el) return;
if (pane && pane.contains(el)) return; // pane-internal links always go to GitHub
var m = BLOB.exec(el.href);
if (!m) return;
ev.preventDefault();
show({
org: m[1], repo: m[2], commit: m[3], path: m[4],
from: m[5] ? parseInt(m[5], 10) : null,
to: m[6] ? parseInt(m[6], 10) : null,
href: el.href
});
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && pane) {
var inForm = document.activeElement && pane.contains(document.activeElement) && document.activeElement.tagName === "INPUT";
if (!inForm) closePane();
}
});
window.addEventListener("resize", function () {
if (pane) syncWidth();
});
if (typeof window !== "undefined" && window.__crpTest) {
window.__crpTest({ splitHighlighted: splitHighlighted, langFor: langFor, clampWidth: clampWidth });
}
})();
</script>
assets/template.html
<!DOCTYPE html>
<!--
Canonical readout template (bundled asset from the readout skill).
Start every readout from this file and fill the slots (marked with comments).
The style[data-readout] and script[data-readout] blocks are the shared chrome —
copy them VERBATIM and never edit them; every readout must look like every other.
Document-specific CSS (diagram sizing, one-off content elements) goes ONLY in the
style[data-doc] block and must not override the chrome's tokens or selectors.
(Tag names are written selector-style in this comment on purpose: scripts that
extract the chrome by searching for the literal opening tags must never match
inside this comment.)
Header anatomy (fixed): .kicker (REPO · CONTEXT · READOUT TYPE, mono uppercase),
h1 title, .subtitle (one-line framing), .meta (date · audience · org/repo@commit pin).
Sections: <section id="..."> with <h2>, one TOC <li> per section (class="sub" for nested).
When the document contains GitHub blob links, inline assets/code-pane.html verbatim
where indicated before </body>.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><!-- TITLE --></title>
<meta name="description" content="<!-- ONE-SENTENCE SUMMARY (the readouts index shows this) -->">
<style data-readout>
:root {
--bg: #faf8f4; --fg: #262220; --muted: #857c6e;
--accent: #31567d; --warn: #7d6a2e;
--hairline: #e3ddd1; --code-bg: #f0ece3;
--tint: #f3efe7; --warn-tint: #f2eedd;
--good: #3d6247; --bad: #8e3b3b;
--sidebar-w: 15rem;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1e1c19; --fg: #e8e4dc; --muted: #948b7c;
--accent: #8caec9; --warn: #c2ae76;
--hairline: #383530; --code-bg: #2a2723;
--tint: #262320; --warn-tint: #2c2a20;
--good: #9ab894; --bad: #c98f8f;
}
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0; background: var(--bg); color: var(--fg);
font-family: Charter, 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
font-size: 17px; line-height: 1.65;
}
code, pre {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.82em;
}
code { background: var(--code-bg); border-radius: 4px; padding: 0.1em 0.35em; }
pre {
background: var(--code-bg); border: 1px solid var(--hairline); border-radius: 8px;
padding: 0.8rem 1rem; overflow-x: auto; line-height: 1.5;
}
pre code { background: none; border: none; padding: 0; font-size: 1em; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.layout {
display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
max-width: 68rem; margin: 0 auto; gap: 2.5rem; padding: 0 1.5rem;
}
nav.toc {
position: sticky; top: 0; align-self: start; height: 100vh; overflow-y: auto;
padding: 2.75rem 0 2rem; font-size: 0.85rem;
}
nav.toc .toc-title {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
font-size: 0.68rem; color: var(--muted); margin-bottom: 0.7rem;
}
nav.toc ol { list-style: none; margin: 0; padding: 0; }
nav.toc li { margin: 0; }
nav.toc a {
display: block; color: var(--muted);
padding: 0.28rem 0.75rem; border-left: 2px solid var(--hairline);
}
nav.toc a.sub { padding-left: 1.6rem; font-size: 0.8rem; }
nav.toc a:hover { color: var(--fg); text-decoration: none; }
nav.toc a.active { color: var(--accent); border-left-color: var(--accent); font-weight: 600; }
main { padding: 2.75rem 0 4rem; max-width: 72ch; }
header.doc-header { margin-bottom: 2.2rem; }
.kicker {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
text-transform: uppercase; letter-spacing: 0.08em;
font-size: 0.7rem; color: var(--accent); margin: 0 0 0.7rem;
}
h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 0.5rem; font-weight: 700; }
.subtitle { color: var(--muted); font-size: 1rem; margin: 0 0 0.5rem; }
.meta {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.72rem; color: var(--muted); margin: 0;
}
h2 { font-size: 1.3rem; margin: 2.6rem 0 0.9rem; padding-top: 0.8rem; border-top: 1px solid var(--hairline); }
h3 { font-size: 1.05rem; margin: 1.6rem 0 0.5rem; }
p { margin: 0.8rem 0; }
ul, ol { padding-left: 1.4rem; }
li { margin: 0.35rem 0; }
.badge {
display: inline-block;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.66rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
padding: 0.12rem 0.5rem; border-radius: 999px; border: 1px solid;
vertical-align: middle; white-space: nowrap;
}
.badge.good { color: var(--good); border-color: var(--good); }
.badge.bad { color: var(--bad); border-color: var(--bad); }
.badge.neutral { color: var(--muted); border-color: var(--hairline); }
.callout {
background: var(--tint); border: 1px solid var(--hairline); border-radius: 8px;
padding: 0.85rem 1.1rem; margin: 1.3rem 0;
}
.callout p { margin: 0.3rem 0; }
.callout .callout-label {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-weight: 700; font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--muted); display: block; margin-bottom: 0.3rem;
}
.callout.warn { background: var(--warn-tint); }
.callout.warn .callout-label { color: var(--warn); }
details { margin: 1.2rem 0; }
details summary { cursor: pointer; color: var(--muted); }
details[open] summary { margin-bottom: 0.6rem; }
.table-wrap { overflow-x: auto; margin: 1.2rem 0; }
table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
th, td { border: 1px solid var(--hairline); padding: 0.5rem 0.75rem; text-align: left; vertical-align: top; }
th { background: var(--code-bg); font-weight: 600; }
td .delta { font-weight: 600; color: var(--good); white-space: nowrap; }
figure { margin: 1.6rem 0; }
figure svg { width: 100%; height: auto; display: block; color: var(--fg); }
figcaption { font-size: 0.8rem; color: var(--muted); text-align: center; margin-top: 0.5rem; }
footer.provenance {
margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--hairline);
font-size: 0.8rem; color: var(--muted);
}
footer.provenance p { margin: 0.3rem 0; }
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; gap: 0; }
nav.toc {
position: static; height: auto;
padding: 1.5rem 0 0; border-bottom: 1px solid var(--hairline);
}
nav.toc a { border-left: none; padding-left: 0; }
main { padding-top: 1.5rem; }
}
</style>
<style data-doc>
/* Document-specific styles ONLY (e.g. a diagram's dimensions).
Never restyle the chrome above. */
</style>
</head>
<body>
<div class="layout">
<nav class="toc" aria-label="Table of contents">
<div class="toc-title">Contents</div>
<ol>
<li><a href="#summary">Executive summary</a></li>
<!-- one <li><a href="#section-id">…</a></li> per section; class="sub" for nested entries -->
</ol>
</nav>
<main>
<header class="doc-header">
<p class="kicker"><!-- REPO · CONTEXT · READOUT TYPE (e.g. WARP-SERVER · RESEARCH READOUT) --></p>
<h1><!-- TITLE --></h1>
<p class="subtitle"><!-- One line framing what this document covers and who it's for. --></p>
<p class="meta"><!-- DATE · audience · code references pinned to <code>org/repo@commit</code> --></p>
</header>
<section id="summary">
<h2>Executive summary</h2>
<!-- A few sentences a reader can stop after. -->
</section>
<!-- <section id="..."> blocks; <h2> per section, <h3> for subsections.
Use .callout / .badge / .table-wrap / figure per the doc guide. -->
<footer class="provenance">
<p><strong>Provenance.</strong> <!-- date, source conversation/investigation, repos @ commits, verification caveats --></p>
</footer>
</main>
</div>
<script data-readout>
(function () {
var links = Array.prototype.slice.call(document.querySelectorAll("nav.toc a"));
if (!links.length || !("IntersectionObserver" in window)) return;
var map = {};
links.forEach(function (a) {
var href = a.getAttribute("href") || "";
if (href.charAt(0) === "#") map[href.slice(1)] = a;
});
var current = null;
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
if (current) current.classList.remove("active");
current = map[entry.target.id];
if (current) current.classList.add("active");
}
});
}, { rootMargin: "0px 0px -70% 0px" });
Object.keys(map).forEach(function (id) {
var el = document.getElementById(id);
if (el) observer.observe(el);
});
})();
</script>
<!-- Inline assets/code-pane.html VERBATIM here when the document contains GitHub blob links. -->
</body>
</html>
references/doc-guide.md
# Readout document guide
A readout is the durable artifact of a conversation-length investigation. The reader may be the author weeks later, or a teammate who has none of the conversation's context. Every choice below serves standalone readability.
## The one hard requirement: self-contained
One `.html` file. All CSS and JS inline; no CDN links, no external fonts, no external images (use inline SVG or data URIs). Readouts get shared over Slack and opened offline — a broken external dependency silently ruins the document. System font stacks look good everywhere and avoid font embedding entirely. (Sanctioned exceptions, all inside the bundled code pane and all click-time: fetching source from GitHub, and lazy-loading highlight.js from its CDN when the pane opens. The document itself must still render completely offline.)
## The canonical template — chrome is fixed
Readouts share one visual identity so the library reads as a set; small per-document styling drift is a bug. Start every document from `assets/template.html` (in this skill's directory, next to `references/`) and fill its slots:
- The `<style data-readout>` and `<script data-readout>` blocks are the shared chrome — copy them **verbatim** and never edit them. They fix the warm-paper serif typography and palette (light + dark), the sticky Contents rail with scrollspy, the header anatomy, section rules, callouts, badges, tables, figures, collapsibles, and the provenance footer.
- The header anatomy is fixed: a mono uppercase `.kicker` (repo · context · readout type), the `h1` title, a one-line `.subtitle` framing the document, and a mono `.meta` line (date · audience · `org/repo@commit` pin).
- Use the template's classes — `.callout` (+ `.callout-label`, `.warn`), `.badge` (`good`/`bad`/`neutral`), `.table-wrap`, `figure`/`figcaption`, `<details>` — rather than inventing parallel patterns.
- Document-specific CSS (a diagram's dimensions, a one-off content element) goes ONLY in the empty `<style data-doc>` block, and must not override the chrome's tokens or selectors.
## Linked code references
When the repository's host and the examined commit are known, make every `file.rs:123` reference a real hyperlink into the code — e.g. `https://github.com/<org>/<repo>/blob/<commit>/<path>#L123`, with `#L10-L25` for ranges, and link code-snippet captions the same way. Pin links to the commit, never a branch, so they stay valid as the code moves. Get the remote and commit from `git remote get-url origin` / `git rev-parse HEAD` when the repo is checked out; otherwise use whatever the source material states. If the host or commit is unknown, keep references as plain monospace text — never guess URLs.
### The code pane
Linked references get an in-document source viewer: clicking a GitHub blob link opens the code in a split pane on the right, pushing the document content aside rather than overlaying it; clicking another reference loads into the same pane; the pane is resizable by dragging its left edge; a close button (and Escape) dismisses it. Code is syntax-highlighted (highlight.js, loaded lazily from a CDN only when the pane opens — plain text when offline).
Do not write this viewer yourself — inline the bundled asset at `assets/code-pane.html` (in this skill's directory, next to `references/`) **verbatim** immediately before `</body>`. It auto-attaches to every GitHub blob link in the document, so references need no extra markup — which is also why they must stay ordinary anchors: with JS disabled (or on cmd/ctrl-click) the links simply open GitHub.
How the pane finds source, in order:
1. **Files embedded at generation time** — the only path that works offline and for private repos with zero reader setup, so do it whenever a referenced repo is checked out locally. After writing the doc, run the bundled helper:
`python3 <skill-dir>/scripts/embed_snippets.py <doc.html> --repo <checkout> [--repo <another-checkout>]`
It scans the doc's blob links, extracts each referenced file *at its pinned commit* (`git show <commit>:<path>`), embeds whole files (gzip+base64 for anything non-trivial — the pane inflates in-browser), and injects the `data-code-snippets` blob. Mind the audience before embedding: it ships that source inside a shareable file. Only hand-write the JSON blob (`{"org/repo@commit:path": {"start": N, "text": "..."}}` windows) if git isn't available.
2. **Runtime fetch** of `raw.githubusercontent.com` when the reader clicks — works for public repos online.
3. **Reader-supplied token** — when the fetch fails with an HTTP error, the pane offers a paste-a-token form and retries via `api.github.com`, so teammates with repo access can view private code even without embedded snippets. The token stays in the reader's `sessionStorage` for that tab; it is never part of the document — never embed tokens or credentials of any kind in a readout.
4. **Graceful fallback** — a cause-specific message plus an "open on GitHub" link.
The asset handles 2–4 itself; your job is 1.
## Content
- **Document the refined end-state, not the chronology.** Conversations wander and self-correct; the readout presents the final, corrected understanding. Keep a discarded belief only when it's an instructive gotcha — those earn callouts.
- **Curate.** Length should track information density, not conversation length. Cut anything the reader doesn't need; a readout is not a transcript.
- **Ground every claim.** Cite `file.go:123`-style references, function and type names, endpoints. Verify references against the codebase before asserting them — a doc with a wrong line number loses the reader's trust for all the right ones too.
- **Distinguish verified from inferred.** If something was concluded in conversation but not confirmed in code, say so.
- **Open with an executive summary** — a few sentences a reader can stop after and still have the headline understanding.
- **Give the document a one-sentence `<meta name="description" content="...">`** — the readouts index page (`~/.readouts/index.html`, regenerated by `scripts/update_index.py`) uses it as the entry's summary line.
- **Close with a provenance footer**: date, what conversation/investigation it came from, which repos (and commit, if relevant) were examined.
## Structure and layout
The chrome is fixed; the sectioning is yours. Let the material choose it:
- Cross-platform / cross-system findings → per-system sections plus a comparison matrix
- A "how does X work" investigation → narrative explainer following the data flow
- A decision or tradeoff discussion → options, criteria, recommendation
Elements that usually earn their place in longer documents (all already styled by the template):
- Tables for anything the reader will want to compare across columns — for before/after numbers, add an explicit delta column (`<span class="delta">`)
- `<details>` collapsibles for deep-dive appendices that would bloat the main read
- `.badge` chips for statuses, HTTP codes, platform names
- `.callout` with a `.callout-label` for gotchas, cautions, and instructive discarded theories — the template styles these as a soft tint with a full hairline border; never restyle them into a left-accent stripe (a thick colored left border as the sole edge treatment is banned in readouts)
- Diagrams as inline SVG when a flow or topology is central to understanding — draw with `currentColor` so both palettes work, and size them in the `data-doc` style block
## Reading experience
The template handles column width, responsive collapse, and both palettes. What remains your responsibility:
- Wrap wide tables in `.table-wrap` so they scroll instead of overflowing on phones
- Every TOC entry must point at a real `<section id>`; nested entries use `class="sub"`
- Monospace for code, paths, and identifiers; if you add syntax highlighting to code blocks in the document body, it must be inline (the code pane handles its own highlighting via its lazy CDN loader)
## JavaScript
Progressive enhancement only — scrollspy, collapsibles, theme toggles, and the bundled code pane. The document must read fine with JS disabled.
## Before you finish
Sanity-check the artifact: parse the file (e.g. with Python's `html.parser`) to catch unclosed tags, confirm there are zero external `http(s)://` resource references outside the bundled code-pane asset, and skim the rendered structure for empty sections or placeholder text left behind.
scripts/embed_snippets.py
#!/usr/bin/env python3
"""Embed referenced source files into a readout's code-pane snippet blob.
Scans an HTML readout for GitHub blob links, extracts each referenced file at its
pinned commit from a local git checkout (git show <commit>:<path>), and injects a
<script type="application/json" data-code-snippets> blob that the bundled code
pane reads. Files are embedded whole; anything larger than a couple of KB is
gzip+base64'd ("gz"), which the pane inflates in the browser via
DecompressionStream. This makes the pane work offline and for private repos with
zero reader setup.
Usage:
python3 embed_snippets.py <doc.html> --repo <checkout-path> [--repo <path> ...]
Each --repo must be a git checkout; its origin remote determines which org/repo
links it can satisfy. Re-running replaces any existing snippet blob.
"""
import argparse
import base64
import gzip
import json
import re
import subprocess
import sys
from pathlib import Path
BLOB_RE = re.compile(r'https://github\.com/([^/"\s<>]+)/([^/"\s<>]+)/blob/([^/"\s<>]+)/([^"#\s<>?]+)')
# Requires "{" right after the tag so the mention of this tag inside the code-pane
# asset's header comment can never match (a real blob always starts with JSON).
SNIPPET_BLOCK_RE = re.compile(
r'<script type="application/json" data-code-snippets>\s*\{.*?</script>\n?', re.S
)
GZ_THRESHOLD = 2048
def repo_slug(checkout: Path):
"""(org, repo) from the checkout's origin remote, or None."""
r = subprocess.run(
["git", "-C", str(checkout), "remote", "get-url", "origin"],
capture_output=True, text=True,
)
if r.returncode != 0:
return None
m = re.search(r"github\.com[:/]([^/]+)/(.+?)(?:\.git)?/?$", r.stdout.strip())
return (m.group(1), m.group(2)) if m else None
def file_at(checkout: Path, commit: str, path: str):
"""File contents at the pinned commit, falling back to HEAD. -> (text, ref) or None."""
for ref in (commit, "HEAD"):
r = subprocess.run(
["git", "-C", str(checkout), "show", f"{ref}:{path}"],
capture_output=True,
)
if r.returncode == 0:
try:
return r.stdout.decode("utf-8"), ref
except UnicodeDecodeError:
return None # binary file; nothing sensible to embed
return None
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("doc", type=Path, help="readout HTML file to modify in place")
ap.add_argument("--repo", action="append", type=Path, default=[], required=True,
help="git checkout that can satisfy links (repeatable)")
args = ap.parse_args()
html = args.doc.read_text()
refs = {m for m in BLOB_RE.findall(html)}
if not refs:
print("No GitHub blob links found; nothing to embed.")
return 0
slugs = {}
for checkout in args.repo:
slug = repo_slug(checkout)
if slug is None:
print(f"warning: {checkout} has no GitHub origin remote; skipping", file=sys.stderr)
continue
slugs.setdefault(slug, checkout)
snippets, missing, from_head = {}, [], []
for org, repo, commit, path in sorted(refs):
label = f"{org}/{repo}@{commit[:10]}:{path}"
checkout = slugs.get((org, repo))
if checkout is None:
missing.append(f"{label} (no --repo checkout for {org}/{repo})")
continue
got = file_at(checkout, commit, path)
if got is None:
missing.append(f"{label} (not found in {checkout}, or binary)")
continue
text, ref_used = got
if ref_used != commit:
from_head.append(label)
entry = {"start": 1, "full": True}
raw = text.encode("utf-8")
if len(raw) > GZ_THRESHOLD:
entry["gz"] = base64.b64encode(gzip.compress(raw, 9)).decode("ascii")
else:
entry["text"] = text
snippets[f"{org}/{repo}@{commit}:{path}"] = entry
if not snippets:
print("Nothing could be embedded:", file=sys.stderr)
for m in missing:
print(f" - {m}", file=sys.stderr)
return 1
# "</" must not appear literally inside a <script> block; "<\/" is the same string in JSON.
payload = json.dumps(snippets, ensure_ascii=False).replace("</", "<\\/")
block = f'<script type="application/json" data-code-snippets>\n{payload}\n</script>\n'
if SNIPPET_BLOCK_RE.search(html):
html = SNIPPET_BLOCK_RE.sub(lambda _: block, html, count=1)
where = "replaced existing blob"
elif "<style data-code-pane>" in html:
html = html.replace("<style data-code-pane>", block + "<style data-code-pane>", 1)
where = "inserted before code-pane asset"
elif "</body>" in html:
html = html.replace("</body>", block + "</body>", 1)
where = "inserted before </body>"
else:
html = html + "\n" + block
where = "appended (no </body> found)"
args.doc.write_text(html)
print(f"Embedded {len(snippets)} file(s) into {args.doc} ({where}); blob is {len(payload):,} bytes.")
if from_head:
print("warning: pinned commit not found locally; embedded from HEAD instead (content may drift from links):", file=sys.stderr)
for m in from_head:
print(f" - {m}", file=sys.stderr)
if missing:
print("warning: could not embed (pane will fall back to fetch/token):", file=sys.stderr)
for m in missing:
print(f" - {m}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
scripts/update_index.py
#!/usr/bin/env python3
"""Regenerate the readouts index page.
Scans a readouts directory for .html files (excluding index.html), pulls each
document's <title>, <meta name="description">, and date (filename YYYY-MM-DD
prefix, else mtime), and writes a styled index.html listing every readout
newest-first. The index is fully regenerated on every run, so it is always
safe to re-run and never drifts from the directory contents.
Usage:
python3 update_index.py [readouts_dir] (default: ~/.readouts)
"""
import html
import re
import sys
from datetime import datetime
from pathlib import Path
TITLE_RE = re.compile(r"<title>(.*?)</title>", re.S | re.I)
DESC_RE = re.compile(r'<meta\s+name="description"\s+content="(.*?)"', re.S | re.I)
DATE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})")
PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Readouts</title>
<style>
:root { --bg: #faf8f4; --fg: #262220; --muted: #857c6e; --hairline: #e3ddd1; --accent: #31567d; }
@media (prefers-color-scheme: dark) {
:root { --bg: #1e1c19; --fg: #e8e4dc; --muted: #948b7c; --hairline: #383530; --accent: #8caec9; }
}
body {
margin: 0; background: var(--bg); color: var(--fg);
font-family: Charter, 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;
line-height: 1.6;
}
main { max-width: 44rem; margin: 0 auto; padding: 3rem 1.25rem 4rem; }
h1 { font-size: 1.6rem; margin: 0 0 0.3rem; }
.sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 2rem; }
ol { list-style: none; margin: 0; padding: 0; }
li { padding: 1rem 0; border-top: 1px solid var(--hairline); }
li:last-child { border-bottom: 1px solid var(--hairline); }
.date {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.72rem; color: var(--muted); letter-spacing: 0.04em;
}
a.title { color: var(--fg); font-size: 1.05rem; font-weight: 600; text-decoration: none; display: block; margin: 0.15rem 0; }
a.title:hover { color: var(--accent); }
p.desc { margin: 0.1rem 0 0; color: var(--muted); font-size: 0.9rem; }
p.empty { color: var(--muted); }
footer { margin-top: 2.5rem; color: var(--muted); font-size: 0.78rem; }
</style>
</head>
<body>
<main>
<h1>Readouts</h1>
<p class="sub">__COUNT__ document__PLURAL__ · newest first</p>
<ol>
__ITEMS__
</ol>
<footer>Index regenerated __UPDATED__ by the readout skill. Run <code>update_index.py</code> to refresh.</footer>
</main>
</body>
</html>
"""
ITEM = """ <li>
<span class="date">__DATE__</span>
<a class="title" href="__FILE__">__TITLE__</a>
__DESC__ </li>"""
def entry(p: Path):
src = p.read_text(errors="replace")
m = TITLE_RE.search(src)
title = html.unescape(m.group(1).strip()) if m else p.stem
m = DESC_RE.search(src)
desc = html.unescape(m.group(1).strip()) if m else ""
m = DATE_RE.match(p.name)
date = m.group(1) if m else datetime.fromtimestamp(p.stat().st_mtime).strftime("%Y-%m-%d")
return {"file": p.name, "title": title, "desc": desc, "date": date, "mtime": p.stat().st_mtime}
def main() -> int:
d = Path(sys.argv[1]).expanduser() if len(sys.argv) > 1 else Path.home() / ".readouts"
d.mkdir(parents=True, exist_ok=True)
docs = [entry(p) for p in sorted(d.glob("*.html")) if p.name != "index.html"]
docs.sort(key=lambda e: (e["date"], e["mtime"]), reverse=True)
items = []
for e in docs:
desc_html = f' <p class="desc">{html.escape(e["desc"])}</p>\n' if e["desc"] else ""
items.append(
ITEM.replace("__DATE__", e["date"])
.replace("__FILE__", html.escape(e["file"], quote=True))
.replace("__TITLE__", html.escape(e["title"]))
.replace("__DESC__", desc_html)
)
body = "\n".join(items) if items else ' <p class="empty">No readouts yet.</p>'
page = (PAGE.replace("__COUNT__", str(len(docs)))
.replace("__PLURAL__", "" if len(docs) == 1 else "s")
.replace("__ITEMS__", body)
.replace("__UPDATED__", datetime.now().strftime("%Y-%m-%d %H:%M")))
(d / "index.html").write_text(page)
print(f"Indexed {len(docs)} readout(s) -> {d / 'index.html'}")
return 0
if __name__ == "__main__":
sys.exit(main())
SKILL.md
---
name: readout
description: Produce a polished, self-contained HTML "readout" document under ~/.readouts (with an auto-maintained index page), either by snapshotting the findings accumulated in the current conversation or — when invoked fresh, e.g. "/readout on how github webhook events are processed" — by sharpening scope with clarifying questions and researching the codebase before documenting. The work runs in a child agent so the main conversation's context stays clean. Use whenever the user invokes /readout, says "write this up", "turn this into a doc/page", "make a readout", or asks for a readable, shareable document capturing findings or explaining how something works.
---
# Readout
A readout turns an investigation into a durable HTML document someone can read weeks later without any of the original context. It starts one of two ways:
- **Snapshot mode** — invoked mid-conversation ("write this up"): the conversation's accumulated findings are the source material.
- **Research mode** — invoked fresh ("/readout on how github webhook events are processed in the server"): there is no conversation to mine, so the investigation itself is part of the job.
Either way, invoking this skill is a **side task**. Your job as the main agent is to sharpen the scope, launch a child agent with a good brief, and get out of the way — the child does the mining/research and the writing, keeping that (often large) work out of your context window.
## Orchestrator workflow
### 1. Sharpen the scope — ask before launching
A vague brief produces a vague document. Before launching you should be able to list the specific questions the document will answer; if you can't, interview the user first:
- Ask 2–4 targeted questions, offering concrete options rather than open prompts — take a quick look at the code or topic first so the options are real (subsystems, entry points, competing concerns). For "/readout on how github webhook events are processed": which direction matters — inbound triggers, post-back, or both? a current-state reference or a gotcha hunt? which repo(s)?
- Always pin down **depth and audience**: high-level orientation vs. deep mechanics with line-level grounding; personal notes vs. shared with the team.
- Respect a shrug. "Just a high-level overview" is a valid answer — record it in the brief and move on rather than interrogating. Even then, try to extract the two or three questions the reader most needs answered; specificity is what makes a readout useful.
- Skip the interview when the scope is already specific — a snapshot of a focused conversation, or a precise research request, needs no questions. In snapshot mode the conversation usually supplies the questions; ask only when the invocation is ambiguous about which threads to include.
### 2. Compose the brief
Write a short brief (roughly 10–20 lines) carrying **pointers, not payloads**:
- A working title / topic, and the mode (snapshot or research)
- The specific questions the document must answer (from the conversation or the interview), plus depth and audience
- Scope: which threads/subsystems to cover, and anything to explicitly exclude
- Snapshot mode: headline conclusions worth centering the doc on, one line each — the child pulls the full content from conversation history itself, so don't paste findings wholesale
- Research mode: starting pointers — entry-point files, symbols, or directories you already know about
- Absolute paths to the repos/directories that ground the work
- Each repo's hosted URL and the examined commit when known (e.g. `github.com/org/repo @ abc123`), so the document can hyperlink code references
### 3. Launch one local child agent
Spawn exactly one child agent via `run_agents`, **local** execution. Local matters: the document lands on the user's filesystem and opens in their browser. Name the child `readout-<topic-slug>`.
Build the child's prompt from the template below. It must include:
- The brief
- The source-material block matching the mode (snapshot mode also needs your agent run ID — `current_run_id` from the orchestration runtime context — so the child can mine the parent conversation with `search_conversation_history`)
- The instruction to read `references/doc-guide.md` from this skill's directory before writing
- The output path convention and completion protocol
### 4. Get back to work
After launching, resume whatever you were doing, or end your turn — the child's completion message arrives on its own; relay the file path to the user with a one-line description when it does. In research mode a fresh conversation may have nothing else pending; just end the turn. Don't sit in a wait loop unless the user asked to wait for the document.
## Child agent prompt template
Adapt this; keep the structure, and include the source-material block that matches the mode.
```
You are producing a "readout": a single self-contained HTML document that answers a
specific set of questions about <topic>, for a reader who has none of this context.
Brief:
<brief — including the questions to answer, depth, and audience>
Source material (snapshot mode):
- The parent conversation: agent run ID <current_run_id>. Use search_conversation_history
with agent_run_id set to that ID. Make several targeted queries — one per question in
the brief — rather than one broad query; targeted queries surface far more usable detail.
- The codebase(s) at <absolute paths>. The conversation is your starting point, not a cage:
verify file references before asserting them, and where a section needs more depth to
stand on its own, go read the code and fill the gap.
Source material (research mode):
- Investigate directly in the codebase(s) at <absolute paths>. Let the brief's questions
drive the investigation: trace the actual code paths, read the real implementations, and
ground every claim in file:line references. Distinguish verified from inferred. Do not
pad the document with generic knowledge — its value is what's true of THIS codebase.
- Repo host + commit for linked code references, if known: <github.com/org/repo @ commit>
(otherwise derive from git; see the doc guide's "Linked code references").
Start from the canonical template at <skill-directory>/assets/template.html — its
data-readout chrome blocks must be copied verbatim so every readout looks like every
other. Before writing, read <skill-directory>/references/doc-guide.md and follow it.
Output:
- Write ONE self-contained HTML file to ~/.readouts/<YYYY-MM-DD>-<topic-slug>.html
(create ~/.readouts if it doesn't exist; suffix -2, -3, ... if the name is taken;
get the date from `date +%F`).
- Embed referenced source per the doc guide when a repo is checked out
(<skill-directory>/scripts/embed_snippets.py).
- Refresh the readouts index: python3 <skill-directory>/scripts/update_index.py
(fully regenerates ~/.readouts/index.html listing every readout).
- When the file is written, open it with `open <path>` (skip this if the environment is
headless).
- Report back to your orchestrator: the absolute file path, a 2–3 sentence summary of what
the document covers, and anything you could not verify.
```
## Fallbacks
- **Child spawning unavailable or denied**: produce the document yourself, following `references/doc-guide.md`. If a research subagent is available, delegate the conversation-mining or code investigation to it so your context still stays lean.
- **Child can't search conversation history** (snapshot mode; it will report this back): reply to the child with a distilled dump of the findings so it can proceed — this is the one case where payload-in-prompt is the right call.
- **User-provided material instead of a conversation** (transcripts, files, links): treat that material as the source; everything else in the workflow is unchanged.