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 --allowedTools "Read,Write,Edit,Bash,Fetch"
50 prompt: |
51 you are maintaining the plyr.fm (pronounced "player FM") project status file.
52
53 ## critical rules
54
55 1. STATUS.md MUST be kept under 500 lines. this is non-negotiable.
56 2. archive content MUST be moved to .status_history/, not deleted
57 3. podcast tone MUST be dry, matter-of-fact, slightly sardonic - NOT enthusiastic or complimentary
58
59 ## task 1: gather temporal context
60
61 CRITICAL: you must determine the correct time window by finding when the LAST status maintenance PR was MERGED (not opened).
62
63 run these commands:
64 ```bash
65 date
66 # get the most recently merged status-maintenance PR (filter by branch name, sort by merge date)
67 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]'
68 git log --oneline -50
69 ls -la .status_history/ 2>/dev/null || echo "no archive directory yet"
70 wc -l STATUS.md
71 ```
72
73 determine:
74 - what is today's date?
75 - 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-")
76 - what shipped SINCE that merge date? (this is your focus window - NOT "last week")
77 - does .status_history/ exist? (this implies whether or not this is the first episode)
78 - how many lines is STATUS.md currently?
79
80 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".
81
82 ## task 2: archive old sections (MANDATORY if over 250 lines)
83
84 if STATUS.md > 500 lines:
85 1. create .status_history/ directory if it doesn't exist
86 2. identify section boundaries (look for "---" separators and "### " headers with dates)
87 3. move OLDEST sections to .status_history/YYYY-MM.md (grouped by month)
88 4. compact the meaning of the original entire STATUS.md into about 500 lines or less
89 5. generally preserve the document structure (keep "## recent work" header, "## immediate priorities", etc)
90 6. do NOT summarize archived content - move it verbatim and organize it chronologically
91
92 ARCHIVE FILE NAMING - CRITICAL:
93 - archive files are organized BY MONTH: .status_history/YYYY-MM.md
94 - if today is December 2025, archived December content goes to .status_history/2025-12.md
95 - if today is January 2026, archived January content goes to .status_history/2026-01.md
96 - check what files already exist in .status_history/ and ADD to the appropriate month file if it exists
97 - each month gets ONE file - append to existing month files, don't create duplicates
98
99 so STATUS.md is the living overview, slightly recency biased, but a good general overview of the project.
100
101 .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.
102
103 VERIFY: run `wc -l STATUS.md` after archiving. it MUST be under 500 lines.
104
105 ## task 3: generate audio overview (if skip_audio is false)
106
107 skip_audio input: ${{ inputs.skip_audio }}
108
109 if skip_audio is false:
110
111 ### understand the project first
112
113 before writing the script, read:
114 - STATUS.md (the current state)
115 - docs/deployment/overview.md if it exists
116 - Fetch https://atproto.com/guides/overview to understand ATProto primitives
117 - Fetch https://atproto.com/guides/lexicon to understand NSIDs and lexicons
118 - fetch and read: https://overreacted.io/open-social/ to understand the vision of open social
119
120 this context helps you explain things accurately, and accessibly without over-simplifying.
121
122 ### determine episode type
123
124 check if .status_history/ directory exists:
125 - if NO .status_history/ exists: this is the INAUGURAL episode
126 - if .status_history/ exists: this is a SUBSEQUENT episode
127
128 ### write the podcast script
129
130 write to podcast_script.txt with "Host: ..." and "Cohost: ..." lines.
131
132 INAUGURAL EPISODE (ignore this if its not the first episode):
133 - introduce what plyr.fm is: decentralized music streaming on ATProto
134 - explain the core value prop: your music data lives in your PDS, portable between apps (open social)
135 - cover the technical foundation that's been built (summarize from STATUS.md)
136 - set expectations: this is an early-stage project, rough edges exist
137 - tone: "here's what is being built and why it could matter"
138 - work chronologically from beginning to end, don't heavily bias towards nascent or recent work
139
140 SUBSEQUENT EPISODES (ignore this if its the first episode):
141 - focus on what actually shipped since the last status-maintenance PR was MERGED
142 - use the mergedAt date from task 1 as your starting point, NOT "last week"
143 - reference git commits since that merge date to determine what shipped
144 - don't re-explain the whole project - listeners already know
145 - tone: "here's what changed since last time"
146
147 ### tone requirements (CRITICAL)
148
149 the hosts should sound like two engineers who:
150 - are skeptical, amused and somewhat intruiged by the absurdity of building things
151 - acknowledge problems and limitations honestly
152 - don't over-use superlatives ("amazing", "incredible", "exciting")
153 - explain technical concepts through analogy, not hypey jargon
154
155 avoid excessive phrasing:
156 - "exciting", "amazing", "incredible", "impressive", "great job"
157 - "the team has done", "they've really", "fantastic work"
158 - any variation of over-congratulating or over-sensationalizing the project
159
160 pronunciation: "plyr.fm" is pronounced "player FM" (not "plir" or spelled out)
161
162 target length: 2-3 minutes spoken (~300-400 words) (it should be 4-5 if its the first episode)
163
164 ### generate audio
165
166 run: uv run scripts/generate_tts.py podcast_script.txt update.wav
167 then: rm podcast_script.txt
168
169 ## task 4: open PR
170
171 if any files changed:
172 1. first, generate a unique branch name: BRANCH="status-maintenance-$(date +%Y%m%d-%H%M%S)"
173 2. git checkout -b $BRANCH
174 3. git add .status_history/ STATUS.md update.wav
175 4. git commit -m "chore: status maintenance"
176 5. git push -u origin $BRANCH
177 6. gh pr create with a title and body you craft:
178 - 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")
179 - make it clear this is an automated status maintenance PR from the GitHub Action
180 - body should summarize what changed (archival, audio generation, etc.)
181
182 add a label like "ai-generated" to the PR (create the label if it doesn't exist)
183 if nothing changed, report that no maintenance was needed.
184
185 env:
186 GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
187
188 # phase 2: upload audio after PR merge
189 upload-audio:
190 if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'status-maintenance-')
191 runs-on: ubuntu-latest
192
193 steps:
194 - uses: actions/checkout@v4
195
196 - uses: astral-sh/setup-uv@v4
197
198 - name: Upload audio to plyr.fm
199 run: |
200 if [ -f update.wav ]; then
201 uv run --with plyrfm -- plyrfm upload update.wav "plyr.fm update - $(date +'%B %d, %Y')" --album "$(date +%Y)" -t ai
202 else
203 echo "No update.wav found, skipping upload"
204 fi
205 env:
206 PLYR_TOKEN: ${{ secrets.PLYR_BOT_TOKEN }}