music on atproto
plyr.fm
1# status maintenance via letta-backed agent
2#
3# two-phase workflow:
4# 1. workflow_dispatch: runs letta-backed script to archive, generate audio, open PR
5# 2. on PR merge: uploads audio to plyr.fm
6#
7# the letta agent maintains persistent memory across runs, remembering:
8# - previous status updates and their content
9# - architectural decisions and patterns
10# - recurring themes in development
11#
12# required secrets:
13# LETTA_API_KEY - letta cloud API key (for persistent memory)
14# ANTHROPIC_API_KEY - anthropic API key (for claude)
15# GOOGLE_API_KEY - gemini TTS (for audio generation)
16# PLYR_BOT_TOKEN - plyr.fm developer token (for audio upload)
17
18name: status maintenance
19
20on:
21 # TODO: restore schedule after testing
22 # schedule:
23 # - cron: "0 9 * * 1" # every monday 9am UTC
24 workflow_dispatch:
25 inputs:
26 skip_audio:
27 description: "skip audio generation"
28 type: boolean
29 default: false
30 dry_run:
31 description: "dry run (no file changes)"
32 type: boolean
33 default: false
34 pull_request:
35 types: [closed]
36 branches: [main]
37
38jobs:
39 # phase 1: run letta-backed maintenance script + open PR
40 maintain:
41 if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
42 runs-on: ubuntu-latest
43 permissions:
44 contents: write
45 pull-requests: write
46
47 steps:
48 - uses: actions/checkout@v4
49 with:
50 fetch-depth: 0
51
52 - uses: astral-sh/setup-uv@v4
53
54 - name: Run letta-backed status maintenance
55 id: maintenance
56 run: |
57 ARGS=""
58 if [ "${{ inputs.skip_audio }}" = "true" ]; then
59 ARGS="$ARGS --skip-audio"
60 fi
61 if [ "${{ inputs.dry_run }}" = "true" ]; then
62 ARGS="$ARGS --dry-run"
63 fi
64
65 uv run scripts/status_maintenance.py $ARGS
66
67 # check if files were modified
68 if [ -f .modified_files ]; then
69 echo "files_modified=true" >> $GITHUB_OUTPUT
70 else
71 echo "files_modified=false" >> $GITHUB_OUTPUT
72 fi
73 env:
74 LETTA_API_KEY: ${{ secrets.LETTA_API_KEY }}
75 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
76 GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
77
78 - name: Create PR
79 if: steps.maintenance.outputs.files_modified == 'true' && inputs.dry_run != true
80 run: |
81 # generate unique branch name
82 BRANCH="status-maintenance-$(date +%Y%m%d-%H%M%S)"
83
84 # configure git
85 git config user.name "github-actions[bot]"
86 git config user.email "github-actions[bot]@users.noreply.github.com"
87
88 # create branch and commit
89 git checkout -b "$BRANCH"
90 git add .status_history/ STATUS.md update.wav 2>/dev/null || true
91 git add -A # catch any other changes
92
93 # check if there are changes to commit
94 if git diff --cached --quiet; then
95 echo "No changes to commit"
96 exit 0
97 fi
98
99 git commit -m "chore: status maintenance (letta-backed)
100
101 🤖 Generated with letta-backed status agent
102 "
103
104 git push -u origin "$BRANCH"
105
106 # create PR
107 gh pr create \
108 --title "chore: status maintenance - $(date +'%B %d, %Y')" \
109 --body "## automated status maintenance
110
111 this PR was generated by the letta-backed status maintenance agent.
112
113 ### what's included
114 - STATUS.md updates (if any changes were detected)
115 - archived content moved to .status_history/ (if needed)
116 - update.wav podcast audio (if audio generation was enabled)
117
118 ### letta memory
119 the agent maintains persistent memory across runs, improving context
120 and consistency over time.
121
122 ---
123 🤖 generated by status-maintenance workflow" \
124 --label "ai-generated" || gh label create "ai-generated" --description "Generated by AI" && gh pr create \
125 --title "chore: status maintenance - $(date +'%B %d, %Y')" \
126 --body "automated status maintenance PR" \
127 --label "ai-generated"
128 env:
129 GH_TOKEN: ${{ github.token }}
130
131 # phase 2: upload audio after PR merge
132 upload-audio:
133 if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'status-maintenance-')
134 runs-on: ubuntu-latest
135
136 steps:
137 - uses: actions/checkout@v4
138
139 - uses: astral-sh/setup-uv@v4
140
141 - name: Upload audio to plyr.fm
142 run: |
143 if [ ! -f update.wav ]; then
144 echo "No update.wav found, skipping upload"
145 exit 0
146 fi
147
148 # check existing tracks to determine episode number
149 EXISTING=$(uv run --with plyrfm -- plyrfm my-tracks --limit 50 2>/dev/null || echo "")
150 TODAY=$(date +'%B %d, %Y')
151 YEAR=$(date +%Y)
152
153 # count how many "plyr.fm update - {date}" tracks exist for today
154 TODAY_COUNT=$(echo "$EXISTING" | grep -c "plyr.fm update - $TODAY" || echo "0")
155
156 if [ "$TODAY_COUNT" -gt 0 ]; then
157 # already have one today, add episode number
158 EPISODE=$((TODAY_COUNT + 1))
159 TITLE="plyr.fm update - $TODAY (#$EPISODE)"
160 else
161 TITLE="plyr.fm update - $TODAY"
162 fi
163
164 echo "Uploading as: $TITLE"
165 uv run --with plyrfm -- plyrfm upload update.wav "$TITLE" --album "$YEAR" -t '["ai"]'
166 env:
167 PLYR_TOKEN: ${{ secrets.PLYR_BOT_TOKEN }}