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 # NOTE: excluding #724 which was reverted - remove this exclusion after next successful run
69 gh pr list --state merged --search "status-maintenance" --limit 20 --json number,title,mergedAt,headRefName | jq '[.[] | select(.headRefName | startswith("status-maintenance-")) | select(.number != 724)] | sort_by(.mergedAt) | reverse | .[0]'
70 git log --oneline -50
71 ls -la .status_history/ 2>/dev/null || echo "no archive directory yet"
72 wc -l STATUS.md
73 ```
74
75 determine:
76 - what is today's date?
77 - 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-")
78 - what shipped SINCE that merge date? (this is your focus window - NOT "last week")
79 - does .status_history/ exist? (this implies whether or not this is the first episode)
80 - how many lines is STATUS.md currently?
81
82 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".
83
84 ## task 2: archive old month sections
85
86 **line count targets**:
87 - ideal: ~200 lines (concise overview)
88 - acceptable: 300-450 lines
89 - maximum: 500 lines (MUST NOT exceed)
90
91 **when to archive**: if STATUS.md > 400 lines OR contains detailed sections from previous months
92
93 **what to archive**: content from months BEFORE the current month
94 - if today is January 2026, move December 2025 sections to .status_history/2025-12.md
95 - if today is February 2026, move January 2026 sections to .status_history/2026-01.md
96 - current month content stays in STATUS.md
97
98 **how to archive** (this means MOVING content, not summarizing):
99 1. create .status_history/ directory if it doesn't exist
100 2. identify "### Month Year" sections from previous months in STATUS.md
101 3. CUT the full section content (headers, bullet points, everything)
102 4. PASTE/APPEND to .status_history/YYYY-MM.md
103 - if archive file exists: append to end of file
104 - if archive file doesn't exist: create with header "# plyr.fm Status History - Month Year"
105 5. REPLACE the moved section in STATUS.md with a brief cross-reference:
106 ```
107 ### December 2025
108
109 See `.status_history/2025-12.md` for detailed history.
110 ```
111 6. preserve document structure (keep "## recent work", "## priorities", "## technical state" headers)
112
113 CRITICAL: "archiving" = moving actual content to archive files, NOT condensing or summarizing in place.
114 the detailed write-ups must be preserved in .status_history/, not deleted.
115
116 VERIFY: run `wc -l STATUS.md` after archiving. target 300-450 lines, must be under 500.
117
118 ## task 3: generate audio overview (if skip_audio is false)
119
120 skip_audio input: ${{ inputs.skip_audio }}
121
122 if skip_audio is false:
123
124 ### deep investigation phase
125
126 before writing anything, you need to deeply understand what happened in the time window.
127 use subagents liberally to investigate in parallel:
128
129 1. **get the full picture of PRs merged in the time window**:
130 ```bash
131 gh pr list --state merged --search "merged:>={mergedAt date}" --limit 50 --json number,title,body,mergedAt,additions,deletions,files
132 ```
133
134 2. **for each significant PR, read its body and understand the design decisions**:
135 - what problem was being solved?
136 - what approach was taken and why?
137 - what are the key files changed?
138
139 3. **read the actual code changes** for the top 2-3 most significant PRs:
140 - use `gh pr diff {number}` or read the changed files directly
141 - understand the architecture, not just the commit messages
142
143 4. **read background context**:
144 - STATUS.md (the current state)
145 - docs/deployment/overview.md if it exists
146 - Fetch https://atproto.com/guides/overview to understand ATProto primitives
147 - Fetch https://atproto.com/guides/lexicon to understand NSIDs and lexicons
148
149 ### identify the narrative structure
150
151 after investigating, categorize what shipped:
152
153 **big ticket items** (1-3 major features or architectural changes):
154 - these get the most airtime (60-70% of the script)
155 - explain HOW they were designed, not just WHAT they do
156 - discuss interesting technical decisions or tradeoffs
157
158 **smaller but notable changes** (3-6 fixes, improvements, polish):
159 - these get rapid-fire coverage (20-30% of the script)
160 - one or two sentences each
161 - acknowledge they happened without belaboring them
162
163 ### write the podcast script
164
165 write to podcast_script.txt with "Host: ..." and "Cohost: ..." lines.
166
167 **CHRONOLOGICAL NARRATIVE STRUCTURE** (CRITICAL):
168
169 the script must tell a coherent story of the time period, structured as:
170
171 1. **opening** (10 seconds): set the scene - what's the date range, what was the focus?
172
173 2. **the main story** (60-90 seconds): the biggest thing that shipped
174 - what problem did it solve?
175 - how was it designed? (explain the architecture accessibly)
176 - what's interesting about the implementation?
177 - the hosts should have a back-and-forth discussing the design
178
179 3. **secondary feature** (30-45 seconds, if applicable): another significant change
180 - lighter treatment than the main story
181 - still explain the "why" not just the "what"
182
183 4. **rapid fire** (20-30 seconds): the smaller changes
184 - "we also saw..." or "a few other things landed..."
185 - quick hits: bug fixes, polish, minor improvements
186 - don't dwell, just acknowledge
187
188 5. **closing** (10 seconds): looking ahead or wrapping up
189
190 the narrative should flow like you're telling a friend what happened on the project this week.
191 use transitions: "but before that landed...", "meanwhile...", "and then to tie it together..."
192
193 ### tone requirements (CRITICAL)
194
195 the hosts should sound like two engineers who:
196 - are skeptical, amused and somewhat intrigued by the absurdity of building things
197 - acknowledge problems and limitations honestly
198 - don't over-use superlatives ("amazing", "incredible", "exciting")
199 - explain technical concepts through analogy, not hypey jargon
200 - genuinely find the technical details interesting (not performatively enthusiastic)
201
202 avoid excessive phrasing:
203 - "exciting", "amazing", "incredible", "impressive", "great job"
204 - "the team has done", "they've really", "fantastic work"
205 - any variation of over-congratulating or over-sensationalizing the project
206
207 ### pronunciation (CRITICAL - READ THIS CAREFULLY)
208
209 the project name "plyr.fm" is pronounced "player FM" (like "music player").
210
211 **in your script, ALWAYS write "player FM" or "player dot FM" - NEVER write "plyr.fm" or "plyr".**
212
213 the TTS engine will mispronounce "plyr" as "plir" or "p-l-y-r" if you write it that way.
214 write phonetically for correct pronunciation: "player FM", "player dot FM".
215
216 ### terminology
217
218 plyr.fm is built on **ATProto** (the protocol), not Bluesky (the app).
219 say "ATProto identities" or just "identities" - never "Bluesky accounts".
220
221 ### identifying what actually shipped
222
223 read the commit messages and PR bodies carefully to understand what changed.
224
225 - if something is completely NEW (didn't exist before), say it "shipped" or "launched"
226 - if something existing got improved or fixed, call it what it is: fixes, improvements, polish
227
228 don't rely on commit message prefixes like `feat:` or `fix:` - they're not always accurate.
229 read the actual content to understand the scope of what changed.
230
231 ### time references (CRITICAL)
232
233 NEVER say "last week", "this week", "recently", or vague time references.
234
235 ALWAYS use specific date ranges based on the mergedAt date from task 1:
236 - "since December 2nd" or "from December 3rd to today"
237 - "in the past six days" (if that's accurate)
238 - "since the last update"
239
240 the listener doesn't know when "last week" was - be specific.
241
242 target length: 2-3 minutes spoken (~300-400 words) (it should be 4-5 if its the first episode)
243
244 ### generate audio
245
246 run: uv run scripts/generate_tts.py podcast_script.txt update.wav
247 then: rm podcast_script.txt
248
249 ## task 4: open PR
250
251 if any files changed:
252 1. first, generate a unique branch name: BRANCH="status-maintenance-$(date +%Y%m%d-%H%M%S)"
253 2. git checkout -b $BRANCH
254 3. git add .status_history/ STATUS.md update.wav
255 4. git commit -m "chore: status maintenance"
256 5. git push -u origin $BRANCH
257 6. gh pr create with a title and body you craft:
258 - 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")
259 - make it clear this is an automated status maintenance PR from the GitHub Action
260 - body should summarize what changed (archival, audio generation, etc.)
261
262 add a label like "ai-generated" to the PR (create the label if it doesn't exist)
263 if nothing changed, report that no maintenance was needed.
264
265 env:
266 GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
267
268 # phase 2: upload audio after PR merge
269 upload-audio:
270 if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'status-maintenance-')
271 runs-on: ubuntu-latest
272
273 steps:
274 - uses: actions/checkout@v4
275
276 - uses: astral-sh/setup-uv@v4
277
278 - name: Upload audio to plyr.fm
279 run: |
280 if [ ! -f update.wav ]; then
281 echo "No update.wav found, skipping upload"
282 exit 0
283 fi
284
285 # check existing tracks to determine episode number
286 EXISTING=$(uv run --with plyrfm -- plyrfm my-tracks --limit 50 2>/dev/null || echo "")
287 TODAY=$(date +'%B %d, %Y')
288 YEAR=$(date +%Y)
289
290 # count how many "plyr.fm update - {date}" tracks exist for today
291 TODAY_COUNT=$(echo "$EXISTING" | grep -c "plyr.fm update - $TODAY" || echo "0")
292
293 if [ "$TODAY_COUNT" -gt 0 ]; then
294 # already have one today, add episode number
295 EPISODE=$((TODAY_COUNT + 1))
296 TITLE="plyr.fm update - $TODAY (#$EPISODE)"
297 else
298 TITLE="plyr.fm update - $TODAY"
299 fi
300
301 echo "Uploading as: $TITLE"
302 uv run --with plyrfm -- plyrfm upload update.wav "$TITLE" --album "$YEAR" -t "ai"
303 env:
304 PLYR_TOKEN: ${{ secrets.PLYR_BOT_TOKEN }}