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