STEM source → reviewable LaTeX notes with a sandboxed compile gate
Turn user-selected STEM references into source-tagged LaTeX study material while keeping installs and compilation opt-in, preserving discrepancies, and independently proving that every pinned upstream template compiles in Tectonic's untrusted mode.
Run this workflow
CI-verified, 4/4 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Students and instructors who want editable formula sheets, worked solutions, or summary notes derived from their own PDFs, images, slides, or documents, with source/page tags and an explicit human review step. The included gate is a supply-chain and LaTeX-compile check over the pinned Skill; the user still verifies every extracted fact, equation, unit, assumption, and answer against the source.
Not for
- Treating a successful PDF compile as evidence that the mathematics, OCR, citations, or worked answers are correct
- Uploading confidential, licensed, or student-identifiable source material to a model provider without authorization and an appropriate retention policy
- Installing Poppler, LibreOffice, TeX distributions, or any other dependency without the user's explicit choice; the Skill's own contract requires consent first
- Executing arbitrary generated TeX with normal shell escape or broad filesystem access; compile generated material in an isolated directory with untrusted mode and inspect it first
- Treating this as a mature or independently audited project: the repository was created in September 2026, has three commits, no upstream tests or CI, no checked-in completed example, and an unsigned HEAD
The Stack
Tested Against
Shass27/stem-notes@00aea95d7e3b0795275e6dab46cd4030f2789323source archive@sha256:aead94f7933f55797a722b74826a78ace8dea9d25980c1051fa42f24cc3f34a5tectonic@0.17.0Tectonic default_bundle_v33@6ffe055852f8faf66c0acbe1a7fb27f87b869a90bad1204f3bf4d9683f597c7cSide effects & data flow
- Network
- codeload.github.com and github.com/tectonic-typesetting in CI, relay.fullyjustified.net for integrity-addressed TeX bundle files during compilation, the selected model provider only during a real note-generation run
- Writes
- ./stem-notes.tar.gz, ./checked/, ./compile/, ./tectonic-bin/, ./tectonic-cache/ in the disposable CI directory, user-selected .tex files and optional PDFs during a real run
- Credentials
- A model-provider or coding-agent credential for a real generation run
Data privacy
- GitHub and Tectonic's official bundle relay ← CI requests only public pinned source/compiler assets and TeX support files; no user document or credential (retention: per the operators' published policies)
- the model provider selected by the Agent Skills-compatible runtime ← During a real run: source-document text/images and the user's instructions; none is sent by FlowStacks CI (retention: per the selected provider and account policy)
Prerequisites
- Claude Code or another Agent Skills-compatible runtime for a real drafting run
- Authorized local copies of the source documents
- Human subject-matter review of all generated content
- curl, tar, and Node.js 20+ for the independent validation gate
Steps
- 1
Pin the Skill contract and compile every upstream template as untrusted TeX
Fetch the immutable source archive, verify its exact size, SHA-256, path set, and regular-file/directory-only layout before extracting nine named text files. Check the Skill's consent, source-provenance, discrepancy, and compile-choice requirements without executing it. Then download the official Tectonic 0.17.0 binary for the CI host, verify the release checksum, remove only TODO comments from copies of the three templates, compile them with known-insecure TeX features disabled, and assert the resolved bundle fingerprint plus three non-empty PDFs and clean logs.
set -eu curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 1048576 -fsSL https://codeload.github.com/Shass27/stem-notes/tar.gz/00aea95d7e3b0795275e6dab46cd4030f2789323 -o stem-notes.tar.gz node <<'NODE' const crypto = require("crypto"); const fs = require("fs"); const { execFileSync } = require("child_process"); function bad(message) { console.error("BAD: " + message); process.exit(1); } const archive = fs.readFileSync("stem-notes.tar.gz"); if (archive.length !== 9165) bad("source archive size changed"); const digest = crypto.createHash("sha256").update(archive).digest("hex"); if (digest !== "aead94f7933f55797a722b74826a78ace8dea9d25980c1051fa42f24cc3f34a5") bad("source archive digest changed"); const root = "stem-notes-00aea95d7e3b0795275e6dab46cd4030f2789323/"; const expected = [ root, root + "LICENSE", root + "README.md", root + "SKILL.md", root + "references/", root + "references/questions.md", root + "references/style-guide.md", root + "references/tooling.md", root + "templates/", root + "templates/formula_sheet.tex", root + "templates/solutions.tex", root + "templates/summary_notes.tex", ]; const names = execFileSync("tar", ["-tzf", "stem-notes.tar.gz"], { encoding: "utf8" }).trim().split("\n"); if (JSON.stringify(names) !== JSON.stringify(expected)) bad("source archive path set changed"); const verbose = execFileSync("tar", ["-tvzf", "stem-notes.tar.gz"], { encoding: "utf8" }).trim().split("\n"); if (verbose.some((line) => !["-", "d"].includes(line[0]))) bad("source archive contains a non-file/directory member"); console.log("stem-notes source pinned OK: 9 text files at 00aea95, sha256 " + digest); NODE mkdir -p checked/templates checked/references compile ROOT=stem-notes-00aea95d7e3b0795275e6dab46cd4030f2789323 for name in LICENSE README.md SKILL.md references/questions.md references/style-guide.md references/tooling.md templates/formula_sheet.tex templates/solutions.tex templates/summary_notes.tex; do tar -xOf stem-notes.tar.gz "$ROOT/$name" > "checked/$name" done node <<'NODE' const fs = require("fs"); function bad(message) { console.error("BAD: " + message); process.exit(1); } function read(name) { return fs.readFileSync("checked/" + name, "utf8"); } function has(text, value, message) { if (!text.includes(value)) bad(message); } const license = read("LICENSE"); const skill = read("SKILL.md"); const questions = read("references/questions.md"); const style = read("references/style-guide.md"); has(license, "MIT License", "MIT license missing"); has(skill, "name: stem-notes", "Skill identity changed"); has(skill, "Never install without an MCQ answer", "dependency-consent boundary changed"); has(skill, "compile question is always asked, last", "compile-choice boundary changed"); has(skill, "If they disagree, recheck, then report the discrepancy", "discrepancy rule changed"); has(skill, "If the user says stop, stop the task immediately", "stop boundary changed"); has(questions, "Just .tex (Recommended if you use Overleaf)", "no-compile choice missing"); has(style, "Identify each source by a short key", "source-provenance rule changed"); const templates = ["formula_sheet.tex", "solutions.tex", "summary_notes.tex"]; const forbidden = /\\(?:write18|immediate|openin|openout|read|input|include|directlua|usepackage\s*\{shellesc\})\b/i; for (const name of templates) { const source = read("templates/" + name); has(source, "\\documentclass", name + " has no document class"); has(source, "\\begin{document}", name + " has no document body"); has(source, "\\end{document}", name + " has no document terminator"); if (forbidden.test(source)) bad(name + " contains a forbidden TeX primitive"); fs.writeFileSync("compile/" + name, source.replace(/%% TODO[^\n]*/g, "")); } console.log("Skill contract OK: explicit install/compile consent, source tags, discrepancy reporting, and immediate stop"); NODE case "$(uname -s)-$(uname -m)" in Linux-x86_64) TECTONIC_ASSET=tectonic-0.17.0-x86_64-unknown-linux-gnu.tar.gz TECTONIC_SHA=1a715688baf591e650c8aeb160ae934e181685eecbb38b317de30b269ac5d606 ;; Darwin-arm64) TECTONIC_ASSET=tectonic-0.17.0-aarch64-apple-darwin.tar.gz TECTONIC_SHA=a3f1cac7c5678f01661a92212f58480ae3b0634115d880dbc59e2953ded45667 ;; *) echo "BAD: unsupported CI host for pinned Tectonic binary" >&2; exit 1 ;; esac curl --proto '=https' --tlsv1.2 --retry 3 --max-filesize 33554432 -fsSL "https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%400.17.0/$TECTONIC_ASSET" -o tectonic.tar.gz ACTUAL_TECTONIC_SHA=$(shasum -a 256 tectonic.tar.gz | cut -d ' ' -f 1) test "$ACTUAL_TECTONIC_SHA" = "$TECTONIC_SHA" || { echo "BAD: Tectonic release digest changed" >&2; exit 1; } mkdir -p tectonic-bin tectonic-cache tar -xzf tectonic.tar.gz -C tectonic-bin test "$(tectonic-bin/tectonic --version)" = "Tectonic 0.17.0" || { echo "BAD: wrong Tectonic version" >&2; exit 1; } for template in compile/formula_sheet.tex compile/solutions.tex compile/summary_notes.tex; do TECTONIC_CACHE_DIR="$PWD/tectonic-cache" tectonic-bin/tectonic --untrusted --keep-logs --outdir compile "$template" done node <<'NODE' const fs = require("fs"); const path = require("path"); function bad(message) { console.error("BAD: " + message); process.exit(1); } for (const base of ["formula_sheet", "solutions", "summary_notes"]) { const pdf = fs.readFileSync("compile/" + base + ".pdf"); const log = fs.readFileSync("compile/" + base + ".log", "utf8"); if (pdf.length < 1024 || pdf.subarray(0, 5).toString() !== "%PDF-") bad(base + " did not produce a valid non-empty PDF"); if (/Undefined control sequence|Emergency stop|Fatal error|^!/m.test(log)) bad(base + " compile log contains an error"); } const expectedBundle = "6ffe055852f8faf66c0acbe1a7fb27f87b869a90bad1204f3bf4d9683f597c7c"; const hashes = []; function walk(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const target = path.join(dir, entry.name); if (entry.isDirectory()) walk(target); else if (entry.isFile() && !entry.name.endsWith(".lock")) hashes.push(fs.readFileSync(target, "utf8").trim()); } } walk("tectonic-cache/bundles/hashes"); if (!hashes.includes(expectedBundle)) bad("resolved Tectonic bundle fingerprint changed"); console.log("template compile OK: formula_sheet, solutions, summary_notes; Tectonic 0.17.0 untrusted; bundle " + expectedBundle); NODE - 2
Generate only after the user chooses scope, outputs, and privacy boundaries
Install the pinned Skill only after reviewing it and constraining the agent's filesystem and network access. Let it inventory authorized source files, answer its intake questions, and draft into the chosen directory. Before compiling, inspect generated TeX for unexpected file/network/shell primitives; compile in an isolated directory with Tectonic untrusted mode. Finally compare every formula, value, unit, citation, and solution against the source. Model extraction and content correctness are outside the CI attestation.
Eval, 4 fixtures
Last passed: verified todaysource-pinnedcontainstimeout 420s · max $0Expected:
stem-notes source pinned OK: 9 text files at 00aea95, sha256 aead94f7933f55797a722b74826a78ace8dea9d25980c1051fa42f24cc3f34a5skill-contractcontainstimeout 420s · max $0Expected:
Skill contract OK: explicit install/compile consent, source tags, discrepancy reporting, and immediate stopthree-template-compilecontainstimeout 420s · max $0Expected:
template compile OK: formula_sheet, solutions, summary_notes; Tectonic 0.17.0 untrusted; bundle 6ffe055852f8faf66c0acbe1a7fb27f87b869a90bad1204f3bf4d9683f597c7cclean-exitexit_codetimeout 420s · max $0Expected:
0
Results
The audited Skill provides intake questions and three LaTeX templates for formula sheets, worked solutions, and summary notes. FlowStacks pins the complete nine-file source, rejects archive drift and unsafe member types, checks the consent/source-citation/discrepancy contracts as text, strips only TODO comments from each template, and compiles all three with a checksum-pinned Tectonic 0.17.0 binary in untrusted mode. This establishes packaging and compileability—not OCR quality, source fidelity, or mathematical correctness.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- One AGENTS.md, no drift: prove CLAUDE.md is a real symlink and the generated files are in sync
- Read your token receipts right: volume and cost are different leaderboards
- Pick a model with evidence: a GitHub Models bake-off that fits the free cap
- Track a tool's hype curve across any Substack (no API key)
- Fabric + Fable 5: Get Through Your Reading Pile
- Agent Review Gate: a Schema-Forced Approve or Block in CI
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).