Skip to content

Enforce the 60-char sign-off button cap on the writer path #224

Description

@schmug

Task

SIGNOFF_BUTTON_MAX_CHARS = 60 is currently advisory: it reaches the model as prompt text and nothing checks what comes back. Make the writer path in orchestrate.py actually measure the button on a returned outro and react when it busts the cap, so the design claim behind the whole feature — "a button lands; a second sentence explains the joke" — is held up by code rather than by hope. After this, an over-long or multi-sentence button is detectable in the run log instead of silently shipping as a weaker close.

Context

PR #223 replaced the daily show's "I'm <host_name>" close with a button: one dry joke about the host being a machine, with the angle assigned from a bank (day % 5) and the wording written fresh by the model each day.

The 60-char cap is the load-bearing constraint on that design — it is what separates a button from a bit. But it is enforced nowhere:

  • It is interpolated into the writer prompt at skills/daily-podcast/orchestrate.py:805 and never checked against the reply.
  • The two tests that assert the cap — test_fallback_buttons_fit_the_button_cap (tests/test_orchestrate.py:828) and test_burned_buttons_fit_the_button_cap (tests/test_fc_script_plan.py:137) — check the burned/fallback literals, which are hardcoded and can only fail if someone edits the tuple. Neither exercises the writer's returned outro.
  • On the Frontier path it is looser still: build_plan (skills/frontier-commits/fc_script_plan.py:219) emits button_angle / button_text but not SIGNOFF_BUTTON_MAX_CHARS_W, so that writer only ever learns the number from SKILL.md prose.

This was noticed while auditioning the six button angles through TTS against PR #223's spec (all six hand-written lines happened to land 36-55 chars, so nothing was caught by luck rather than by a check).

Motivation

As the show's operator, I want an over-long button to show up as a signal I can see, so that the button constraint degrades visibly instead of silently — the same posture #219 took for title-less segments.

What triggered it now: PR #223 is open and green, and this is the moment to decide whether the cap is a rule or a suggestion, before episodes start shipping against it.

Pointers

All line numbers are on the PR branch claude/podcast-signoff-revision-3xjloy (this issue depends on #223 landing first):

  • skills/daily-podcast/orchestrate.py:158SIGNOFF_BUTTON_MAX_CHARS = 60
  • skills/daily-podcast/orchestrate.py:805 — the cap's only current use: prompt text
  • skills/daily-podcast/orchestrate.py:780make_intro_outro; runner (line 784) is the injectable seam a test drives
  • skills/daily-podcast/orchestrate.py:823-833 — the accept branch where a returned outro is validated today; the natural home for the check
  • skills/daily-podcast/orchestrate.py:765fallback_intro_outro, which already appends a FALLBACK_BUTTONS literal
  • skills/daily-podcast/orchestrate.py:164FALLBACK_BUTTONS
  • skills/frontier-commits/fc_script_plan.py:102SIGNOFF_BUTTON_MAX_CHARS_W, not currently emitted by build_plan
  • skills/frontier-commits/fc_script_plan.py:219build_plan, whose JSON contract would gain the cap
  • Precedent for the degrade-and-warn posture: fix(render): warn on a title-less segment instead of publishing "Segment N" (#96) #219 (warn on a title-less segment instead of publishing "Segment N")

Constraints

  • Degrade, never die. make_intro_outro's documented contract is "Any failure → deterministic fallback so a run never dies here", and CLAUDE.md states the same posture for make_transitions. An over-long button must not fail a run.
  • Do not truncate the joke. Cutting a punchline at 60 chars produces a worse line than the one it replaced. Either keep it and warn, or swap the whole button.
  • Do not silently ship a burned line on the writer path. FALLBACK_BUTTONS are SKILL.md's calibration examples; the no-model path shipping them is deliberate, but the writer path substituting one turns "burned" into "shipped" and reintroduces the identical-close problem feat: close the sign-off on a machine joke instead of the host's name #223 exists to kill. If substitution is the chosen degrade, say so explicitly in SKILL.md.
  • No new dependencies; orchestrate.py stays self-contained.
  • fc_script_plan.py stays a pure module — no IO beyond the plan CLI's single JSON print, no wall-clock reads.
  • If a new run-log-ish field is added anywhere, follow the RUN_LOG_FIELDS rule: append, and leave it null on paths that never set it.

Acceptance criteria

  • A returned outro whose last sentence exceeds SIGNOFF_BUTTON_MAX_CHARS is detected on the writer path, and the run continues.
  • A test injects a fake runner into make_intro_outro returning an over-long button and asserts the chosen behavior (warn / substitute / re-ask) — driving the writer path, not the FALLBACK_BUTTONS tuple.
  • A test asserts the in-cap case is untouched: a compliant button passes through byte-identical.
  • build_plan's JSON carries the cap (e.g. button_max_chars) so the Frontier writer gets the number from the plan rather than from prose alone, with a drift test tying it to SIGNOFF_BUTTON_MAX_CHARS_W.
  • ruff clean and the full pytest suite passes; report counts.

Out of scope

  • The SKILL.md production path. Per CLAUDE.md, the scheduled daily run is a claude -p following SKILL.md, not orchestrate.py — so a check added here does not govern real episodes. This issue closes the code path only; whether the production path can be mechanically governed at all (render.py cannot tell which sentence is the button) is a separate question and should be filed separately if wanted.
  • Changing the cap's value, the angle banks, or any button wording.
  • The daily/Frontier bank divergence (provenance vs downstream).
  • Enforcing the other button rules (no exclamation marks, one sentence, no new facts) — pick those up only if they fall out for free.

Reasoning guidance

The mechanical change is small; the judgement call is what "detected" should do, and the three options (warn-only, substitute a fallback, re-ask once) have real tradeoffs against the burned-lines rule. Think that choice through before writing code, and state the option chosen in the PR description.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions