ts-paper — Traitement du Signal paper generator (orchestrator)
You produce a publication-format paper (in whatever template the user picks) from a dropped input
by first routing it (Stage 0) and then orchestrating seven focused sub-skills (the figure stage
delegates its editable-vector output to ts-figure-svg — PaperBanana renders, then the figure is redrawn
as a native audited SVG; falling back to ts-figure-optimize's DrawAI HYBRID, then to keeping the approved
PNG as-is, never a lossy redraw — a 7-stage chain, not 8 stages).
Priority: QUALITY FIRST, cost second
The goal is a paper that reads like a real, well-written TS journal article with real, complete,
correctly-matched citations. Never trade quality for cost. Do every quality step — verify each
citation, self-review the draft, run the linters, right-size and polish, fix every compile error.
Cost is kept reasonable as a by-product of Claude doing more per turn (one holistic pass instead of
hundreds of micro-calls) — not by skipping reviews, shortening sections, or accepting a weaker
draft. If a quality step needs another pass, take it.
The full suite (idea → figured paper) — where this orchestrator sits
This skill is the story2paper == proposal2paper spine. Two NEW upstream blocks are optional:
[corpus.jsonl] ─▶ ts-kg-build (NEW, optional) ─▶ kg/ build a research-pattern KG (best-effort)
[raw idea] ─▶ ts-idea2story (NEW) ─▶ story_proposal.md + retrieved_papers.json
(uses kg/ if present; searches the web)
[story_proposal.md OR a user's own proposal] ─▶ ts-paper (THIS) ─▶ plan→cite→write→refine→review→figure→latex ─▶ main.pdf
( review = adversarial hardening, runs by default; user may opt out for a quick draft )
( figure ends every figure as an editable VECTOR PDF — matplotlib born-vector; image-model raster → ts-figure-optimize reconstruction )
- If the user hands you a raw idea, run ts-idea2story first → it writes
story_proposal.md
(feed it here as the proposal) and retrieved_papers.json (the cite stage reuses it, so its
search is much lighter — verify-the-seed + fill-the-gaps instead of a cold sweep).
- If the user hands you a proposal, start here directly (skip the upstream blocks).
- ts-kg-build only matters if you want KG-grounded recall in idea2story; it needs a user-supplied
embedding endpoint and is honestly best-effort (TS already ships a built KG).
Inputs
- A proposal (markdown/text): problem, gap, proposed method/solution, evaluation plan, claimed contributions.
- Optional references the user already has (BibTeX, DOIs, or a
references.json with title/authors/year/venue/doi).
- An optional template name (default
ts_iieta). The suite is template-agnostic — see below.
- Optional
retrieved_papers.json (the citation seed produced by ts-idea2story, if the upstream idea→story stage ran) — the cite stage reuses it.
- Integrity (mode-dependent, overrides everything). In proposal mode there is no real data: never invent results/metrics/percentages; result tables stay blank (
--); prose is forward-looking ("we evaluate… we expect…"). In data-aware mode (the router found real results) the opposite holds: report the real numbers in past tense, and every number must trace to the user's data (machine-checked by the number-audit) — still never invent or round a number that wasn't measured.
Working directory & handoff (files on disk = the contract between stages)
Create one working dir (default ./ts_paper_run/, or a user-named one) holding:
proposal.md # input
references.{json,bib} # input (optional)
blueprint.json # ← stage 1 (plan)
refs.bib # ← stage 2 (cite): complete, real BibTeX only
template.json # ← the active template spec (copied in by the plan stage)
sections/<id>.tex # ← stage 3 (write): LaTeX body per section (+ abstract.tex)
figures/<label>.pdf # ← stage 6 (figure): EMBEDDED editable vector (matplotlib born-vector;
# image-model raster → ts-figure-optimize); .png (kept original) + source alongside
main.tex, main.pdf # ← stage 7 (assemble): compiled paper
<template assets> # the template's .sty/.cls + masthead assets (copied by assemble)
Template-agnostic (NOT locked to one venue)
The suite writes to whatever template the user picks; paper content quality is invariant.
A template is a directory templates/<name>/ (under this skill) holding template.json (the
parametric spec: ordered section list + per-section recipes, word bands, citation style + floor,
title/keyword rules, caption positions, heading case, masthead) + its LaTeX .sty/.cls +
main.tex.tmpl. 🔴 A template's .sty/.cls must be either USER-PROVIDED or the venue's OFFICIAL files
fetched verbatim — NEVER hand-authored or approximated; if neither is available, STOP and ask the user
(see ts-paper-latex's HARD RULE). Two unofficial approximations ship for demo only ("official": false):
ts_iieta (Traitement du Signal, two-column numeric — the default) and neurips (single-column,
author-year) — replace with official/user files before any real submission. To use a template, pass
template=<name>; the plan stage validates it (template_lint.py) and copies it into the workdir,
and every downstream script reads template.json from the workdir — no script hardcodes TS.
A user adds a new venue by dropping a templates/<name>/ dir; no code changes. Word bands, shape
contracts (item/subsection/table counts), citation style/floor, and the preamble all come from the
spec. What stays invariant across templates = the content-quality + integrity logic (no
fabricated results, claims-map gating, no stubs, terminology consistency, Method-First order,
LaTeX safety, the figure integrity rule).
Version check (silent, non-blocking)
Run python ts-paper/scripts/check_update.py once at the start. If it prints an update notice,
show it to the user and continue — never block the pipeline for a version check.
Preflight — environment & keys (DO THIS FIRST, before Stage 0)
Before routing, run ONE environment + credential check and resolve gaps by this policy:
credentials / user-data → ASK the user (and explain exactly what to configure); environment / runtime → SELF-CONFIGURE (only ask if you genuinely cannot). First source ./input/env.sh (or the repo .env) if present — keys live in key.txt.
- Embedding + KG (for
ts-idea2story KG-recall & novelty). Probe TS_EMBED_* (a Qwen3-Embedding-8B endpoint) and whether a KG exists (kg/). If EITHER is missing → ASK the user: "Run the KG-grounded recall / novelty part? It needs an embedding API — Qwen3-Embedding-8B (TS_EMBED_API_KEY / TS_EMBED_BASE_URL / TS_EMBED_MODEL) — plus a research-pattern KG (build it with ts-kg-build)." If they decline (or the input skips idea2story) → degrade gracefully to web-search-only story (idea2story supports this; label it web/lexical-only). Never fabricate a key.
- Image-gen key (for
ts-paper-figure). Probe the official PaperBanana first — python3 skills/ts-figure-svg/scripts/setup_paperbanana.py --check-only (it wants OPENROUTER_API_KEY, or GOOGLE_API_KEY); if that is not ready, probe gen_image.py / TS_FIG_* (a quick test render). If neither is available (unset env: … or a failing render) → ASK the user: "Generate figures? It needs an image-model key — TS_FIG_API_KEY / TS_FIG_BASE_URL / TS_FIG_MODEL (e.g. gpt-image-2)." If they decline → skip image-model figure drawing (every non-results figure; in proposal mode that is ALL figures — only results-section matplotlib data plots need no key); note the skipped placeholders in logs/0_route.io.md. Never improvise a key.
- Environment / runtime → SELF-CONFIGURE (do NOT ask first; you have the ability).
- Native SVG redraw (
ts-figure-svg, the default path from an approved render): nothing to provision — audit_svg.py is stdlib-only. Pull the official PaperBanana with python3 skills/ts-figure-svg/scripts/setup_paperbanana.py. An SVG→PDF backend is needed at the end (rsvg-convert / inkscape / a headless browser / cairosvg) — install one yourself if none is present.
- DrawAI vectorizer (the FALLBACK, only when the native redraw cannot converge): if the
ts-figure-optimize runtime is not ready (drawai doctor ≠ ok / models absent) → provision it yourself per ts-figure-optimize's setup — ModelScope source = NO HF token, GPU: install uv, run drawai setup local --source modelscope --device gpu, then apply the documented fixes (see that skill). Only if provisioning is genuinely impossible (no GPU / no network after trying) → tell the user, and keep the approved high-res PNG as-is (no conversion — never a redraw or flat fallback).
- python deps (numpy / matplotlib / python-pptx / cairosvg) and LaTeX (latexmk / TinyTeX): if missing → install them yourself.
Record the preflight outcome (what's available, what the user opted out of, what you auto-provisioned) in
logs/0_route.io.md.
Stage 0 — route the dropped input (a Claude reasoning step, not a sub-skill)
The user drops one input and usually does not declare a pipeline or a mode. Before anything else,
read the input (and any sidecar files) and classify it yourself — a plain Claude judgement over the
content, with no fixed schema to match. Pick the entry stage and set results_mode in the
workdir's template.json — the one switch the whole downstream suite reads. Set/re-assert it after
the plan stage copies the template in (the cp resets it to the bundled proposal default), or as the
plan stage's first action — so the router's choice survives the copy.
| class |
what was dropped |
route / entry |
results_mode |
| (a) bare/short IDEA |
a one-line message or a thin note, no method/eval structure |
run ts-idea2story first → then plan |
proposal |
| (b) structured PROPOSAL |
problem / method / eval / contributions, no measured results |
plan (proposal) |
proposal |
| (c) PROPOSAL/REPORT + REAL RESULTS |
measured numbers/tables in the text, or an attached data file (CSV/JSON/any form) |
plan → ts-paper-data (DATA-AWARE) |
data_aware |
(d) existing story.json |
an 8-field story from a prior idea2story run |
plan (skip idea2story) |
proposal (a story.json from idea2story is forward-looking — no results); if the user also dropped real results alongside it, that is class (c)/data_aware by step 1 |
How to judge (shape-agnostic — there is NO fixed results schema):
- Is there real measured data? Any attached data file (CSV/JSON/a pasted table), or measured
result numbers in the prose ("achieved 0.62 HOTA", "outperforms by 3.2 points", a filled results
table — as opposed to hyperparameters, dataset sizes, or years) → CLASS (c),
data_aware. When
real numbers exist, never route them down the no-numbers proposal path; hand the data to ts-paper-data.
- Else, is it a fleshed-out proposal (problem + method + evaluation plan, all present)? → CLASS (b),
proposal.
- Else it's just an idea (a sentence, a sketch) → CLASS (a): run ts-idea2story first to grow it
into
story_proposal.md (+ retrieved_papers.json), then continue here.
- A primary input that is itself an 8-field story.json → CLASS (d).
Sidecars don't change the route: references.{json,bib} and retrieved_papers.json are just citation
seeds for the cite stage. Then write logs/0_route.io.md (the input shape, the signals you saw, the
chosen class + why). In proposal mode the suite runs exactly as the proposal path always has; in
data_aware mode the data-flow below adds steps.
DATA-AWARE data-flow (only when results_mode == "data_aware") — driven by ts-paper-data
Claude reads the data in ANY form and judges it directly (no rigid schema, no loader, no marker indirection):
route -> results_mode = data_aware
-> ts-paper-data Step 1: read the data, write results.facts.json (the real-number set = audit ground truth);
align any over-stated proposal claims to the real data inline (honest writing, no extra artifact)
(Step 1 only reads the user's raw data + writes results.facts.json; it does NOT touch
<workdir>/template.json — so the durable results_mode write happens in plan Step 0, after the cp.)
-> ts-paper-plan (data-aware): FIRST re-assert results_mode=data_aware in <workdir>/template.json (the cp reset it
to the bundled proposal default), then plan the result tables with the real metric/method keys + results figures
-> ts-paper-cite (unchanged)
-> ts-paper-write (data-aware): each result-bearing section the active template defines (abstract +
experiments always; plus any analysis / results / limitations section present in
template.sections) uses past-tense + real numbers; Claude FILLS the result tables
itself with the real numbers (no markers, no auto-filler)
-> draft_lint.py (data-aware: flags any prose decimal/percent NOT in results.facts.json)
-> ts-paper-refine (keep real numbers + past tense; cross-section number consistency)
-> ts-paper-review (adversarial hardening; data-aware = full claims-vs-evidence scrutiny)
-> ts-paper-figure (ONE owner of all figures): results plots drawn by matplotlib from results.facts.json
(figures4papers house style); free-form schematics by the image model, GROUNDED on an
on-topic top-journal MAIN figure + enforced vision critique; every figure
then vectorized to an embedded editable PDF (matplotlib born-vector; raster -> ts-figure-optimize) -> ts-paper-latex
Pipeline (run in order; each stage is a sub-skill you invoke or follow directly)
- ts-paper-plan — proposal →
blueprint.json (title, ≤6 keywords, 3 contributions, notation, terminology, experiment design, per-section plans with word targets). ONE reasoning pass.
- ts-paper-cite — build
refs.bib of only real, fully-specified papers: use the user's references first; use WebSearch/WebFetch broadly to reach a well-read ~40–50 real refs (floor 40, enforced); read each candidate's abstract to triage relevance AND decide its section; never emit a title-only stub or off-topic filler.
- ts-paper-write — draft every section as LaTeX body in
sections/<id>.tex (+ abstract.tex), citing the real bibkeys, following the per-section recipes and the no-fabrication rule. Write ALL sections in one pass.
- ts-paper-refine — one holistic pass: right-size each section to the template's word bands (do NOT over-compress), enforce term consistency, run the de-AI pass (scrub AI tells — these drafts are AI-written) + a logic self-check, fix the linter's findings.
- ts-paper-review (adversarial hardening — distilled from PaperJury; runs by default) — argue the OTHER side before finalizing: N isolated reviewers critique the whole draft (verbatim-quote anti-skim) → adversarial-verify each issue → loop-until-dry → triage. Valid issues are fixed back through
ts-paper-refine (each bound to its close_criterion) and re-linted; author-required ones are surfaced. Cost-tiered (lean / cheapest / thorough). Engine-agnostic: runs by default via the best available execution tier (Workflow → subagents → in-context); the algorithm and output are identical across tiers, so it is never skipped merely because the Workflow tool is absent. May be skipped ONLY when the user explicitly requests a quick/no-review draft — record that choice in logs/0_route.io.md.
- ts-paper-figure — fill every figure placeholder via SECTION-based routing: only real measured-data results plots in the results/experiments section (data-aware mode) are drawn by matplotlib (
ts-paper-data's plot_results.py + the figures4papers house style); every other figure — architecture/pipeline/framework/flow schematics, qualitative scenes, AND concept/math-geometry illustrations — is drawn by the image model — the official PaperBanana (ts-figure-svg/engine/PaperBanana/skill/run.py) when provisioned, else gen_image.py, first GROUNDED on a real on-topic top/mid-journal MAIN figure (fetch_reference_figures.py selects it; gen_image.py --reference image-conditions the render via /images/edits, with a text→image fallback), then driven through the Claude vision-critique loop (≥2 polish rounds, enforced: check_figure_critique fails the build on an empty critique trace or missing grounding fields). Results plots need real data (data-aware only); a proposal has none. No placeholder left blank. Then redraw it natively so every figure embeds as an editable PDF (matplotlib is born-vector via finalize; an approved image-model raster is redrawn by ts-figure-svg — its design language learned, its content re-derived from the paper, repaired over ≥4 rounds against audit_svg.py, which is stdlib-only and needs no key; never a pixel trace, never the render's logic. If the redraw cannot converge, fall back to ts-figure-optimize's DrawAI HYBRID, then to keeping the high-res PNG as-is — never a flat hand-draw; the original PNG is always kept). The main free-form schematic is results-independent, so vectorize it ONCE at this proposal/first-draft figure stage — never defer to a post-experiment re-run; later experiment-phase figures are matplotlib born-vector. The figure gate is ts-figure-optimize/scripts/check_vector_pdf.py (every figure has an embedded artifact + every converted figure's SVG is a valid hybrid — editable text over the render) plus check_figure_critique (every free-form figure is a real image-model/paperbanana render, never a hand-drawn flat SVG — and a redrawn figure carries svg_rounds ≥ 4 plus a PASSING audit JSON). Vectorization must NEVER reduce quality. Optional — skip the whole stage only if there are no figures or the image model is unconfigured.
- ts-paper-latex — run the bundled
assemble_paper.py to build main.tex, apply the deterministic template-driven post-processes (caption position, merge \cite for numeric styles, canonical headings from template.json, format keywords), copy the template's .sty/.cls + assets, and compile with latexmk. Fix real compile errors in a bounded loop (≤3 tries; abort if errors increase).
ts-paper-experiment (IN-REPO experiment + repair skill) — RUNS AUTOMATICALLY after the complete first-draft main.pdf is produced (all gates green). This skill lives in this same suite (../ts-paper-experiment/) and is driven automatically as the final stage: it diagnoses research logic, runs FEASIBLE experiments, fills the experiment tables, and re-compiles — turning the no-results draft into a results-bearing manuscript (or, if no real data/code is available, a requirements report — never fabricated numbers). See "Stage 8" below.
Stage 8 — experiments + repair (IN-REPO ts-paper-experiment) — AUTO-RUN, not optional
Stages 0–7 produce a complete first-draft paper (proposal mode). Stage 8 then runs AUTOMATICALLY as the
final stage — you do not wait for the user to ask. The earlier stages already locked the experiment
DESIGN (blueprint + claims map), so Stage 8 just executes it. Flow (Claude drives it end-to-end):
# 1) stage the run into a clean experiment workspace (manuscript + figures + sty + any real data/code)
python scripts/handoff_to_experiments.py --workdir <ts_paper_run> # -> <ts_paper_run>/experiments/
# 2) run the in-repo ts-paper-experiment skill FROM that workspace (cd <ts_paper_run>/experiments)
# it auto-proceeds (Embedded Stage-8 mode): input/draft is staged, no zip, Overleaf off, no stop-and-ask
# 3) flow results back: copy the repaired sections/main + filled tables back into <ts_paper_run>, recompile
handoff_to_experiments.py seeds experiments/ from the ts-paper-experiment/paper_config.yaml template and
copies a CLEAN minimal manuscript into experiments/input/draft/ (main.tex, sections/, refs.bib,
figures/, template .sty/.cls/.bst — needed to re-compile; no template.json/config noise), plus
input/data/ and input/code/ if the run has real experiment inputs. Then ts-paper-experiment (Embedded
Stage-8 mode) ingests input/draft/ into ./paper/, diagnoses research logic, runs only FEASIBLE
experiments (real data/code only — non-negotiable no-fabrication rules), fills result tables with
measured numbers, keeps the paper claim–evidence consistent, and re-compiles. If no real data/code is
present, it writes a requirements report and leaves tables in proposal form — it NEVER invents results.
Finally, copy the repaired sections/ + main.tex (+ refreshed tables) back into <ts_paper_run> and
recompile via ts-paper-latex so the run's main.pdf reflects the results.
Overleaf is OFF by default (paper_config.yaml: require_overleaf_url=false) → no account needed. To
sync to Overleaf, set require_overleaf_url=true + push_* flags and provide OVERLEAF_GIT_URL/
OVERLEAF_TOKEN in the workspace ./.env (also in the repo-root .env.example).
Why here (timing): the main schematic is vectorized once at Stage 6 (results-independent); experiments
come after a complete draft exists and only add results (+ matplotlib born-vector results plots), so the
expensive figure vectorization is never re-run by the experiment loop.
Per-step traceability (so every stage's input/output is visible)
The original product logged every model call's exact input+output to llm_calls/*.json. This suite runs
inline, so instead each stage writes a logs/<n>_<stage>.io.md artifact with three labeled blocks —
INPUT (files/text it consumed, with paths), DECISIONS (the judgment calls it made), OUTPUT
(artifact path + a short excerpt). After the final stage, write logs/index.md linking them all. This is
instructed capture, not byte-exact API logging, but it gives a faithful, inspectable per-step trail:
logs/0_route.io.md 1_plan.io.md 2_cite.io.md 3_write.io.md 4_refine.io.md 5_review.io.md 6_figure.io.md 7_latex.io.md index.md
(5_review.io.md is present whenever review ran — record an opt-out in 0_route.io.md otherwise; 6_figure.io.md only when the figure stage runs. Conditional/optional logs are appended to index.md when they run: data.io.md only when results_mode == data_aware, plus idea2story.io.md, kg_build.io.md, novelty.io.md for the upstream blocks.)
Each stage also leaves its hard artifact (blueprint.json, refs.bib + claims_map.json, sections/*.tex, main.tex/pdf), which IS that stage's output.
The quality stack (one coherent system, four complementary layers)
Quality is not one check; it is layered, and each layer does what it is best at — Claude does the
judgement, code is the deterministic backstop:
- Deterministic gates (code) — fail the build, don't proceed past a red gate.
template_lint +
blueprint_lint (plan); citations_lint + claims_map.json (cite/write — real, complete, justified
refs); draft_lint (write/refine — section shape, word bands, no-fabrication / data-aware number-audit,
figure floor, and the ai_tell AI-phrase gate); the latexmk error_count gate (latex). Upstream,
story_lint (idea2story) and kg_lint (kg-build). "It looked fine" is not acceptance; a green linter is.
Run these gates through scripts/run_gates.py <workdir> <stage|all> — the single place that consumes the
linters' exit codes and stops on the first red gate (nonzero exit = a hard stop, do not proceed past it).
- Self-review (Claude, in-pass) — refine. Right-size, term/coherence consistency, the de-AI pass
(these drafts are AI-written, so scrub the tells the gate can't catch with taste) and a logic
self-check after each edit.
- Adversarial review (Claude, default) —
ts-paper-review. The one thing self-review can't do:
argue the other side. Isolated reviewers + verbatim-quote anti-skim + adversarial-verify + loop-until-dry;
valid issues fixed back through refine. Runs by default before finalizing via the best available
execution tier (Workflow → subagents → in-context — identical algorithm/output, never skipped for a
missing Workflow tool); skip only when the user explicitly requests a no-review quick draft.
- Vision critique (Claude, figures) —
ts-paper-figure (+ ts-figure-optimize). Each free-form figure
is first GROUNDED on a real on-topic top/mid-journal MAIN figure (retrieved + image-conditioned), then
Claude Reads the render and critiques it (faithfulness/richness/readability/aesthetics) over ≥2
ENFORCED rounds (the check_figure_critique gate); only results-section matplotlib data plots bypass this
(deterministic matplotlib + the house style). The same
layer then vectorizes every figure to an editable PDF — matplotlib born-vector; an image-model
raster redrawn as a native SVG by ts-figure-svg (design language learned from the render, content
re-derived from the paper, ≥4 audited repair rounds), falling back to ts-figure-optimize's DrawAI HYBRID
and then to the kept PNG, gated by check_vector_pdf.py — editability is a quality gain, never a loss.
Layers 1, 2, and 3 run by default; layer 3 (adversarial review) may be skipped only on explicit user request for a quick draft; layer 4 runs when there are free-form figures.
Efficiency (a by-product, never a goal that hurts quality)
The original product made ~145 LLM calls largely for orchestration overhead; you can match its
quality in far fewer turns because you hold the whole paper in context (coherence, anti-repetition,
and term-consistency come for free). Typical shape: plan ≈1 turn, cite ≈1 pass + a few searches,
write ≈1 pass, refine ≈1 pass, figure ≈1 critique loop per figure (if any), assemble ≈1 script run + 0–2
fix turns — but always add passes when quality needs them (more citation searches, a second review,
another refine). Don't pad with busywork; don't cut a quality step to save a turn.
Definition of done
main.pdf exists and is non-trivial, the log shows zero LaTeX errors, main.bbl resolved all citations, every \cite{} maps to a complete refs.bib entry (no stubs/orphans), and no fabricated numbers appear in any sentence. Every figure is editable (figures/<label>.pdf embedded, with the original .png kept) — matplotlib plots are born-vector; an image-model schematic is a hybrid (the exact approved render as a whole-canvas <image> + an editable <text> overlay), so richness is preserved 1:1 while labels stay editable; if DrawAI was unavailable the approved PNG is kept as-is (never a lossy redraw or flat hand-draw — check_figure_critique blocks any non-image-model free-form figure). The adversarial review stage ran (logs/5_review.io.md exists, recording which execution tier ran) with every surviving blocker/major issue either closed (re-linted) or surfaced to the user as author-required. Review absence is acceptable only via an explicit user opt-out recorded in logs/0_route.io.md — never because no Workflow/subagent tool was available (in that case the in-context tier runs).
Before reporting done, run python scripts/run_gates.py <workdir> all (see the quality stack): it re-runs citations_lint.py and draft_lint.py against the final sections/*.tex, asserts every figure has an embedded artifact AND that every converted figure's .svg is a valid hybrid (editable text over the render) (the ts-figure-optimize/scripts/check_vector_pdf.py gate, manifest-driven), plus check_figure_critique (every free-form figure is engine=image-model — blocks the hand-drawn flat-SVG regression), and asserts the latex verdict (error_count == 0 / main.bbl resolved). A nonzero exit means NOT done — fix and re-run; do not emit the done report on a nonzero exit. Report: page count, section list, reference count (all complete), the review outcome (issues found / closed / author-required, plus the tier used), the figures (all editable vector PDFs), and the rough turn/token cost.
1---2name: ts-paper3description: Generate a complete, publication-format journal/conference paper (LaTeX → compiled PDF) from whatever the user drops — a one-line idea, a proposal, or a proposal WITH real results. It ROUTES the input (idea→idea2story; proposal→proposal mode; results-present→data-aware mode), then runs plan→cite→write→refine→review→figure→latex using Claude's native abilities (reasoning, WebSearch, vision, file I/O, running Python) instead of a heavyweight per-call pipeline. Template-agnostic (ts_iieta + neurips bundled). Use for "write the paper", "proposal to paper", "turn this idea/report into a paper". Integrity is absolute: in proposal mode never fabricate numbers; in data-aware mode every number must trace to the user's real data.4---56# ts-paper — Traitement du Signal paper generator (orchestrator)78You produce a publication-format paper (in whatever **template** the user picks) from a dropped input9by first **routing it** (Stage 0) and then orchestrating seven focused sub-skills (the figure stage10delegates its editable-vector output to **ts-figure-svg** — PaperBanana renders, then the figure is redrawn11as a native audited SVG; falling back to ts-figure-optimize's DrawAI HYBRID, then to keeping the approved12PNG as-is, never a lossy redraw — a 7-stage chain, not 8 stages).1314## Priority: QUALITY FIRST, cost second15The goal is a paper that reads like a real, well-written TS journal article with real, complete,16correctly-matched citations. **Never trade quality for cost.** Do every quality step — verify each17citation, self-review the draft, run the linters, right-size and polish, fix every compile error.18Cost is kept reasonable as a *by-product* of Claude doing more per turn (one holistic pass instead of19hundreds of micro-calls) — **not** by skipping reviews, shortening sections, or accepting a weaker20draft. If a quality step needs another pass, take it.2122## The full suite (idea → figured paper) — where this orchestrator sits23This skill is the **story2paper == proposal2paper** spine. Two NEW upstream blocks are optional:24```25[corpus.jsonl] ─▶ ts-kg-build (NEW, optional) ─▶ kg/ build a research-pattern KG (best-effort)26[raw idea] ─▶ ts-idea2story (NEW) ─▶ story_proposal.md + retrieved_papers.json27 (uses kg/ if present; searches the web)28[story_proposal.md OR a user's own proposal] ─▶ ts-paper (THIS) ─▶ plan→cite→write→refine→review→figure→latex ─▶ main.pdf29 ( review = adversarial hardening, runs by default; user may opt out for a quick draft )30 ( figure ends every figure as an editable VECTOR PDF — matplotlib born-vector; image-model raster → ts-figure-optimize reconstruction )31```32- If the user hands you a **raw idea**, run **ts-idea2story** first → it writes `story_proposal.md`33 (feed it here as the proposal) **and** `retrieved_papers.json` (the cite stage reuses it, so its34 search is much lighter — verify-the-seed + fill-the-gaps instead of a cold sweep).35- If the user hands you a **proposal**, start here directly (skip the upstream blocks).36- **ts-kg-build** only matters if you want KG-grounded recall in idea2story; it needs a user-supplied37 embedding endpoint and is honestly best-effort (TS already ships a built KG).3839## Inputs40- A **proposal** (markdown/text): problem, gap, proposed method/solution, evaluation plan, claimed contributions.41- Optional **references** the user already has (BibTeX, DOIs, or a `references.json` with title/authors/year/venue/doi).42- An optional **template name** (default `ts_iieta`). The suite is **template-agnostic** — see below.43- Optional **`retrieved_papers.json`** (the citation seed produced by `ts-idea2story`, if the upstream idea→story stage ran) — the cite stage reuses it.44- **Integrity (mode-dependent, overrides everything).** In **proposal** mode there is no real data: **never invent results/metrics/percentages**; result tables stay blank (`--`); prose is forward-looking ("we evaluate… we expect…"). In **data-aware** mode (the router found real results) the opposite holds: report the real numbers in past tense, and **every number must trace to the user's data** (machine-checked by the number-audit) — still never invent or round a number that wasn't measured.4546## Working directory & handoff (files on disk = the contract between stages)47Create one working dir (default `./ts_paper_run/`, or a user-named one) holding:48```49proposal.md # input50references.{json,bib} # input (optional)51blueprint.json # ← stage 1 (plan)52refs.bib # ← stage 2 (cite): complete, real BibTeX only53template.json # ← the active template spec (copied in by the plan stage)54sections/<id>.tex # ← stage 3 (write): LaTeX body per section (+ abstract.tex)55figures/<label>.pdf # ← stage 6 (figure): EMBEDDED editable vector (matplotlib born-vector;56 # image-model raster → ts-figure-optimize); .png (kept original) + source alongside57main.tex, main.pdf # ← stage 7 (assemble): compiled paper58<template assets> # the template's .sty/.cls + masthead assets (copied by assemble)59```6061## Template-agnostic (NOT locked to one venue)62The suite writes to whatever **template** the user picks; paper *content quality* is invariant.63A template is a directory `templates/<name>/` (under this skill) holding `template.json` (the64parametric spec: ordered section list + per-section recipes, word bands, citation style + floor,65title/keyword rules, caption positions, heading case, masthead) + its LaTeX `.sty`/`.cls` +66`main.tex.tmpl`. **🔴 A template's `.sty`/`.cls` must be either USER-PROVIDED or the venue's OFFICIAL files67fetched verbatim — NEVER hand-authored or approximated; if neither is available, STOP and ask the user**68(see ts-paper-latex's HARD RULE). Two **unofficial approximations** ship for demo only (`"official": false`):69`ts_iieta` (Traitement du Signal, two-column numeric — the default) and `neurips` (single-column,70author-year) — **replace with official/user files before any real submission.** To use a template, pass71`template=<name>`; the plan stage validates it (`template_lint.py`) and copies it into the workdir,72and **every downstream script reads `template.json` from the workdir** — no script hardcodes TS.73A user adds a new venue by dropping a `templates/<name>/` dir; no code changes. Word bands, shape74contracts (item/subsection/table counts), citation style/floor, and the preamble all come from the75spec. What stays **invariant across templates** = the content-quality + integrity logic (no76fabricated results, claims-map gating, no stubs, terminology consistency, Method-First order,77LaTeX safety, the figure integrity rule).7879## Version check (silent, non-blocking)80Run `python ts-paper/scripts/check_update.py` once at the start. If it prints an update notice,81show it to the user and continue — never block the pipeline for a version check.8283## Preflight — environment & keys (DO THIS FIRST, before Stage 0)84Before routing, run ONE environment + credential check and resolve gaps by this policy:85**credentials / user-data → ASK the user (and explain exactly what to configure); environment / runtime → SELF-CONFIGURE (only ask if you genuinely cannot).** First `source ./input/env.sh` (or the repo `.env`) if present — keys live in `key.txt`.86871. **Embedding + KG (for `ts-idea2story` KG-recall & novelty).** Probe `TS_EMBED_*` (a Qwen3-Embedding-8B endpoint) and whether a KG exists (`kg/`). If EITHER is missing → **ASK the user**: *"Run the KG-grounded recall / novelty part? It needs an embedding API — Qwen3-Embedding-8B (`TS_EMBED_API_KEY` / `TS_EMBED_BASE_URL` / `TS_EMBED_MODEL`) — plus a research-pattern KG (build it with `ts-kg-build`)."* If they decline (or the input skips idea2story) → **degrade gracefully to web-search-only** story (idea2story supports this; label it web/lexical-only). Never fabricate a key.882. **Image-gen key (for `ts-paper-figure`).** Probe the official PaperBanana first — `python3 skills/ts-figure-svg/scripts/setup_paperbanana.py --check-only` (it wants `OPENROUTER_API_KEY`, or `GOOGLE_API_KEY`); if that is not ready, probe `gen_image.py` / `TS_FIG_*` (a quick test render). If neither is available (`unset env: …` or a failing render) → **ASK the user**: *"Generate figures? It needs an image-model key — `TS_FIG_API_KEY` / `TS_FIG_BASE_URL` / `TS_FIG_MODEL` (e.g. gpt-image-2)."* If they decline → **skip image-model figure drawing** (every non-results figure; in proposal mode that is ALL figures — only results-section matplotlib data plots need no key); note the skipped placeholders in `logs/0_route.io.md`. Never improvise a key.893. **Environment / runtime → SELF-CONFIGURE (do NOT ask first; you have the ability).**90 - **Native SVG redraw** (`ts-figure-svg`, the default path from an approved render): nothing to provision — `audit_svg.py` is stdlib-only. Pull the official PaperBanana with `python3 skills/ts-figure-svg/scripts/setup_paperbanana.py`. An SVG→PDF backend is needed at the end (`rsvg-convert` / `inkscape` / a headless browser / `cairosvg`) — install one yourself if none is present.91 - **DrawAI vectorizer** (the FALLBACK, only when the native redraw cannot converge): if the `ts-figure-optimize` runtime is not ready (`drawai doctor` ≠ ok / models absent) → **provision it yourself** per `ts-figure-optimize`'s setup — **ModelScope source = NO HF token**, GPU: install `uv`, run `drawai setup local --source modelscope --device gpu`, then apply the documented fixes (see that skill). Only if provisioning is genuinely impossible (no GPU / no network after trying) → tell the user, and **keep the approved high-res PNG as-is** (no conversion — never a redraw or flat fallback).92 - python deps (numpy / matplotlib / python-pptx / cairosvg) and LaTeX (latexmk / TinyTeX): if missing → install them yourself.93Record the preflight outcome (what's available, what the user opted out of, what you auto-provisioned) in `logs/0_route.io.md`.9495## Stage 0 — route the dropped input (a Claude reasoning step, not a sub-skill)96The user drops **one** input and usually does **not** declare a pipeline or a mode. Before anything else,97**read the input (and any sidecar files) and classify it yourself** — a plain Claude judgement over the98content, with **no fixed schema** to match. Pick the entry stage and **set `results_mode`** in the99workdir's `template.json` — the one switch the whole downstream suite reads. Set/re-assert it **after**100the plan stage copies the template in (the `cp` resets it to the bundled `proposal` default), or as the101plan stage's first action — so the router's choice survives the copy.102103| class | what was dropped | route / entry | results_mode |104|---|---|---|---|105| (a) **bare/short IDEA** | a one-line message or a thin note, no method/eval structure | run **ts-idea2story** first → then plan | `proposal` |106| (b) **structured PROPOSAL** | problem / method / eval / contributions, **no measured results** | plan (proposal) | `proposal` |107| (c) **PROPOSAL/REPORT + REAL RESULTS** | measured numbers/tables in the text, **or** an attached data file (CSV/JSON/**any** form) | plan → **ts-paper-data** (DATA-AWARE) | `data_aware` |108| (d) **existing `story.json`** | an 8-field story from a prior idea2story run | plan (skip idea2story) | `proposal` (a story.json from idea2story is forward-looking — no results); if the user also dropped real results alongside it, that is class (c)/`data_aware` by step 1 |109110**How to judge (shape-agnostic — there is NO fixed results schema):**1111. **Is there real measured data?** Any attached data file (CSV/JSON/a pasted table), or **measured112 result numbers in the prose** ("achieved 0.62 HOTA", "outperforms by 3.2 points", a filled results113 table — as opposed to hyperparameters, dataset sizes, or years) → **CLASS (c), `data_aware`.** When114 real numbers exist, **never** route them down the no-numbers proposal path; hand the data to **ts-paper-data**.1152. Else, is it a **fleshed-out proposal** (problem + method + evaluation plan, all present)? → CLASS (b), `proposal`.1163. Else it's **just an idea** (a sentence, a sketch) → CLASS (a): run **ts-idea2story** first to grow it117 into `story_proposal.md` (+ `retrieved_papers.json`), then continue here.1184. A primary input that is itself an 8-field **story.json** → CLASS (d).119120Sidecars don't change the route: `references.{json,bib}` and `retrieved_papers.json` are just citation121seeds for the cite stage. Then write `logs/0_route.io.md` (the input shape, the signals you saw, the122chosen class + why). In `proposal` mode the suite runs exactly as the proposal path always has; in123`data_aware` mode the data-flow below adds steps.124125## DATA-AWARE data-flow (only when results_mode == "data_aware") — driven by **ts-paper-data**126Claude reads the data in ANY form and judges it directly (no rigid schema, no loader, no marker indirection):127```128route -> results_mode = data_aware129 -> ts-paper-data Step 1: read the data, write results.facts.json (the real-number set = audit ground truth);130 align any over-stated proposal claims to the real data inline (honest writing, no extra artifact)131 (Step 1 only reads the user's raw data + writes results.facts.json; it does NOT touch132 <workdir>/template.json — so the durable results_mode write happens in plan Step 0, after the cp.)133 -> ts-paper-plan (data-aware): FIRST re-assert results_mode=data_aware in <workdir>/template.json (the cp reset it134 to the bundled proposal default), then plan the result tables with the real metric/method keys + results figures135 -> ts-paper-cite (unchanged)136 -> ts-paper-write (data-aware): each result-bearing section the active template defines (abstract +137 experiments always; plus any analysis / results / limitations section present in138 template.sections) uses past-tense + real numbers; Claude FILLS the result tables139 itself with the real numbers (no markers, no auto-filler)140 -> draft_lint.py (data-aware: flags any prose decimal/percent NOT in results.facts.json)141 -> ts-paper-refine (keep real numbers + past tense; cross-section number consistency)142 -> ts-paper-review (adversarial hardening; data-aware = full claims-vs-evidence scrutiny)143 -> ts-paper-figure (ONE owner of all figures): results plots drawn by matplotlib from results.facts.json144 (figures4papers house style); free-form schematics by the image model, GROUNDED on an145 on-topic top-journal MAIN figure + enforced vision critique; every figure146 then vectorized to an embedded editable PDF (matplotlib born-vector; raster -> ts-figure-optimize) -> ts-paper-latex147```148## Pipeline (run in order; each stage is a sub-skill you invoke or follow directly)1491. **ts-paper-plan** — proposal → `blueprint.json` (title, ≤6 keywords, 3 contributions, notation, terminology, experiment design, per-section plans with word targets). ONE reasoning pass.1502. **ts-paper-cite** — build `refs.bib` of **only real, fully-specified** papers: use the user's references first; use **WebSearch/WebFetch** broadly to reach a well-read **~40–50 real refs** (floor 40, enforced); read each candidate's **abstract** to triage relevance AND decide its section; **never** emit a title-only stub or off-topic filler.1513. **ts-paper-write** — draft every section as **LaTeX body** in `sections/<id>.tex` (+ `abstract.tex`), citing the real bibkeys, following the per-section recipes and the no-fabrication rule. Write ALL sections in one pass.1524. **ts-paper-refine** — one holistic pass: right-size each section to the template's word bands (do NOT over-compress), enforce term consistency, run the **de-AI pass** (scrub AI tells — these drafts are AI-written) + a **logic self-check**, fix the linter's findings.1535. **ts-paper-review** *(adversarial hardening — distilled from PaperJury; runs by default)* — argue the OTHER side before finalizing: N isolated reviewers critique the whole draft (verbatim-quote anti-skim) → adversarial-verify each issue → loop-until-dry → triage. Valid issues are fixed **back through `ts-paper-refine`** (each bound to its `close_criterion`) and re-linted; author-required ones are surfaced. Cost-tiered (lean / cheapest / thorough). **Engine-agnostic**: runs by default via the best available execution tier (Workflow → subagents → in-context); the algorithm and output are identical across tiers, so it is **never skipped merely because the Workflow tool is absent**. May be skipped ONLY when the user explicitly requests a quick/no-review draft — record that choice in `logs/0_route.io.md`.1546. **ts-paper-figure** — fill every figure placeholder via SECTION-based routing: **only real measured-data results plots in the results/experiments section** (data-aware mode) are drawn by **matplotlib** (`ts-paper-data`'s `plot_results.py` + the figures4papers house style); **every other figure** — architecture/pipeline/framework/flow schematics, qualitative scenes, AND concept/math-geometry illustrations — is drawn by the **image model** — the official **PaperBanana** (`ts-figure-svg/engine/PaperBanana/skill/run.py`) when provisioned, else `gen_image.py`, first **GROUNDED** on a real on-topic top/mid-journal **MAIN** figure (`fetch_reference_figures.py` selects it; `gen_image.py --reference` image-conditions the render via `/images/edits`, with a text→image fallback), then driven through the Claude **vision-critique loop** (≥2 polish rounds, **enforced**: `check_figure_critique` fails the build on an empty critique trace or missing grounding fields). Results plots need real data (data-aware only); a proposal has none. No placeholder left blank. **Then redraw it natively** so every figure embeds as an editable PDF (matplotlib is born-vector via `finalize`; an approved image-model raster is redrawn by **`ts-figure-svg`** — its design language learned, its content re-derived from the paper, repaired over **≥4 rounds** against `audit_svg.py`, which is stdlib-only and needs no key; **never a pixel trace, never the render's logic**. If the redraw cannot converge, fall back to ts-figure-optimize's DrawAI HYBRID, then to keeping the high-res PNG as-is — never a flat hand-draw; the original PNG is always kept). The main free-form schematic is results-independent, so vectorize it ONCE at this proposal/first-draft figure stage — never defer to a post-experiment re-run; later experiment-phase figures are matplotlib born-vector. The figure **gate** is `ts-figure-optimize/scripts/check_vector_pdf.py` (every figure has an embedded artifact + every converted figure's SVG is a valid hybrid — editable text over the render) plus `check_figure_critique` (every free-form figure is a real image-model/paperbanana render, never a hand-drawn flat SVG — and a redrawn figure carries `svg_rounds ≥ 4` plus a PASSING audit JSON). Vectorization must NEVER reduce quality. Optional — skip the whole stage only if there are no figures or the image model is unconfigured.1557. **ts-paper-latex** — run the bundled `assemble_paper.py` to build `main.tex`, apply the deterministic **template-driven** post-processes (caption position, merge `\cite` for numeric styles, canonical headings from `template.json`, format keywords), copy the template's `.sty`/`.cls` + assets, and compile with `latexmk`. Fix real compile errors in a bounded loop (≤3 tries; abort if errors increase).1568. **`ts-paper-experiment` (IN-REPO experiment + repair skill) — RUNS AUTOMATICALLY** *after* the complete first-draft `main.pdf` is produced (all gates green). This skill lives **in this same suite** (`../ts-paper-experiment/`) and is driven automatically as the final stage: it **diagnoses research logic, runs FEASIBLE experiments, fills the experiment tables, and re-compiles** — turning the no-results draft into a results-bearing manuscript (or, if no real data/code is available, a requirements report — never fabricated numbers). See "Stage 8" below.157158## Stage 8 — experiments + repair (IN-REPO `ts-paper-experiment`) — AUTO-RUN, not optional159Stages 0–7 produce a complete first-draft paper (proposal mode). **Stage 8 then runs AUTOMATICALLY as the160final stage** — you do not wait for the user to ask. The earlier stages already locked the experiment161DESIGN (blueprint + claims map), so Stage 8 just executes it. Flow (Claude drives it end-to-end):162163```164# 1) stage the run into a clean experiment workspace (manuscript + figures + sty + any real data/code)165python scripts/handoff_to_experiments.py --workdir <ts_paper_run> # -> <ts_paper_run>/experiments/166# 2) run the in-repo ts-paper-experiment skill FROM that workspace (cd <ts_paper_run>/experiments)167# it auto-proceeds (Embedded Stage-8 mode): input/draft is staged, no zip, Overleaf off, no stop-and-ask168# 3) flow results back: copy the repaired sections/main + filled tables back into <ts_paper_run>, recompile169```170`handoff_to_experiments.py` seeds `experiments/` from the `ts-paper-experiment/paper_config.yaml` template and171copies a CLEAN minimal manuscript into `experiments/input/draft/` (`main.tex`, `sections/`, `refs.bib`,172`figures/`, template `.sty`/`.cls`/`.bst` — needed to re-compile; no template.json/config noise), plus173`input/data/` and `input/code/` if the run has real experiment inputs. Then `ts-paper-experiment` (Embedded174Stage-8 mode) **ingests `input/draft/` into `./paper/`, diagnoses research logic, runs only FEASIBLE175experiments** (real data/code only — non-negotiable no-fabrication rules), **fills result tables** with176measured numbers, keeps the paper **claim–evidence consistent**, and re-compiles. **If no real data/code is177present, it writes a requirements report and leaves tables in proposal form — it NEVER invents results.**178Finally, copy the repaired `sections/` + `main.tex` (+ refreshed tables) back into `<ts_paper_run>` and179recompile via ts-paper-latex so the run's `main.pdf` reflects the results.180181**Overleaf is OFF by default** (`paper_config.yaml: require_overleaf_url=false`) → no account needed. To182sync to Overleaf, set `require_overleaf_url=true` + `push_*` flags and provide `OVERLEAF_GIT_URL`/183`OVERLEAF_TOKEN` in the workspace `./.env` (also in the repo-root `.env.example`).184185**Why here (timing):** the main schematic is vectorized once at Stage 6 (results-independent); experiments186come after a complete draft exists and only add results (+ matplotlib born-vector results plots), so the187expensive figure vectorization is never re-run by the experiment loop.188189## Per-step traceability (so every stage's input/output is visible)190The original product logged every model call's exact input+output to `llm_calls/*.json`. This suite runs191inline, so instead **each stage writes a `logs/<n>_<stage>.io.md` artifact** with three labeled blocks —192**INPUT** (files/text it consumed, with paths), **DECISIONS** (the judgment calls it made), **OUTPUT**193(artifact path + a short excerpt). After the final stage, write `logs/index.md` linking them all. This is194*instructed* capture, not byte-exact API logging, but it gives a faithful, inspectable per-step trail:195```196logs/0_route.io.md 1_plan.io.md 2_cite.io.md 3_write.io.md 4_refine.io.md 5_review.io.md 6_figure.io.md 7_latex.io.md index.md197```198(`5_review.io.md` is present whenever review ran — record an opt-out in `0_route.io.md` otherwise; `6_figure.io.md` only when the figure stage runs. Conditional/optional logs are appended to `index.md` when they run: `data.io.md` only when `results_mode == data_aware`, plus `idea2story.io.md`, `kg_build.io.md`, `novelty.io.md` for the upstream blocks.)199Each stage also leaves its hard artifact (blueprint.json, refs.bib + claims_map.json, sections/*.tex, main.tex/pdf), which IS that stage's output.200201## The quality stack (one coherent system, four complementary layers)202Quality is not one check; it is layered, and each layer does what it is best at — **Claude does the203judgement, code is the deterministic backstop**:2041. **Deterministic gates (code) — fail the build, don't proceed past a red gate.** `template_lint` +205 `blueprint_lint` (plan); `citations_lint` + `claims_map.json` (cite/write — real, complete, justified206 refs); `draft_lint` (write/refine — section shape, word bands, no-fabrication / data-aware number-audit,207 figure floor, and the **`ai_tell`** AI-phrase gate); the latexmk `error_count` gate (latex). Upstream,208 `story_lint` (idea2story) and `kg_lint` (kg-build). "It looked fine" is not acceptance; a green linter is.209 Run these gates through `scripts/run_gates.py <workdir> <stage|all>` — the single place that consumes the210 linters' exit codes and **stops on the first red gate** (nonzero exit = a hard stop, do not proceed past it).2112. **Self-review (Claude, in-pass) — refine.** Right-size, term/coherence consistency, the **de-AI pass**212 (these drafts are AI-written, so scrub the tells the gate can't catch with taste) and a **logic213 self-check** after each edit.2143. **Adversarial review (Claude, default) — `ts-paper-review`.** The one thing self-review can't do:215 argue the *other* side. Isolated reviewers + verbatim-quote anti-skim + adversarial-verify + loop-until-dry;216 valid issues fixed back through refine. Runs by default before finalizing via the best available217 execution tier (Workflow → subagents → in-context — identical algorithm/output, never skipped for a218 missing Workflow tool); skip only when the user explicitly requests a no-review quick draft.2194. **Vision critique (Claude, figures) — `ts-paper-figure` (+ `ts-figure-optimize`).** Each free-form figure220 is first **GROUNDED** on a real on-topic top/mid-journal MAIN figure (retrieved + image-conditioned), then221 Claude `Read`s the render and critiques it (faithfulness/**richness**/readability/aesthetics) over ≥2222 ENFORCED rounds (the `check_figure_critique` gate); only results-section matplotlib data plots bypass this223 (deterministic matplotlib + the house style). The same224 layer then **vectorizes** every figure to an editable PDF — matplotlib born-vector; an image-model225 raster **redrawn as a native SVG by `ts-figure-svg`** (design language learned from the render, content226 re-derived from the paper, ≥4 audited repair rounds), falling back to ts-figure-optimize's DrawAI HYBRID227 and then to the kept PNG, gated by `check_vector_pdf.py` — editability is a quality gain, never a loss.228229Layers 1, 2, and 3 run by default; layer 3 (adversarial review) may be skipped only on explicit user request for a quick draft; layer 4 runs when there are free-form figures.230231## Efficiency (a by-product, never a goal that hurts quality)232The original product made ~145 LLM calls largely for orchestration overhead; you can match its233quality in far fewer turns because you hold the whole paper in context (coherence, anti-repetition,234and term-consistency come for free). Typical shape: plan ≈1 turn, cite ≈1 pass + a few searches,235write ≈1 pass, refine ≈1 pass, figure ≈1 critique loop per figure (if any), assemble ≈1 script run + 0–2236fix turns — **but always add passes when quality needs them** (more citation searches, a second review,237another refine). Don't pad with busywork; don't cut a quality step to save a turn.238239## Definition of done240`main.pdf` exists and is non-trivial, the log shows **zero LaTeX errors**, `main.bbl` resolved all citations, every `\cite{}` maps to a complete `refs.bib` entry (no stubs/orphans), and no fabricated numbers appear in any sentence. **Every figure is editable** (`figures/<label>.pdf` embedded, with the original `.png` kept) — matplotlib plots are born-vector; an image-model schematic is a **hybrid** (the exact approved render as a whole-canvas `<image>` + an editable `<text>` overlay), so richness is preserved 1:1 while labels stay editable; if DrawAI was unavailable the approved PNG is kept as-is (never a lossy redraw or flat hand-draw — `check_figure_critique` blocks any non-`image-model` free-form figure). The adversarial **review stage ran** (`logs/5_review.io.md` exists, recording which execution tier ran) with every surviving `blocker`/`major` issue either closed (re-linted) or surfaced to the user as author-required. Review absence is acceptable **only** via an explicit user opt-out recorded in `logs/0_route.io.md` — **never** because no Workflow/subagent tool was available (in that case the in-context tier runs).241242Before reporting done, run **`python scripts/run_gates.py <workdir> all`** (see the quality stack): it re-runs `citations_lint.py` and `draft_lint.py` against the final `sections/*.tex`, asserts **every figure has an embedded artifact AND that every converted figure's `.svg` is a valid hybrid (editable text over the render)** (the `ts-figure-optimize/scripts/check_vector_pdf.py` gate, manifest-driven), plus `check_figure_critique` (every free-form figure is engine=image-model — blocks the hand-drawn flat-SVG regression), and asserts the latex verdict (`error_count == 0` / `main.bbl` resolved). A nonzero exit means **NOT done** — fix and re-run; do not emit the done report on a nonzero exit. Report: page count, section list, reference count (all complete), the review outcome (issues found / closed / author-required, plus the tier used), the figures (all editable vector PDFs), and the rough turn/token cost.