music on atproto
plyr.fm
1# status maintenance via claude code
2#
3# two-phase workflow:
4# 1. workflow_dispatch: archives old STATUS.md sections, generates audio, opens PR
5# 2. on PR merge: uploads audio to plyr.fm
6#
7# required secrets:
8# ANTHROPIC_API_KEY - claude code
9# GOOGLE_API_KEY - gemini TTS (for audio generation)
10# PLYR_BOT_TOKEN - plyr.fm developer token (for audio upload)
11
12name: status maintenance
13
14on:
15 # TODO: restore schedule after testing
16 # schedule:
17 # - cron: "0 9 * * 1" # every monday 9am UTC
18 workflow_dispatch:
19 inputs:
20 skip_audio:
21 description: "skip audio generation"
22 type: boolean
23 default: false
24 pull_request:
25 types: [closed]
26 branches: [main]
27
28jobs:
29 # phase 1: archive + generate audio + open PR
30 maintain:
31 if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
32 runs-on: ubuntu-latest
33 permissions:
34 contents: write
35 pull-requests: write
36 id-token: write
37
38 steps:
39 - uses: actions/checkout@v4
40 with:
41 fetch-depth: 0
42
43 - uses: astral-sh/setup-uv@v4
44
45 - uses: anthropics/claude-code-action@v1
46 with:
47 anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
48 claude_args: |
49 --model opus
50 --allowedTools "Read,Write,Edit,Bash,Fetch,Task"
51 prompt: |
52 you are maintaining the plyr.fm (pronounced "player FM") project status file.
53
54 ## critical rules
55
56 1. STATUS.md MUST be kept under 500 lines. this is non-negotiable.
57 2. archive content MUST be moved to .status_history/, not deleted
58 3. podcast tone MUST be dry, matter-of-fact, slightly sardonic - NOT enthusiastic or complimentary
59
60 ## task 1: gather temporal context
61
62 CRITICAL: you must determine the correct time window by finding when the LAST status maintenance PR was MERGED (not opened).
63
64 run these commands:
65 ```bash
66 date
67 # get the most recently merged status-maintenance PR (filter by branch name, sort by merge date)
68 gh pr list --state merged --search "status-maintenance" --limit 20 --json number,title,mergedAt,headRefName | jq '[.[] | select(.headRefName | startswith("status-maintenance-"))] | sort_by(.mergedAt) | reverse | .[0]'
69 git log --oneline -50
70 ls -la .status_history/ 2>/dev/null || echo "no archive directory yet"
71 wc -l STATUS.md
72 ```
73
74 determine:
75 - what is today's date?
76 - when was the last status-maintenance PR MERGED? (use the mergedAt field from the jq output - it's the most recent PR with a branch starting with "status-maintenance-")
77 - what shipped SINCE that merge date? (this is your focus window - NOT "last week")
78 - does .status_history/ exist? (this implies whether or not this is the first episode)
79 - how many lines is STATUS.md currently?
80
81 IMPORTANT: the time window for this maintenance run is from the last merged status-maintenance PR until now. if the last PR was merged on Dec 2nd and today is Dec 8th, you should focus on everything from Dec 3rd onwards, NOT just "the last week".
82
83 ## task 2: archive old sections (MANDATORY if over 250 lines)
84
85 if STATUS.md > 500 lines:
86 1. create .status_history/ directory if it doesn't exist
87 2. identify section boundaries (look for "---" separators and "### " headers with dates)
88 3. move OLDEST sections to .status_history/YYYY-MM.md (grouped by month)
89 4. compact the meaning of the original entire STATUS.md into about 500 lines or less
90 5. generally preserve the document structure (keep "## recent work" header, "## immediate priorities", etc)
91 6. do NOT summarize archived content - move it verbatim and organize it chronologically
92
93 ARCHIVE FILE NAMING - CRITICAL:
94 - archive files are organized BY MONTH: .status_history/YYYY-MM.md
95 - if today is December 2025, archived December content goes to .status_history/2025-12.md
96 - if today is January 2026, archived January content goes to .status_history/2026-01.md
97 - check what files already exist in .status_history/ and ADD to the appropriate month file if it exists
98 - each month gets ONE file - append to existing month files, don't create duplicates
99
100 so STATUS.md is the living overview, slightly recency biased, but a good general overview of the project.
101
102 .status_history/ is the archive of temporally specific sections of STATUS.md that are worth preserving for historical context, but not significant enough to be stated literally in STATUS.md in perpetuity.
103
104 VERIFY: run `wc -l STATUS.md` after archiving. it MUST be under 500 lines.
105
106 ## task 3: generate audio overview (if skip_audio is false)
107
108 skip_audio input: ${{ inputs.skip_audio }}
109
110 if skip_audio is false:
111
112 ### deep investigation phase
113
114 before writing anything, you need to deeply understand what happened in the time window.
115 use subagents liberally to investigate in parallel:
116
117 1. **get the full picture of PRs merged in the time window**:
118 ```bash
119 gh pr list --state merged --search "merged:>={mergedAt date}" --limit 50 --json number,title,body,mergedAt,additions,deletions,files
120 ```
121
122 2. **for each significant PR, read its body and understand the design decisions**:
123 - what problem was being solved?
124 - what approach was taken and why?
125 - what are the key files changed?
126
127 3. **read the actual code changes** for the top 2-3 most significant PRs:
128 - use `gh pr diff {number}` or read the changed files directly
129 - understand the architecture, not just the commit messages
130
131 4. **read background context**:
132 - STATUS.md (the current state)
133 - docs/deployment/overview.md if it exists
134 - Fetch https://atproto.com/guides/overview to understand ATProto primitives
135 - Fetch https://atproto.com/guides/lexicon to understand NSIDs and lexicons
136
137 ### identify the narrative structure
138
139 after investigating, categorize what shipped:
140
141 **big ticket items** (1-3 major features or architectural changes):
142 - these get the most airtime (60-70% of the script)
143 - explain HOW they were designed, not just WHAT they do
144 - discuss interesting technical decisions or tradeoffs
145
146 **smaller but notable changes** (3-6 fixes, improvements, polish):
147 - these get rapid-fire coverage (20-30% of the script)
148 - one or two sentences each
149 - acknowledge they happened without belaboring them
150
151 ### write the podcast script
152
153 write to podcast_script.txt with "Host: ..." and "Cohost: ..." lines.
154
155 **CHRONOLOGICAL NARRATIVE STRUCTURE** (CRITICAL):
156
157 the script must tell a coherent story of the time period, structured as:
158
159 1. **opening** (10 seconds): set the scene - what's the date range, what was the focus?
160
161 2. **the main story** (60-90 seconds): the biggest thing that shipped
162 - what problem did it solve?
163 - how was it designed? (explain the architecture accessibly)
164 - what's interesting about the implementation?
165 - the hosts should have a back-and-forth discussing the design
166
167 3. **secondary feature** (30-45 seconds, if applicable): another significant change
168 - lighter treatment than the main story
169 - still explain the "why" not just the "what"
170
171 4. **rapid fire** (20-30 seconds): the smaller changes
172 - "we also saw..." or "a few other things landed..."
173 - quick hits: bug fixes, polish, minor improvements
174 - don't dwell, just acknowledge
175
176 5. **closing** (10 seconds): looking ahead or wrapping up
177
178 the narrative should flow like you're telling a friend what happened on the project this week.
179 use transitions: "but before that landed...", "meanwhile...", "and then to tie it together..."
180
181 ### tone requirements (CRITICAL)
182
183 the hosts should sound like two engineers who:
184 - are skeptical, amused and somewhat intrigued by the absurdity of building things
185 - acknowledge problems and limitations honestly
186 - don't over-use superlatives ("amazing", "incredible", "exciting")
187 - explain technical concepts through analogy, not hypey jargon
188 - genuinely find the technical details interesting (not performatively enthusiastic)
189
190 avoid excessive phrasing:
191 - "exciting", "amazing", "incredible", "impressive", "great job"
192 - "the team has done", "they've really", "fantastic work"
193 - any variation of over-congratulating or over-sensationalizing the project
194
195 ### pronunciation (CRITICAL - READ THIS CAREFULLY)
196
197 the project name "plyr.fm" is pronounced "player FM" (like "music player").
198
199 **in your script, ALWAYS write "player FM" or "player dot FM" - NEVER write "plyr.fm" or "plyr".**
200
201 the TTS engine will mispronounce "plyr" as "plir" or "p-l-y-r" if you write it that way.
202 write phonetically for correct pronunciation: "player FM", "player dot FM".
203
204 ### identifying what actually shipped
205
206 read the commit messages and PR bodies carefully to understand what changed.
207
208 - if something is completely NEW (didn't exist before), say it "shipped" or "launched"
209 - if something existing got improved or fixed, call it what it is: fixes, improvements, polish
210
211 don't rely on commit message prefixes like `feat:` or `fix:` - they're not always accurate.
212 read the actual content to understand the scope of what changed.
213
214 ### time references (CRITICAL)
215
216 NEVER say "last week", "this week", "recently", or vague time references.
217
218 ALWAYS use specific date ranges based on the mergedAt date from task 1:
219 - "since December 2nd" or "from December 3rd to today"
220 - "in the past six days" (if that's accurate)
221 - "since the last update"
222
223 the listener doesn't know when "last week" was - be specific.
224
225 target length: 2-3 minutes spoken (~300-400 words) (it should be 4-5 if its the first episode)
226
227 ### generate audio
228
229 run: uv run scripts/generate_tts.py podcast_script.txt update.wav
230 then: rm podcast_script.txt
231
232 ## task 4: open PR
233
234 if any files changed:
235 1. first, generate a unique branch name: BRANCH="status-maintenance-$(date +%Y%m%d-%H%M%S)"
236 2. git checkout -b $BRANCH
237 3. git add .status_history/ STATUS.md update.wav
238 4. git commit -m "chore: status maintenance"
239 5. git push -u origin $BRANCH
240 6. gh pr create with a title and body you craft:
241 - title should be descriptive of what this status update covers (e.g. "chore: status maintenance - playlist fast-follow fixes" or "chore: status maintenance - December updates")
242 - make it clear this is an automated status maintenance PR from the GitHub Action
243 - body should summarize what changed (archival, audio generation, etc.)
244
245 add a label like "ai-generated" to the PR (create the label if it doesn't exist)
246 if nothing changed, report that no maintenance was needed.
247
248 env:
249 GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
250
251 # phase 2: upload audio after PR merge
252 upload-audio:
253 if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'status-maintenance-')
254 runs-on: ubuntu-latest
255
256 steps:
257 - uses: actions/checkout@v4
258
259 - uses: astral-sh/setup-uv@v4
260
261 - name: Upload audio to plyr.fm
262 run: |
263 if [ ! -f update.wav ]; then
264 echo "No update.wav found, skipping upload"
265 exit 0
266 fi
267
268 # check existing tracks to determine episode number
269 EXISTING=$(uv run --with plyrfm -- plyrfm my-tracks --limit 50 2>/dev/null || echo "")
270 TODAY=$(date +'%B %d, %Y')
271 YEAR=$(date +%Y)
272
273 # count how many "plyr.fm update - {date}" tracks exist for today
274 TODAY_COUNT=$(echo "$EXISTING" | grep -c "plyr.fm update - $TODAY" || echo "0")
275
276 if [ "$TODAY_COUNT" -gt 0 ]; then
277 # already have one today, add episode number
278 EPISODE=$((TODAY_COUNT + 1))
279 TITLE="plyr.fm update - $TODAY (#$EPISODE)"
280 else
281 TITLE="plyr.fm update - $TODAY"
282 fi
283
284 echo "Uploading as: $TITLE"
285 uv run --with plyrfm -- plyrfm upload update.wav "$TITLE" --album "$YEAR" -t '["ai"]'
286 env:
287 PLYR_TOKEN: ${{ secrets.PLYR_BOT_TOKEN }}