Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ResearchStudio-Reel/skills/paper2poster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ python ~/.claude/skills/paper2poster/scripts/check_poster.py slack \

`--with-polish` runs the fill gate (`slack`) and the visual-polish gate (`FIG/NARROW` etc.) on **one** rendered page — a single browser launch instead of two — and under `--strict` **both** must pass, so this one command replaces the old separate `slack` + `polish` calls. Do not stop iterating while it exits non-zero. `--strict` is the same measurement you read each pass, but with a hard exit code — there is no "acceptable SPARSE" or "figure too tight to fix" escape. Keep applying the modification methods (and, for a stubborn figure, the column-width nudge / vertical-room methods in `references/staged_fill.md`) until the gate passes.

**Converge fast, and bound the loop (critical for smaller models).** The `slack` report gives each off-band section a precise **`needPx`** delta — e.g. `key-result SPARSE grow +50px [+18..+83]`. Edit *by that number* with a continuous CSS lever (`margin-bottom` / `.col` gap / figure `max-height`), don't guess with whole text lines and overshoot the 0.05-wide FULL band. Track recent measurements and switch levers the instant a section ping-pongs `SPARSE`↔`SPILLAGE`. And the loop is **bounded**: if both gates aren't green after **~12 rounds / ~20 min**, render the best-measured state, mark the stage **DEGRADED** with the residual off-band section ids, and move on — never grind indefinitely. This is **script-enforced**: `slack` counts every call in `<poster_dir>/.fill_budget.json` and **exits 3 with a `CIRCUIT BREAKER` banner** once it passes `--max-iterations` (default **80**) — an on-disk cap that survives context compaction, so a lost round-count can't make you grind. Treat **exit 3 as a hard stop** (render best state, mark DEGRADED). The exit gate stays strict; only the iteration count is capped. Full rules: the **"Convergence protocol"** at the top of `references/staged_fill.md`.
**Converge fast, and bound the loop (critical for smaller models).** The `slack` report gives each off-band section a precise **`needPx`** delta — e.g. `key-result SPARSE grow +50px [+18..+83]`. Edit *by that number* with a continuous CSS lever (`margin-bottom` / `.col` gap / figure `max-height`), don't guess with whole text lines and overshoot the 0.05-wide FULL band. Track recent measurements and switch levers the instant a section ping-pongs `SPARSE`↔`SPILLAGE`. And the loop is **bounded**: if both gates aren't green after **~12 rounds / ~20 min**, render the best-measured state, mark the stage **DEGRADED** with the residual off-band section ids, and move on — never grind indefinitely. This is **script-enforced**: `slack` counts *consecutive non-converged* measurements in `<poster_dir>/assets/meta/.fill_budget.json` and **exits 3 with a `CIRCUIT BREAKER` banner** once it passes `--max-iterations` (default **80**) — an on-disk cap that survives context compaction, so a lost round-count can't make you grind. A converged measurement clears the count, and a call that produced no geometry at all (nav timeout, MathJax settle failure) doesn't count against it. Treat **exit 3 as a hard stop** (render best state, mark DEGRADED). The exit gate stays strict; only the iteration count is capped. Full rules: the **"Convergence protocol"** at the top of `references/staged_fill.md`.

**When the `.grow` section is persistently `EMPTY` or `SPARSE`** even after exhausting its own Additional/optional content, don't keep stretching that one section — instead **refine the content of the other (non-grow) sections in the same column**: lift their `Additional` paragraphs into the rendered card, promote bullets from concise to expanded form, or fold in a paper-specific custom section. The `.grow` section then absorbs the residual slack naturally instead of inflating a single card with filler.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ Weaker / smaller models stall here, and the reason is structural: the FULL band
- **Column-stack coupling (rigid `headline-numbers` ⟷ `.grow` absorber).** A landscape column often stacks the *rigid* `headline-numbers` card — its big `.hero-val` is a fixed-pt block (160pt) that pokes past the card padding when the column is tight — above `ablation` and a `.grow` `takeaway`. Micro-shrinking the hero pt (160→140→120…) is whack-a-mole: every change re-disturbs the column's other cards, so you ping-pong for dozens of rounds. Instead, **lock the rigid card ONCE**: give `headline-numbers` a `min-height` ≈ its natural hero height (so the grid can't crush it), drop the hero to a single fitting pt, then **never touch it again** — let the column's `.grow` section absorb all remaining slack. If it still paints past the card, end it with a plain `<p>` (a real caption/footnote) so a text bottom-margin seats the content inside.
- **Column-width nudges are GLOBAL — set them once, never inside the fill loop.** Resizing one outer-grid column re-flows *every* column, so a "shift width col3→col2" fix throws col2 off and you ping-pong col2⟷col3 forever (this is a distinct oscillation source from the per-section levers). Pick the column widths at render time; inside the loop, balance a section ONLY with *in-column* continuous levers (margin / `min-height` / font-size / content), never by resizing the outer columns.

**5. Hard circuit breaker — the loop is bounded, and the script enforces it.** Aim to converge within **~12 measure→edit rounds OR ~20 minutes**. That is the *soft target*; the *hard backstop* is built into `check_poster.py slack` itself. Every `slack` call increments a persistent counter in `<poster_dir>/.fill_budget.json` and, once it passes `--max-iterations` (**default 80**), `slack` prints a `CIRCUIT BREAKER` banner and **exits 3** instead of its normal verdict. The counter lives on disk, so it **survives context compaction** — you cannot reset it by losing your in-prompt round count (the exact failure mode that lets a loop grind to 100 measurements). When you see **exit 3 / the breaker banner**, stop immediately: **render the best-measured state** (fewest off-band sections / smallest total `|needPx|`), record the stage as **DEGRADED** with the residual off-band section ids in the stage note, and **move on**. A poster that is 95% there in 12 rounds beats one that burns 90 minutes chasing the last 1px. The exit gate itself stays strict (`--strict` is unchanged) — this breaker bounds only the *iteration count*, never the quality target. (For a genuine fresh re-render of the same `poster_dir`, pass `--reset-budget` once to zero the counter; set `--max-iterations 0` only to disable the backstop deliberately.)
**5. Hard circuit breaker — the loop is bounded, and the script enforces it.** Aim to converge within **~12 measure→edit rounds OR ~20 minutes**. That is the *soft target*; the *hard backstop* is built into `check_poster.py slack` itself. Every `slack` call that measures the poster and finds it **not yet converged** increments a persistent counter in `<poster_dir>/assets/meta/.fill_budget.json` and, once it passes `--max-iterations` (**default 80**), `slack` prints a `CIRCUIT BREAKER` banner and **exits 3** instead of its normal verdict. The counter lives on disk, so it **survives context compaction** — you cannot reset it by losing your in-prompt round count (the exact failure mode that lets a loop grind to 100 measurements). It counts **consecutive** non-converged rounds: a measurement where every section is FULL and every figure OK clears it (and, under `--with-polish --strict`, only if the polish gates passed too — a poster that is FULL everywhere but keeps failing e.g. `ORPHAN` is still a loop the breaker must bound), so a later touch-up on an already-converged poster starts from a clean budget rather than inheriting the debt of the rounds that got it there. A call that never produced geometry (nav timeout, MathJax settle failure, no columns found) is an environment problem and does not count against you, and a counter left idle for over 12h is treated as a previous session and dropped. When you see **exit 3 / the breaker banner**, stop immediately: **render the best-measured state** (fewest off-band sections / smallest total `|needPx|`), record the stage as **DEGRADED** with the residual off-band section ids in the stage note, and **move on**. A poster that is 95% there in 12 rounds beats one that burns 90 minutes chasing the last 1px. The exit gate itself stays strict (`--strict` is unchanged) — this breaker bounds only the *iteration count*, never the quality target. (For a genuine fresh re-render of the same `poster_dir`, pass `--reset-budget` once to zero the counter; set `--max-iterations 0` only to disable the backstop deliberately.)

**6. Context discipline — do NOT re-`Read` the whole `poster.html` each round.** It's ~100 KB (~25–30k tokens); pulling it into context every round floods a smaller model's window and triggers **auto-compaction**, which wipes your precise per-section fill state — so the loop forgets what it already tried and **thrashes without ever converging** (measured: a 200 K-window model auto-compacted twice and never converged on papers a large-window model finished compaction-free). Each round needs only the **`slack` report**, which is now self-sufficient: besides each section's verdict + `needPx`, it prints an **`EDIT TARGETS`** block carrying the **verbatim source of every off-band section**. Lift your `Edit` `old_string` straight from that block — a short, unique sub-snippet (a bullet's words, a stat value, a `margin-bottom`) — and grow/shrink the section by `needPx` without ever opening `poster.html`. If you somehow need markup the report didn't surface, `Read` a **narrow `offset`/`limit` slice** or `grep` for it — never the whole file. Beyond the one initial orientation read, re-reading the full `poster.html` is the single biggest avoidable context cost in this loop; on a small-context model it is the difference between converging and thrashing.

Expand Down
32 changes: 26 additions & 6 deletions ResearchStudio-Reel/skills/paper2poster/scripts/check_poster.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,19 @@ def build_parser() -> argparse.ArgumentParser:
)
ps.add_argument(
"--max-iterations", type=int, default=80,
help="script-enforced circuit breaker: persist a per-poster "
"measurement count in <poster_dir>/.fill_budget.json; once it "
"exceeds this cap, slack prints a STOP banner and exits 3 "
"(survives context compaction, unlike an in-prompt round count). "
"Default 80. Set <=0 to disable.",
help="script-enforced circuit breaker: persist a per-poster count of "
"CONSECUTIVE non-converged measurements in "
"<poster_dir>/assets/meta/.fill_budget.json; once it exceeds this "
"cap, slack "
"prints a STOP banner and exits 3 (survives context compaction, "
"unlike an in-prompt round count). A converged measurement clears "
"the count, a run that produced no geometry (nav timeout, MathJax "
"settle failure, no columns found) does not count, and a state "
"file idle >12h is dropped. Default 80. Set <=0 to disable.",
)
ps.add_argument(
"--reset-budget", action="store_true",
help="zero the persistent .fill_budget.json counter before measuring "
help="clear the persistent .fill_budget.json counter before measuring "
"(use for a genuine fresh re-render of the same poster_dir).",
)
ps.add_argument(
Expand Down Expand Up @@ -201,6 +205,22 @@ def build_parser() -> argparse.ArgumentParser:
"to 0.05 to match the slack gate's 5pt FULL band — anything "
"looser hides 5-9%% trailing voids the eye still sees",
)
ppl.add_argument(
"--max-card-inner-void", type=float,
default=_polish.DEFAULT_MAX_CARD_INNER_VOID,
help="warn (CARD/INNER-VOID) if a card has an inter-child gap "
"exceeding this fraction of its height "
"(default 0.15); catches a void in the MIDDLE of a card, "
"which CARD/TRAILING cannot see because a bottom-pinned "
"tail drives trailing to ~0",
)
ppl.add_argument(
"--min-card-inner-void-px", type=float,
default=_polish.DEFAULT_MIN_CARD_INNER_VOID_PX,
help="CARD/INNER-VOID also requires the gap to exceed this many "
"px (default 24) so a sub-line gap on a small card cannot "
"trip the gate",
)
ppl.add_argument(
"--max-widow-fraction", type=float,
default=_polish.DEFAULT_MAX_WIDOW_FRACTION,
Expand Down
Loading