strands-coder 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ name: Agent
2
+
3
+ on:
4
+ schedule:
5
+ # Autonomous run every 4 hours - general maintenance and discovery
6
+ # - cron: '0 */4 * * *'
7
+ # Additional scheduled jobs are handled by control.yml (hourly checks)
8
+ # Use the scheduler tool to configure custom schedules via AGENT_SCHEDULES variable
9
+ issues:
10
+ types: [opened, edited, closed, reopened, assigned, unassigned, labeled, unlabeled]
11
+ issue_comment:
12
+ types: [created, edited, deleted]
13
+ pull_request:
14
+ types: [opened, closed, edited, reopened, synchronize, ready_for_review]
15
+ pull_request_review:
16
+ types: [submitted, edited]
17
+ discussion:
18
+ types: [created, edited, answered, unanswered, category_changed, labeled, unlabeled, transferred, pinned, unpinned, locked, unlocked]
19
+ discussion_comment:
20
+ types: [created, edited, deleted]
21
+ pull_request_review_comment:
22
+ types: [created, edited]
23
+ workflow_dispatch:
24
+ inputs:
25
+ prompt:
26
+ description: 'Prompt for agent to perform'
27
+ required: false
28
+ type: string
29
+ system_prompt:
30
+ description: 'Additional system prompt instructions'
31
+ required: false
32
+ type: string
33
+ tools:
34
+ description: 'Tool config (e.g., strands_tools:shell;strands_coder:use_github)'
35
+ required: false
36
+ type: string
37
+ model:
38
+ description: 'Model ID'
39
+ default: "global.anthropic.claude-opus-4-5-20251101-v1:0"
40
+ required: false
41
+ type: string
42
+ max_tokens:
43
+ description: 'Max tokens'
44
+ default: "60000"
45
+ required: false
46
+ type: string
47
+
48
+ permissions: write-all
49
+
50
+ jobs:
51
+ agent:
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - name: Check user authorization
55
+ id: check-auth
56
+ run: |
57
+ # For scheduled runs, always authorize
58
+ if [ "${{ github.event_name }}" = "schedule" ]; then
59
+ echo "✅ Scheduled run - authorized"
60
+ echo "authorized=true" >> $GITHUB_OUTPUT
61
+ exit 0
62
+ fi
63
+
64
+ # For workflow_dispatch from control.yml (github-actions[bot])
65
+ if [ "${{ github.actor }}" = "github-actions[bot]" ]; then
66
+ echo "✅ Control loop dispatch - authorized"
67
+ echo "authorized=true" >> $GITHUB_OUTPUT
68
+ exit 0
69
+ fi
70
+
71
+ # For manual/event-triggered runs, check authorization
72
+ AUTHORIZED_USERS="${{ secrets.AUTHORIZED_USERS }}"
73
+
74
+ echo "Checking authorization for user: ${{ github.actor }}"
75
+
76
+ if [[ ",$AUTHORIZED_USERS," == *",${{ github.actor }},"* ]]; then
77
+ echo "✅ User ${{ github.actor }} is authorized"
78
+ echo "authorized=true" >> $GITHUB_OUTPUT
79
+ else
80
+ echo "❌ User ${{ github.actor }} is NOT authorized"
81
+ echo "Authorized users: $AUTHORIZED_USERS"
82
+ echo "🚫 UNAUTHORIZED ACCESS ATTEMPT"
83
+ echo "Repository: ${{ github.repository }}"
84
+ echo "Event: ${{ github.event_name }}"
85
+ echo "Time: $(date)"
86
+ echo "Contact repository administrators for access."
87
+ echo "authorized=false" >> $GITHUB_OUTPUT
88
+ exit 1
89
+ fi
90
+
91
+ - name: Checkout code
92
+ if: steps.check-auth.outputs.authorized == 'true'
93
+ uses: actions/checkout@v4
94
+ with:
95
+ token: ${{ secrets.GITHUB_TOKEN }}
96
+
97
+ - name: Run Strands Agent
98
+ if: steps.check-auth.outputs.authorized == 'true'
99
+ uses: ./
100
+ env:
101
+ # GitHub tokens
102
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103
+ PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
104
+
105
+ # Model provider API keys (set the one you need based on STRANDS_PROVIDER)
106
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
107
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
108
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
109
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
110
+ COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
111
+ MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
112
+ WRITER_API_KEY: ${{ secrets.WRITER_API_KEY }}
113
+ LITELLM_API_KEY: ${{ secrets.LITELLM_API_KEY }}
114
+ LLAMAAPI_API_KEY: ${{ secrets.LLAMAAPI_API_KEY }}
115
+ AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
116
+
117
+ # Advanced model configuration
118
+ STRANDS_ADDITIONAL_REQUEST_FIELDS: ${{ vars.STRANDS_ADDITIONAL_REQUEST_FIELDS }}
119
+
120
+ # MCP servers
121
+ MCP_SERVERS: ${{ vars.MCP_SERVERS }}
122
+
123
+ # Project & Knowledge Base
124
+ STRANDS_CODER_PROJECT_ID: ${{ vars.STRANDS_CODER_PROJECT_ID }}
125
+ STRANDS_KNOWLEDGE_BASE_ID: ${{ vars.STRANDS_KNOWLEDGE_BASE_ID }}
126
+
127
+ # Session persistence
128
+ S3_SESSION_BUCKET: ${{ vars.S3_SESSION_BUCKET }}
129
+ S3_SESSION_PREFIX: ${{ vars.S3_SESSION_PREFIX }}
130
+
131
+ # Slack integration
132
+ SLACK_APP_TOKEN: ${{ secrets.SLACK_APP_TOKEN }}
133
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
134
+
135
+ # Observability (Langfuse)
136
+ LANGFUSE_BASE_URL: ${{ secrets.LANGFUSE_BASE_URL }}
137
+ LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
138
+ LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
139
+
140
+ # Advanced settings
141
+ STRANDS_TOOLS_DIRECTORY: ${{ vars.STRANDS_TOOLS_DIRECTORY }}
142
+ with:
143
+ prompt: |
144
+ ${{
145
+ github.event.inputs.prompt ||
146
+ (github.event_name == 'schedule' && 'I am running on a scheduled basis (every 4 hours). I will check the repository status, review open issues and PRs, and provide insights or suggestions for improvements, work on active tracked tasks and discover new opportunities to work on. I have full GitHub context available.') ||
147
+ 'I received a GitHub event and am running autonomously. I will analyze the context and take appropriate action. I have full GitHub event details available.'
148
+ }}
149
+
150
+ # Model configuration
151
+ provider: ${{ github.event.inputs.provider || vars.STRANDS_PROVIDER || 'bedrock' }}
152
+ model: ${{ github.event.inputs.model || vars.STRANDS_MODEL_ID || 'global.anthropic.claude-opus-4-5-20251101-v1:0' }}
153
+ max_tokens: ${{ vars.STRANDS_MAX_TOKENS || '60000' }}
154
+
155
+ # System prompt configuration
156
+ system_prompt: ${{ github.event.inputs.system_prompt || vars.SYSTEM_PROMPT || vars.INPUT_SYSTEM_PROMPT || 'You are a restricted GitHub agent for this repository, powered by Strands Agents SDK. Only authorized users can trigger your execution.' }}
157
+
158
+ # Tool configuration
159
+ tools: ${{ github.event.inputs.tools || vars.STRANDS_TOOLS || 'strands_tools:shell,retrieve,slack;strands_coder:use_github,create_subagent,system_prompt,store_in_kb,projects,scheduler' }}
160
+
161
+ # AWS configuration
162
+ aws_role_arn: ${{ secrets.AWS_ROLE_ARN }}
163
+ aws_region: ${{ secrets.AWS_REGION || 'us-west-2' }}
164
+ git_user_email: "217235299+strands-agent@users.noreply.github.com"
165
+ git_user_name: "strands-agent"
@@ -0,0 +1,85 @@
1
+ name: Auto Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ permissions:
9
+ contents: write
10
+ packages: write
11
+ id-token: write
12
+
13
+ jobs:
14
+ auto-release:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+ token: ${{ secrets.GITHUB_TOKEN }}
23
+
24
+ - name: Set up Python
25
+ uses: actions/setup-python@v4
26
+ with:
27
+ python-version: "3.11"
28
+
29
+ - name: Install dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install build twine
33
+
34
+ - name: Extract version from tag
35
+ id: get_version
36
+ run: |
37
+ VERSION=${GITHUB_REF#refs/tags/v}
38
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
39
+ echo "Version: $VERSION"
40
+
41
+ - name: Build package
42
+ run: |
43
+ python -m build
44
+
45
+ - name: Publish to PyPI
46
+ env:
47
+ TWINE_USERNAME: __token__
48
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
49
+ run: |
50
+ twine upload dist/*
51
+
52
+ - name: Create GitHub Release
53
+ uses: actions/create-release@v1
54
+ env:
55
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56
+ with:
57
+ tag_name: ${{ github.ref }}
58
+ release_name: strands-coder v${{ steps.get_version.outputs.version }}
59
+ body: |
60
+ ## 📄 strands-coder v${{ steps.get_version.outputs.version }}
61
+
62
+ ### 📦 Installation
63
+ ```bash
64
+ pip install strands-coder==${{ steps.get_version.outputs.version }}
65
+ ```
66
+
67
+ ### 🔄 Upgrade
68
+ ```bash
69
+ pip install --upgrade strands-coder
70
+ ```
71
+
72
+ ### 📋 Changes
73
+ This release includes the latest changes tagged as v${{ steps.get_version.outputs.version }}.
74
+
75
+ **Full Changelog**: https://github.com/cagataycali/strands-coder/releases
76
+ draft: false
77
+ prerelease: false
78
+
79
+ - name: Summary
80
+ run: |
81
+ echo "## 🎉 Release Summary" >> $GITHUB_STEP_SUMMARY
82
+ echo "" >> $GITHUB_STEP_SUMMARY
83
+ echo "- **Version:** v${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
84
+ echo "- **PyPI:** https://pypi.org/project/strands-coder/${{ steps.get_version.outputs.version }}/" >> $GITHUB_STEP_SUMMARY
85
+ echo "- **GitHub Release:** https://github.com/cagataycali/strands-coder/releases/tag/v${{ steps.get_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
@@ -0,0 +1,395 @@
1
+ name: Control Loop
2
+
3
+ on:
4
+ schedule:
5
+ # Run every hour at minute 0
6
+ # - cron: '0 * * * *'
7
+ workflow_dispatch:
8
+ inputs:
9
+ force_check:
10
+ description: 'Force check schedules regardless of time'
11
+ required: false
12
+ type: boolean
13
+ default: false
14
+
15
+ permissions:
16
+ contents: read
17
+ actions: write
18
+
19
+ jobs:
20
+ control:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Check Schedules and Dispatch Jobs
24
+ env:
25
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26
+ PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
27
+ AGENT_SCHEDULES: ${{ vars.AGENT_SCHEDULES }}
28
+ run: |
29
+ #!/bin/bash
30
+ set -e
31
+
32
+ echo "🕐 Control Loop - $(date -u '+%Y-%m-%d %H:%M') UTC"
33
+ echo "Repository: ${{ github.repository }}"
34
+
35
+ # Parse AGENT_SCHEDULES JSON
36
+ if [ -z "$AGENT_SCHEDULES" ] || [ "$AGENT_SCHEDULES" = "{}" ]; then
37
+ echo "ℹ️ No schedules configured (AGENT_SCHEDULES is empty)"
38
+ echo "To add schedules, use the scheduler tool or set the AGENT_SCHEDULES variable"
39
+ exit 0
40
+ fi
41
+
42
+ # Get current time components (UTC)
43
+ CURRENT_MINUTE=$(date -u '+%M' | sed 's/^0//')
44
+ CURRENT_HOUR=$(date -u '+%H' | sed 's/^0//')
45
+ CURRENT_DAY=$(date -u '+%d' | sed 's/^0//')
46
+ CURRENT_MONTH=$(date -u '+%m' | sed 's/^0//')
47
+ CURRENT_DOW=$(date -u '+%w') # 0=Sunday
48
+ CURRENT_EPOCH=$(date -u '+%s')
49
+
50
+ # Handle empty values (when minute/hour is 0)
51
+ [ -z "$CURRENT_MINUTE" ] && CURRENT_MINUTE=0
52
+ [ -z "$CURRENT_HOUR" ] && CURRENT_HOUR=0
53
+ [ -z "$CURRENT_DAY" ] && CURRENT_DAY=1
54
+ [ -z "$CURRENT_MONTH" ] && CURRENT_MONTH=1
55
+
56
+ echo "Current time: minute=$CURRENT_MINUTE hour=$CURRENT_HOUR day=$CURRENT_DAY month=$CURRENT_MONTH dow=$CURRENT_DOW epoch=$CURRENT_EPOCH"
57
+
58
+ # Window for catching missed jobs (24 hours = 86400 seconds)
59
+ # This handles GitHub Actions schedule delays/skips
60
+ # Large window ensures daily jobs are caught even if control loop was down
61
+ CATCH_UP_WINDOW=86400
62
+
63
+ # Function to check if a cron field matches
64
+ cron_field_matches() {
65
+ local field="$1"
66
+ local value="$2"
67
+
68
+ # Wildcard matches all
69
+ if [ "$field" = "*" ]; then
70
+ return 0
71
+ fi
72
+
73
+ # Step values like */15
74
+ if [[ "$field" == "*/"* ]]; then
75
+ local step="${field#*/}"
76
+ if [ $((value % step)) -eq 0 ]; then
77
+ return 0
78
+ fi
79
+ return 1
80
+ fi
81
+
82
+ # Lists like 1,3,5
83
+ if [[ "$field" == *","* ]]; then
84
+ IFS=',' read -ra VALUES <<< "$field"
85
+ for v in "${VALUES[@]}"; do
86
+ if [ "$v" -eq "$value" ] 2>/dev/null; then
87
+ return 0
88
+ fi
89
+ done
90
+ return 1
91
+ fi
92
+
93
+ # Ranges like 1-5
94
+ if [[ "$field" == *"-"* ]] && [[ "$field" != *"/"* ]]; then
95
+ local start="${field%-*}"
96
+ local end="${field#*-}"
97
+ if [ "$value" -ge "$start" ] && [ "$value" -le "$end" ]; then
98
+ return 0
99
+ fi
100
+ return 1
101
+ fi
102
+
103
+ # Direct match
104
+ if [ "$field" -eq "$value" ] 2>/dev/null; then
105
+ return 0
106
+ fi
107
+
108
+ return 1
109
+ }
110
+
111
+ # Function to check if a cron should have triggered within a time window
112
+ # This uses a window-based approach instead of exact minute matching
113
+ cron_should_trigger() {
114
+ local cron="$1"
115
+ local last_triggered="$2" # epoch timestamp or empty
116
+
117
+ read -r cron_minute cron_hour cron_dom cron_month cron_dow <<< "$cron"
118
+
119
+ # If no last_triggered, assume it never ran (use epoch 0)
120
+ # This ensures the job will trigger on its first scheduled window
121
+ if [ -z "$last_triggered" ] || [ "$last_triggered" = "null" ] || [ "$last_triggered" = "0" ]; then
122
+ last_triggered=0
123
+ echo " (No previous run recorded)"
124
+ fi
125
+
126
+ # Calculate the most recent scheduled time based on cron
127
+ # For simplicity, check if we're within the scheduled hour
128
+
129
+ # Get target hour (handle wildcards)
130
+ local target_hour="$cron_hour"
131
+ if [ "$target_hour" = "*" ]; then
132
+ target_hour="$CURRENT_HOUR"
133
+ fi
134
+
135
+ # Get target minute (handle wildcards)
136
+ local target_minute="$cron_minute"
137
+ if [ "$target_minute" = "*" ]; then
138
+ target_minute=0
139
+ fi
140
+
141
+ # Check day of week constraint
142
+ if [ "$cron_dow" != "*" ]; then
143
+ if ! cron_field_matches "$cron_dow" "$CURRENT_DOW"; then
144
+ # Not the right day
145
+ return 1
146
+ fi
147
+ fi
148
+
149
+ # Check day of month constraint
150
+ if [ "$cron_dom" != "*" ]; then
151
+ if ! cron_field_matches "$cron_dom" "$CURRENT_DAY"; then
152
+ return 1
153
+ fi
154
+ fi
155
+
156
+ # Check month constraint
157
+ if [ "$cron_month" != "*" ]; then
158
+ if ! cron_field_matches "$cron_month" "$CURRENT_MONTH"; then
159
+ return 1
160
+ fi
161
+ fi
162
+
163
+ # Calculate the scheduled epoch for today at target_hour:target_minute
164
+ local today_date=$(date -u '+%Y-%m-%d')
165
+ local scheduled_time="${today_date}T$(printf '%02d' $target_hour):$(printf '%02d' $target_minute):00"
166
+ local scheduled_epoch=$(date -u -d "$scheduled_time" '+%s' 2>/dev/null || echo "0")
167
+
168
+ if [ "$scheduled_epoch" = "0" ]; then
169
+ echo " ⚠️ Failed to calculate scheduled epoch"
170
+ return 1
171
+ fi
172
+
173
+ echo " Scheduled time: $scheduled_time (epoch: $scheduled_epoch)"
174
+ echo " Last triggered: $last_triggered"
175
+ echo " Current epoch: $CURRENT_EPOCH"
176
+
177
+ # Check if:
178
+ # 1. The scheduled time has passed (scheduled_epoch <= CURRENT_EPOCH)
179
+ # 2. We haven't triggered since the scheduled time (last_triggered < scheduled_epoch)
180
+ # 3. We're within the catch-up window (CURRENT_EPOCH - scheduled_epoch <= CATCH_UP_WINDOW)
181
+
182
+ if [ "$scheduled_epoch" -le "$CURRENT_EPOCH" ] && \
183
+ [ "$last_triggered" -lt "$scheduled_epoch" ] && \
184
+ [ $((CURRENT_EPOCH - scheduled_epoch)) -le "$CATCH_UP_WINDOW" ]; then
185
+ echo " ✅ Should trigger (scheduled time passed and not yet triggered)"
186
+ return 0
187
+ fi
188
+
189
+ echo " ⏰ Not due (already triggered or not in window)"
190
+ return 1
191
+ }
192
+
193
+ # Function to check if run_at time has passed (within window)
194
+ run_at_matches() {
195
+ local run_at="$1"
196
+
197
+ # Parse ISO datetime and convert to epoch
198
+ local run_at_clean=$(echo "$run_at" | sed 's/Z$//')
199
+ local run_at_epoch=$(date -u -d "$run_at_clean" '+%s' 2>/dev/null || echo "0")
200
+
201
+ if [ "$run_at_epoch" = "0" ]; then
202
+ echo " ⚠️ Failed to parse run_at: $run_at"
203
+ return 1
204
+ fi
205
+
206
+ # Check if run_at is within the window (-1 to +CATCH_UP_WINDOW/60 minutes from now)
207
+ local diff=$((CURRENT_EPOCH - run_at_epoch))
208
+ local diff_minutes=$((diff / 60))
209
+ local window_minutes=$((CATCH_UP_WINDOW / 60))
210
+
211
+ if [ "$diff_minutes" -ge -1 ] && [ "$diff_minutes" -le "$window_minutes" ]; then
212
+ return 0
213
+ fi
214
+
215
+ return 1
216
+ }
217
+
218
+ # Parse and check each job
219
+ JOBS_TO_RUN=""
220
+ JOBS_TO_REMOVE=""
221
+ JOBS_TO_UPDATE=""
222
+ JOB_COUNT=0
223
+
224
+ # Use jq to iterate through jobs
225
+ for job_id in $(echo "$AGENT_SCHEDULES" | jq -r '.jobs | keys[]' 2>/dev/null); do
226
+ echo ""
227
+ echo "━━━ Checking job: $job_id ━━━"
228
+
229
+ # Get job details
230
+ enabled=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].enabled // true")
231
+ cron_expr=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].cron // \"\"")
232
+ run_at=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].run_at // \"\"")
233
+ once=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].once // false")
234
+ last_triggered=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].last_triggered // 0")
235
+
236
+ if [ "$enabled" != "true" ]; then
237
+ echo " ⏭️ Skipped (disabled)"
238
+ continue
239
+ fi
240
+
241
+ should_run=false
242
+
243
+ # Check cron expression with window-based approach
244
+ if [ -n "$cron_expr" ] && [ "$cron_expr" != "null" ]; then
245
+ echo " Cron: $cron_expr"
246
+ if [ "${{ inputs.force_check }}" = "true" ]; then
247
+ echo " ✅ Force check enabled"
248
+ should_run=true
249
+ elif cron_should_trigger "$cron_expr" "$last_triggered"; then
250
+ should_run=true
251
+ # Mark for timestamp update
252
+ JOBS_TO_UPDATE="$JOBS_TO_UPDATE $job_id"
253
+ fi
254
+ fi
255
+
256
+ # Check run_at datetime
257
+ if [ -n "$run_at" ] && [ "$run_at" != "null" ]; then
258
+ echo " Run At: $run_at"
259
+ if [ "${{ inputs.force_check }}" = "true" ] || run_at_matches "$run_at"; then
260
+ echo " ✅ Run At MATCH"
261
+ should_run=true
262
+ if [ "$once" = "true" ]; then
263
+ echo " 🗑️ Will be removed after dispatch (once=true)"
264
+ JOBS_TO_REMOVE="$JOBS_TO_REMOVE $job_id"
265
+ fi
266
+ else
267
+ echo " ⏰ Run At not due yet"
268
+ fi
269
+ fi
270
+
271
+ if [ "$should_run" = "true" ]; then
272
+ JOBS_TO_RUN="$JOBS_TO_RUN $job_id"
273
+ JOB_COUNT=$((JOB_COUNT + 1))
274
+ fi
275
+ done
276
+
277
+ echo ""
278
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
279
+
280
+ if [ $JOB_COUNT -eq 0 ]; then
281
+ echo "📭 No jobs scheduled to run at this time"
282
+ exit 0
283
+ fi
284
+
285
+ echo "🚀 Dispatching $JOB_COUNT job(s)..."
286
+
287
+ # Use PAT_TOKEN if available (required for workflow dispatch), fallback to GITHUB_TOKEN
288
+ TOKEN="${PAT_TOKEN:-$GITHUB_TOKEN}"
289
+
290
+ # Track successfully dispatched jobs for timestamp update
291
+ DISPATCHED_JOBS=""
292
+
293
+ # Dispatch each matching job
294
+ for job_id in $JOBS_TO_RUN; do
295
+ echo ""
296
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
297
+ echo "📤 Dispatching: $job_id"
298
+
299
+ # Extract job configuration
300
+ prompt=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].prompt // \"\"")
301
+ system_prompt=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].system_prompt // \"\"")
302
+ tools=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].tools // \"\"")
303
+ model=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].model // \"\"")
304
+ max_tokens=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].max_tokens // \"\"")
305
+ context=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].context // \"\"")
306
+
307
+ echo " Prompt: ${prompt:0:80}..."
308
+
309
+ # Build the full prompt with context
310
+ full_prompt="[Scheduled Job: $job_id]\n\n$prompt"
311
+ if [ -n "$context" ] && [ "$context" != "null" ]; then
312
+ full_prompt="$full_prompt\n\nContext:\n$context"
313
+ fi
314
+
315
+ # Build inputs JSON
316
+ inputs_json=$(jq -n \
317
+ --arg prompt "$full_prompt" \
318
+ --arg system_prompt "$system_prompt" \
319
+ --arg tools "$tools" \
320
+ --arg model "$model" \
321
+ --arg max_tokens "$max_tokens" \
322
+ '{prompt: $prompt}
323
+ | if $system_prompt != "" and $system_prompt != "null" then . + {system_prompt: $system_prompt} else . end
324
+ | if $tools != "" and $tools != "null" then . + {tools: $tools} else . end
325
+ | if $model != "" and $model != "null" then . + {model: $model} else . end
326
+ | if $max_tokens != "" and $max_tokens != "null" then . + {max_tokens: $max_tokens} else . end')
327
+
328
+ echo " Inputs: $(echo "$inputs_json" | jq -c .)"
329
+
330
+ # Dispatch the workflow
331
+ response=$(curl -s -w "\n%{http_code}" -X POST \
332
+ -H "Accept: application/vnd.github+json" \
333
+ -H "Authorization: Bearer $TOKEN" \
334
+ -H "X-GitHub-Api-Version: 2022-11-28" \
335
+ "https://api.github.com/repos/${{ github.repository }}/actions/workflows/agent.yml/dispatches" \
336
+ -d "{\"ref\": \"main\", \"inputs\": $inputs_json}")
337
+
338
+ http_code=$(echo "$response" | tail -n1)
339
+ body=$(echo "$response" | sed '$d')
340
+
341
+ if [ "$http_code" = "204" ]; then
342
+ echo " ✅ Dispatched successfully"
343
+ DISPATCHED_JOBS="$DISPATCHED_JOBS $job_id"
344
+ else
345
+ echo " ❌ Failed to dispatch: HTTP $http_code"
346
+ echo " Response: $body"
347
+ fi
348
+ done
349
+
350
+ # Update AGENT_SCHEDULES with last_triggered timestamps and remove once=true jobs
351
+ updated_schedules="$AGENT_SCHEDULES"
352
+
353
+ # Update last_triggered for dispatched cron jobs
354
+ for job_id in $DISPATCHED_JOBS; do
355
+ # Only update if it's a cron job (not a one-time job being removed)
356
+ if echo "$JOBS_TO_UPDATE" | grep -qw "$job_id"; then
357
+ echo ""
358
+ echo "📝 Updating last_triggered for: $job_id"
359
+ updated_schedules=$(echo "$updated_schedules" | jq ".jobs[\"$job_id\"].last_triggered = $CURRENT_EPOCH")
360
+ fi
361
+ done
362
+
363
+ # Remove once=true jobs that have been dispatched
364
+ for job_id in $JOBS_TO_REMOVE; do
365
+ if echo "$DISPATCHED_JOBS" | grep -qw "$job_id"; then
366
+ echo ""
367
+ echo "🗑️ Removing one-time job: $job_id"
368
+ updated_schedules=$(echo "$updated_schedules" | jq "del(.jobs[\"$job_id\"])")
369
+ fi
370
+ done
371
+
372
+ # Save updated schedules if any changes
373
+ if [ "$updated_schedules" != "$AGENT_SCHEDULES" ]; then
374
+ echo ""
375
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
376
+ echo "💾 Saving updated schedule..."
377
+
378
+ update_response=$(curl -s -w "\n%{http_code}" -X PATCH \
379
+ -H "Accept: application/vnd.github+json" \
380
+ -H "Authorization: Bearer $TOKEN" \
381
+ -H "X-GitHub-Api-Version: 2022-11-28" \
382
+ "https://api.github.com/repos/${{ github.repository }}/actions/variables/AGENT_SCHEDULES" \
383
+ -d "{\"name\": \"AGENT_SCHEDULES\", \"value\": $(echo "$updated_schedules" | jq -c . | jq -Rs .)}")
384
+
385
+ update_code=$(echo "$update_response" | tail -n1)
386
+ if [ "$update_code" = "204" ]; then
387
+ echo " ✅ Schedule updated successfully"
388
+ else
389
+ echo " ⚠️ Failed to update schedule: HTTP $update_code"
390
+ fi
391
+ fi
392
+
393
+ echo ""
394
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
395
+ echo "🏁 Control loop complete - dispatched $JOB_COUNT job(s)"
@@ -0,0 +1,2 @@
1
+ __pycache__
2
+ artifacts/github_exports