You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 notSIGNOFF_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):
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, notorchestrate.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.
Task
SIGNOFF_BUTTON_MAX_CHARS = 60is currently advisory: it reaches the model as prompt text and nothing checks what comes back. Make the writer path inorchestrate.pyactually measure the button on a returnedoutroand 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:
skills/daily-podcast/orchestrate.py:805and never checked against the reply.test_fallback_buttons_fit_the_button_cap(tests/test_orchestrate.py:828) andtest_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 returnedoutro.build_plan(skills/frontier-commits/fc_script_plan.py:219) emitsbutton_angle/button_textbut notSIGNOFF_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:158—SIGNOFF_BUTTON_MAX_CHARS = 60skills/daily-podcast/orchestrate.py:805— the cap's only current use: prompt textskills/daily-podcast/orchestrate.py:780—make_intro_outro;runner(line 784) is the injectable seam a test drivesskills/daily-podcast/orchestrate.py:823-833— the accept branch where a returnedoutrois validated today; the natural home for the checkskills/daily-podcast/orchestrate.py:765—fallback_intro_outro, which already appends aFALLBACK_BUTTONSliteralskills/daily-podcast/orchestrate.py:164—FALLBACK_BUTTONSskills/frontier-commits/fc_script_plan.py:102—SIGNOFF_BUTTON_MAX_CHARS_W, not currently emitted bybuild_planskills/frontier-commits/fc_script_plan.py:219—build_plan, whose JSON contract would gain the capConstraints
make_intro_outro's documented contract is "Any failure → deterministic fallback so a run never dies here", and CLAUDE.md states the same posture formake_transitions. An over-long button must not fail a run.FALLBACK_BUTTONSare 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.orchestrate.pystays self-contained.fc_script_plan.pystays a pure module — no IO beyond theplanCLI's single JSON print, no wall-clock reads.RUN_LOG_FIELDSrule: append, and leave it null on paths that never set it.Acceptance criteria
outrowhose last sentence exceedsSIGNOFF_BUTTON_MAX_CHARSis detected on the writer path, and the run continues.runnerintomake_intro_outroreturning an over-long button and asserts the chosen behavior (warn / substitute / re-ask) — driving the writer path, not theFALLBACK_BUTTONStuple.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 toSIGNOFF_BUTTON_MAX_CHARS_W.ruffclean and the fullpytestsuite passes; report counts.Out of scope
claude -pfollowing SKILL.md, notorchestrate.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.provenancevsdownstream).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.