# status maintenance via claude code # # two-phase workflow: # 1. workflow_dispatch: archives old STATUS.md sections, generates audio, opens PR # 2. on PR merge: uploads audio to plyr.fm # # required secrets: # ANTHROPIC_API_KEY - claude code # GOOGLE_API_KEY - gemini TTS (for audio generation) # PLYR_BOT_TOKEN - plyr.fm developer token (for audio upload) name: status maintenance on: # TODO: restore schedule after testing # schedule: # - cron: "0 9 * * 1" # every monday 9am UTC workflow_dispatch: inputs: skip_audio: description: "skip audio generation" type: boolean default: false pull_request: types: [closed] branches: [main] jobs: # phase 1: archive + generate audio + open PR maintain: if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' runs-on: ubuntu-latest permissions: contents: write pull-requests: write id-token: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: astral-sh/setup-uv@v4 - uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} claude_args: | --model opus --allowedTools "Read,Write,Edit,Bash,Fetch,Task" prompt: | you are maintaining the plyr.fm (pronounced "player FM") project status file. ## critical rules 1. STATUS.md MUST be kept under 500 lines. this is non-negotiable. 2. archive content MUST be moved to .status_history/, not deleted 3. podcast tone MUST be dry, matter-of-fact, slightly sardonic - NOT enthusiastic or complimentary ## task 1: gather temporal context CRITICAL: you must determine the correct time window by finding when the LAST status maintenance PR was MERGED (not opened). run these commands: ```bash date # get the most recently merged status-maintenance PR (filter by branch name, sort by merge date) # NOTE: excluding #724 which was reverted - remove this exclusion after next successful run 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]' git log --oneline -50 ls -la .status_history/ 2>/dev/null || echo "no archive directory yet" wc -l STATUS.md ``` determine: - what is today's date? - 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-") - what shipped SINCE that merge date? (this is your focus window - NOT "last week") - does .status_history/ exist? (this implies whether or not this is the first episode) - how many lines is STATUS.md currently? 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". ## task 2: archive old month sections **line count targets**: - ideal: ~200 lines (concise overview) - acceptable: 300-450 lines - maximum: 500 lines (MUST NOT exceed) **when to archive**: if STATUS.md > 400 lines OR contains detailed sections from previous months **what to archive**: content from months BEFORE the current month - if today is January 2026, move December 2025 sections to .status_history/2025-12.md - if today is February 2026, move January 2026 sections to .status_history/2026-01.md - current month content stays in STATUS.md **how to archive** (this means MOVING content, not summarizing): 1. create .status_history/ directory if it doesn't exist 2. identify "### Month Year" sections from previous months in STATUS.md 3. CUT the full section content (headers, bullet points, everything) 4. PASTE/APPEND to .status_history/YYYY-MM.md - if archive file exists: append to end of file - if archive file doesn't exist: create with header "# plyr.fm Status History - Month Year" 5. REPLACE the moved section in STATUS.md with a brief cross-reference: ``` ### December 2025 See `.status_history/2025-12.md` for detailed history. ``` 6. preserve document structure (keep "## recent work", "## priorities", "## technical state" headers) CRITICAL: "archiving" = moving actual content to archive files, NOT condensing or summarizing in place. the detailed write-ups must be preserved in .status_history/, not deleted. VERIFY: run `wc -l STATUS.md` after archiving. target 300-450 lines, must be under 500. ## task 3: generate audio overview (if skip_audio is false) skip_audio input: ${{ inputs.skip_audio }} if skip_audio is false: ### deep investigation phase before writing anything, you need to deeply understand what happened in the time window. use subagents liberally to investigate in parallel: 1. **get the full picture of PRs merged in the time window**: ```bash gh pr list --state merged --search "merged:>={mergedAt date}" --limit 50 --json number,title,body,mergedAt,additions,deletions,files ``` 2. **for each significant PR, read its body and understand the design decisions**: - what problem was being solved? - what approach was taken and why? - what are the key files changed? 3. **read the actual code changes** for the top 2-3 most significant PRs: - use `gh pr diff {number}` or read the changed files directly - understand the architecture, not just the commit messages 4. **read background context**: - STATUS.md (the current state) - docs/deployment/overview.md if it exists - Fetch https://atproto.com/guides/overview to understand ATProto primitives - Fetch https://atproto.com/guides/lexicon to understand NSIDs and lexicons ### identify the narrative structure after investigating, categorize what shipped: **big ticket items** (1-3 major features or architectural changes): - these get the most airtime (60-70% of the script) - explain HOW they were designed, not just WHAT they do - discuss interesting technical decisions or tradeoffs **smaller but notable changes** (3-6 fixes, improvements, polish): - these get rapid-fire coverage (20-30% of the script) - one or two sentences each - acknowledge they happened without belaboring them ### write the podcast script write to podcast_script.txt with "Host: ..." and "Cohost: ..." lines. **CHRONOLOGICAL NARRATIVE STRUCTURE** (CRITICAL): the script must tell a coherent story of the time period, structured as: 1. **opening** (10 seconds): set the scene - what's the date range, what was the focus? 2. **the main story** (60-90 seconds): the biggest thing that shipped - what problem did it solve? - how was it designed? (explain the architecture accessibly) - what's interesting about the implementation? - the hosts should have a back-and-forth discussing the design 3. **secondary feature** (30-45 seconds, if applicable): another significant change - lighter treatment than the main story - still explain the "why" not just the "what" 4. **rapid fire** (20-30 seconds): the smaller changes - "we also saw..." or "a few other things landed..." - quick hits: bug fixes, polish, minor improvements - don't dwell, just acknowledge 5. **closing** (10 seconds): looking ahead or wrapping up the narrative should flow like you're telling a friend what happened on the project this week. use transitions: "but before that landed...", "meanwhile...", "and then to tie it together..." ### tone requirements (CRITICAL) the hosts should sound like two engineers who: - are skeptical, amused and somewhat intrigued by the absurdity of building things - acknowledge problems and limitations honestly - don't over-use superlatives ("amazing", "incredible", "exciting") - explain technical concepts through analogy, not hypey jargon - genuinely find the technical details interesting (not performatively enthusiastic) avoid excessive phrasing: - "exciting", "amazing", "incredible", "impressive", "great job" - "the team has done", "they've really", "fantastic work" - any variation of over-congratulating or over-sensationalizing the project ### pronunciation (CRITICAL - READ THIS CAREFULLY) the project name "plyr.fm" is pronounced "player FM" (like "music player"). **in your script, ALWAYS write "player FM" or "player dot FM" - NEVER write "plyr.fm" or "plyr".** the TTS engine will mispronounce "plyr" as "plir" or "p-l-y-r" if you write it that way. write phonetically for correct pronunciation: "player FM", "player dot FM". ### terminology plyr.fm is built on **ATProto** (the protocol), not Bluesky (the app). say "ATProto identities" or just "identities" - never "Bluesky accounts". ### identifying what actually shipped read the commit messages and PR bodies carefully to understand what changed. - if something is completely NEW (didn't exist before), say it "shipped" or "launched" - if something existing got improved or fixed, call it what it is: fixes, improvements, polish don't rely on commit message prefixes like `feat:` or `fix:` - they're not always accurate. read the actual content to understand the scope of what changed. ### time references (CRITICAL) NEVER say "last week", "this week", "recently", or vague time references. ALWAYS use specific date ranges based on the mergedAt date from task 1: - "since December 2nd" or "from December 3rd to today" - "in the past six days" (if that's accurate) - "since the last update" the listener doesn't know when "last week" was - be specific. target length: 2-3 minutes spoken (~300-400 words) (it should be 4-5 if its the first episode) ### generate audio run: uv run scripts/generate_tts.py podcast_script.txt update.wav then: rm podcast_script.txt ## task 4: open PR if any files changed: 1. first, generate a unique branch name: BRANCH="status-maintenance-$(date +%Y%m%d-%H%M%S)" 2. git checkout -b $BRANCH 3. git add .status_history/ STATUS.md update.wav 4. git commit -m "chore: status maintenance" 5. git push -u origin $BRANCH 6. gh pr create with a title and body you craft: - 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") - make it clear this is an automated status maintenance PR from the GitHub Action - body should summarize what changed (archival, audio generation, etc.) add a label like "ai-generated" to the PR (create the label if it doesn't exist) if nothing changed, report that no maintenance was needed. env: GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} # phase 2: upload audio after PR merge upload-audio: if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'status-maintenance-') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v4 - name: Upload audio to plyr.fm run: | if [ ! -f update.wav ]; then echo "No update.wav found, skipping upload" exit 0 fi # check existing tracks to determine episode number EXISTING=$(uv run --with plyrfm -- plyrfm my-tracks --limit 50 2>/dev/null || echo "") TODAY=$(date +'%B %d, %Y') YEAR=$(date +%Y) # count how many "plyr.fm update - {date}" tracks exist for today TODAY_COUNT=$(echo "$EXISTING" | grep -c "plyr.fm update - $TODAY" || echo "0") if [ "$TODAY_COUNT" -gt 0 ]; then # already have one today, add episode number EPISODE=$((TODAY_COUNT + 1)) TITLE="plyr.fm update - $TODAY (#$EPISODE)" else TITLE="plyr.fm update - $TODAY" fi echo "Uploading as: $TITLE" uv run --with plyrfm -- plyrfm upload update.wav "$TITLE" --album "$YEAR" -t "ai" env: PLYR_TOKEN: ${{ secrets.PLYR_BOT_TOKEN }}