--- name: synthesize description: Thesis report synthesis — turn a thesis's markdown artifacts into its letter-size thesis-report.html, then OPTIMIZE it against real page renders. Four-step loop — pack (deterministic Python bundle of all sources + report/metrics.json), author (the LLM writes report/content.html — narrative, KPI tiles, badges, timeline, tables and citations are LLM judgment), assemble (deterministic validation, template injection, Q50 pins, Q47 overflow gate), render + optimize (Chrome headless per-page PNGs — the LLM reads them and iterates until every page is visually clean). Spec 046 Q46–Q50. role: kit market_data_stage: none allowed_tools: [] retrieval_scope: structured_only --- # agentii.synthesize The single-point HTML generation step (spec 046 Q46–Q50): ONE `thesis-report.html` per thesis, authored from the markdown artifacts and then optimized against real renders. Analysis skills emit markdown only (Q49); the report is assembled here, at the synthesis step, after the cross-stock synthesis (`_cross/*_synthesis.md`) exists. ## When to run - After the synthesis tasks complete (the `_cross/` deliverable is written). - When `converge` emits an `html_stale` finding (sources or template moved). - On explicit request, or once at thesis completion (Q50 regeneration triggers). ## The loop (four steps + optimize) ```bash # 0. RESOLVE the kit root — contracts/kit-root.md. The kit's scripts do NOT ship with # the skills, so never assume the CWD is the checkout (`cd agentii-investment- # intelligence` was this skill's old step 0, and it only worked for a user sitting # in the checkout). This resolver fails loudly rather than guessing: KIT="" for c in "${AGENTII_KIT_ROOT:-}" \ "$(cat "$HOME/.claude/skills/agentii/.kit-root" 2>/dev/null)" \ "${CLAUDE_PLUGIN_ROOT:-}"; do [ -n "$c" ] && [ -f "$c/scripts/agentii_cmd.py" ] && { KIT="$c"; break; } done if [ -z "$KIT" ]; then d="$PWD"; while [ "$d" != "/" ]; do [ -f "$d/scripts/agentii_cmd.py" ] && { KIT="$d"; break; }; d="$(dirname "$d")"; done; fi [ -n "$KIT" ] || { echo "agentii kit not found — see contracts/kit-root.md" >&2; exit 1; } # 1. PACK — deterministic bundle of every source, verbatim (no timestamps), # PLUS report/metrics.json (per-ticker key_metrics/conclusions/counts — # machine-ready numbers for KPI tiles and kpi_trend charts, RAW values): python3 "$KIT/scripts/synthesize_report.py" pack --thesis # 2. AUTHOR — read /report-input.md (verbatim sources + citations) # and /report/metrics.json (numbers). Write # /report/content.html — the report's actual content is YOUR judgment. # 2b. SCORE (required — spec 058 FR-065). The blocking gate decides what TEXT can # decide; it cannot see whether the report is any good, and "passes the gate" # must never be read as "reads well". Run the scorer for the RUBRIC and the # MEASURED evidence — the judgement is yours, the measurement is not: python3 "$KIT/scripts/score_report_readability.py" /report/content.html # Then read the report and record your total (five criteria, 1–5 each → 1–25) # in /report/readability.json: # {"score": 21, "previous": 18, "explanation": ""} # `previous` is the last RELEASED report's score; omit it (or write null) for a # first report, which sets its own baseline — no score is chosen in advance. # A score BELOW `previous` requires `explanation`, and `assemble` REFUSES the # drop without one: FR-065's consequence is that a regression is explained in # the deliverable, and a score recorded without consequence is the defect # FR-040 forbids. A LOW score is not refused — only an unexplained regression. # The score is rendered on the cover beside the pins (T091), so it has a named # consumer: the human reviewer. # 3. ASSEMBLE — validate + inject + gate (advisories on stderr are guidance, # the hard gates are silent until they fail): python3 "$KIT/scripts/synthesize_report.py" assemble --thesis --check-only # fit loop python3 "$KIT/scripts/synthesize_report.py" assemble --thesis # deliver # 4. RENDER — Chrome headless → letter PDF → per-page PNGs + manifest. # --keep-pdf is REQUIRED for delivery: without it the PDF is written to a temp dir, # used for the PNGs, and DELETED. The deliverable is HTML **and** PDF. python3 "$KIT/scripts/render_report.py" render --thesis --keep-pdf # 5. OPTIMIZE (required, not optional): READ the PNGs page by page, in batches # of 3–4. Fix real problems the estimator cannot see — clipped tables, ugly # URL wrapping, weak density, orphan headings, oversized tiles. Edit # content.html → re-assemble → re-render until EVERY page is visually clean. ``` ## Deliverables Two files, both at the thesis root — report the absolute paths when you finish: - `/thesis-report.html` - `/thesis-report.pdf` The PDF is the deliverable, not a QA by-product. `render`'s docstring frames it as the "visual QA loop" because the PNGs are what the optimize pass reads — but `--keep-pdf` is what turns its `--print-to-pdf` output into the artifact a user actually sends to someone, and it belongs beside the HTML rather than in the `report/pages/` scratch folder with the PNGs. Renders never gate CI (Chrome/poppler may be absent) — **the author's own visual pass is the gate**. `assemble --check-only` remains the estimator fallback; a missing render is a hard stop for delivery, not a silent skip. `render --verify` also checks each PNG is letter-width at the requested dpi. ## content.html contract (the assembler hard-gates every rule) You author **only the page sequence** — a fragment, never a document: 1. One or more `
…
` blocks, no nesting. The cover is page 1 and is template-owned: **do not author it** — the assembler fills kicker / title / claim / pins / universe / generated / TOC, and injects the running sheet head/foot, page marks (`NN / TOTAL`) and corner registration marks into every page. 2. Fragment only — ``, ``, ``, ``, `