strands-coder 1.2.0__tar.gz → 1.4.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.
Files changed (25) hide show
  1. {strands_coder-1.2.0 → strands_coder-1.4.0}/.github/workflows/control.yml +54 -200
  2. {strands_coder-1.2.0 → strands_coder-1.4.0}/PKG-INFO +1 -1
  3. {strands_coder-1.2.0 → strands_coder-1.4.0}/docs/index.html +25 -7
  4. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/context.py +73 -0
  5. {strands_coder-1.2.0 → strands_coder-1.4.0}/.github/workflows/agent.yml +0 -0
  6. {strands_coder-1.2.0 → strands_coder-1.4.0}/.github/workflows/auto-release.yml +0 -0
  7. {strands_coder-1.2.0 → strands_coder-1.4.0}/.gitignore +0 -0
  8. {strands_coder-1.2.0 → strands_coder-1.4.0}/LICENSE +0 -0
  9. {strands_coder-1.2.0 → strands_coder-1.4.0}/README.md +0 -0
  10. {strands_coder-1.2.0 → strands_coder-1.4.0}/SYSTEM_PROMPT.md +0 -0
  11. {strands_coder-1.2.0 → strands_coder-1.4.0}/action.yml +0 -0
  12. {strands_coder-1.2.0 → strands_coder-1.4.0}/docs/CNAME +0 -0
  13. {strands_coder-1.2.0 → strands_coder-1.4.0}/pyproject.toml +0 -0
  14. {strands_coder-1.2.0 → strands_coder-1.4.0}/setup-aws-oidc.sh +0 -0
  15. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/__init__.py +0 -0
  16. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/agent_runner.py +0 -0
  17. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/__init__.py +0 -0
  18. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/create_subagent.py +0 -0
  19. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/github_tools.py +0 -0
  20. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/projects.py +0 -0
  21. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/scheduler.py +0 -0
  22. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/store_in_kb.py +0 -0
  23. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/system_prompt.py +0 -0
  24. {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/use_github.py +0 -0
  25. {strands_coder-1.2.0 → strands_coder-1.4.0}/tools/use_langfuse.py +0 -0
@@ -32,245 +32,126 @@ jobs:
32
32
  echo "🕐 Control Loop - $(date -u '+%Y-%m-%d %H:%M') UTC"
33
33
  echo "Repository: ${{ github.repository }}"
34
34
 
35
- # Parse AGENT_SCHEDULES JSON
36
35
  if [ -z "$AGENT_SCHEDULES" ] || [ "$AGENT_SCHEDULES" = "{}" ]; then
37
36
  echo "ℹ️ No schedules configured (AGENT_SCHEDULES is empty)"
38
- echo "To add schedules, use the scheduler tool or set the AGENT_SCHEDULES variable"
39
37
  exit 0
40
38
  fi
41
39
 
42
- # Get current time components (UTC)
43
40
  CURRENT_MINUTE=$(date -u '+%M' | sed 's/^0//')
44
41
  CURRENT_HOUR=$(date -u '+%H' | sed 's/^0//')
45
42
  CURRENT_DAY=$(date -u '+%d' | sed 's/^0//')
46
43
  CURRENT_MONTH=$(date -u '+%m' | sed 's/^0//')
47
- CURRENT_DOW=$(date -u '+%w') # 0=Sunday
44
+ CURRENT_DOW=$(date -u '+%w')
48
45
  CURRENT_EPOCH=$(date -u '+%s')
49
46
 
50
- # Handle empty values (when minute/hour is 0)
51
47
  [ -z "$CURRENT_MINUTE" ] && CURRENT_MINUTE=0
52
48
  [ -z "$CURRENT_HOUR" ] && CURRENT_HOUR=0
53
49
  [ -z "$CURRENT_DAY" ] && CURRENT_DAY=1
54
50
  [ -z "$CURRENT_MONTH" ] && CURRENT_MONTH=1
55
51
 
56
- echo "Current time: minute=$CURRENT_MINUTE hour=$CURRENT_HOUR day=$CURRENT_DAY month=$CURRENT_MONTH dow=$CURRENT_DOW epoch=$CURRENT_EPOCH"
52
+ echo "Current time: minute=$CURRENT_MINUTE hour=$CURRENT_HOUR day=$CURRENT_DAY month=$CURRENT_MONTH dow=$CURRENT_DOW"
57
53
 
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
54
  CATCH_UP_WINDOW=86400
62
55
 
63
- # Function to check if a cron field matches
64
56
  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
57
+ local field="$1" value="$2"
58
+ if [ "$field" = "*" ]; then return 0; fi
74
59
  if [[ "$field" == "*/"* ]]; then
75
- local step="${field#*/}"
76
- if [ $((value % step)) -eq 0 ]; then
77
- return 0
78
- fi
79
- return 1
60
+ local step="${field#*/}"; [ $((value % step)) -eq 0 ] && return 0; return 1
80
61
  fi
81
-
82
- # Lists like 1,3,5
83
62
  if [[ "$field" == *","* ]]; then
84
63
  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
64
+ for v in "${VALUES[@]}"; do [ "$v" -eq "$value" ] 2>/dev/null && return 0; done; return 1
91
65
  fi
92
-
93
- # Ranges like 1-5
94
66
  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
67
+ local start="${field%-*}" end="${field#*-}"
68
+ [ "$value" -ge "$start" ] && [ "$value" -le "$end" ] && return 0; return 1
101
69
  fi
102
-
103
- # Direct match
104
- if [ "$field" -eq "$value" ] 2>/dev/null; then
105
- return 0
106
- fi
107
-
108
- return 1
70
+ [ "$field" -eq "$value" ] 2>/dev/null && return 0; return 1
109
71
  }
110
72
 
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
73
  cron_should_trigger() {
114
- local cron="$1"
115
- local last_triggered="$2" # epoch timestamp or empty
116
-
74
+ local cron="$1" last_triggered="$2"
117
75
  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
76
  if [ -z "$last_triggered" ] || [ "$last_triggered" = "null" ] || [ "$last_triggered" = "0" ]; then
122
77
  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
78
  fi
162
-
163
- # Calculate the scheduled epoch for today at target_hour:target_minute
79
+ local target_hour="$cron_hour" target_minute="$cron_minute"
80
+ [ "$target_hour" = "*" ] && target_hour="$CURRENT_HOUR"
81
+ [ "$target_minute" = "*" ] && target_minute=0
82
+ [ "$cron_dow" != "*" ] && ! cron_field_matches "$cron_dow" "$CURRENT_DOW" && return 1
83
+ [ "$cron_dom" != "*" ] && ! cron_field_matches "$cron_dom" "$CURRENT_DAY" && return 1
84
+ [ "$cron_month" != "*" ] && ! cron_field_matches "$cron_month" "$CURRENT_MONTH" && return 1
164
85
  local today_date=$(date -u '+%Y-%m-%d')
165
86
  local scheduled_time="${today_date}T$(printf '%02d' $target_hour):$(printf '%02d' $target_minute):00"
166
87
  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
-
88
+ [ "$scheduled_epoch" = "0" ] && return 1
182
89
  if [ "$scheduled_epoch" -le "$CURRENT_EPOCH" ] && \
183
90
  [ "$last_triggered" -lt "$scheduled_epoch" ] && \
184
91
  [ $((CURRENT_EPOCH - scheduled_epoch)) -le "$CATCH_UP_WINDOW" ]; then
185
- echo " ✅ Should trigger (scheduled time passed and not yet triggered)"
186
92
  return 0
187
93
  fi
188
-
189
- echo " ⏰ Not due (already triggered or not in window)"
190
94
  return 1
191
95
  }
192
96
 
193
- # Function to check if run_at time has passed (within window)
194
97
  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$//')
98
+ local run_at_clean=$(echo "$1" | sed 's/Z$//')
199
99
  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
-
100
+ [ "$run_at_epoch" = "0" ] && return 1
101
+ local diff_minutes=$(( (CURRENT_EPOCH - run_at_epoch) / 60 ))
102
+ [ "$diff_minutes" -ge -1 ] && [ "$diff_minutes" -le $((CATCH_UP_WINDOW / 60)) ] && return 0
215
103
  return 1
216
104
  }
217
105
 
218
- # Parse and check each job
219
- JOBS_TO_RUN=""
220
- JOBS_TO_REMOVE=""
221
- JOBS_TO_UPDATE=""
222
- JOB_COUNT=0
106
+ # Parse workflow target: "file.yml" → same repo, "owner/repo/file.yml" → cross-repo
107
+ parse_workflow_target() {
108
+ local workflow="$1"
109
+ local slash_count=$(echo "$workflow" | tr -cd '/' | wc -c | tr -d ' ')
110
+ if [ "$slash_count" -ge 2 ]; then
111
+ DISPATCH_REPO=$(echo "$workflow" | cut -d'/' -f1-2)
112
+ DISPATCH_WORKFLOW=$(echo "$workflow" | cut -d'/' -f3-)
113
+ else
114
+ DISPATCH_REPO="${{ github.repository }}"
115
+ DISPATCH_WORKFLOW="$workflow"
116
+ fi
117
+ }
118
+
119
+ JOBS_TO_RUN="" JOBS_TO_REMOVE="" JOBS_TO_UPDATE="" JOB_COUNT=0
223
120
 
224
- # Use jq to iterate through jobs
225
121
  for job_id in $(echo "$AGENT_SCHEDULES" | jq -r '.jobs | keys[]' 2>/dev/null); do
226
122
  echo ""
227
123
  echo "━━━ Checking job: $job_id ━━━"
228
-
229
- # Get job details
230
124
  enabled=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].enabled // true")
231
125
  cron_expr=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].cron // \"\"")
232
126
  run_at=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].run_at // \"\"")
233
127
  once=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].once // false")
234
128
  last_triggered=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].last_triggered // 0")
235
129
 
236
- if [ "$enabled" != "true" ]; then
237
- echo " ⏭️ Skipped (disabled)"
238
- continue
239
- fi
240
-
130
+ [ "$enabled" != "true" ] && echo " ⏭️ Skipped (disabled)" && continue
241
131
  should_run=false
242
132
 
243
- # Check cron expression with window-based approach
244
133
  if [ -n "$cron_expr" ] && [ "$cron_expr" != "null" ]; then
245
134
  echo " Cron: $cron_expr"
246
135
  if [ "${{ inputs.force_check }}" = "true" ]; then
247
- echo " ✅ Force check enabled"
248
136
  should_run=true
249
137
  elif cron_should_trigger "$cron_expr" "$last_triggered"; then
250
138
  should_run=true
251
- # Mark for timestamp update
252
139
  JOBS_TO_UPDATE="$JOBS_TO_UPDATE $job_id"
253
140
  fi
254
141
  fi
255
142
 
256
- # Check run_at datetime
257
143
  if [ -n "$run_at" ] && [ "$run_at" != "null" ]; then
258
144
  echo " Run At: $run_at"
259
145
  if [ "${{ inputs.force_check }}" = "true" ] || run_at_matches "$run_at"; then
260
- echo " ✅ Run At MATCH"
261
146
  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"
147
+ [ "$once" = "true" ] && JOBS_TO_REMOVE="$JOBS_TO_REMOVE $job_id"
268
148
  fi
269
149
  fi
270
150
 
271
151
  if [ "$should_run" = "true" ]; then
272
152
  JOBS_TO_RUN="$JOBS_TO_RUN $job_id"
273
153
  JOB_COUNT=$((JOB_COUNT + 1))
154
+ echo " ✅ Will dispatch"
274
155
  fi
275
156
  done
276
157
 
@@ -283,36 +164,31 @@ jobs:
283
164
  fi
284
165
 
285
166
  echo "🚀 Dispatching $JOB_COUNT job(s)..."
286
-
287
- # Use PAT_TOKEN if available (required for workflow dispatch), fallback to GITHUB_TOKEN
288
167
  TOKEN="${PAT_TOKEN:-$GITHUB_TOKEN}"
289
-
290
- # Track successfully dispatched jobs for timestamp update
291
168
  DISPATCHED_JOBS=""
292
169
 
293
- # Dispatch each matching job
294
170
  for job_id in $JOBS_TO_RUN; do
295
171
  echo ""
296
172
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
297
173
  echo "📤 Dispatching: $job_id"
298
174
 
299
- # Extract job configuration
300
175
  prompt=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].prompt // \"\"")
301
176
  system_prompt=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].system_prompt // \"\"")
302
177
  tools=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].tools // \"\"")
303
178
  model=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].model // \"\"")
304
179
  max_tokens=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].max_tokens // \"\"")
305
180
  context=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].context // \"\"")
181
+ target_workflow=$(echo "$AGENT_SCHEDULES" | jq -r ".jobs[\"$job_id\"].workflow // \"agent.yml\"")
182
+
183
+ # Parse workflow: "agent.yml" → same repo, "owner/repo/workflow.yml" → cross-repo
184
+ parse_workflow_target "$target_workflow"
306
185
 
186
+ echo " Target: $DISPATCH_REPO/$DISPATCH_WORKFLOW"
307
187
  echo " Prompt: ${prompt:0:80}..."
308
188
 
309
- # Build the full prompt with context
310
189
  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
190
+ [ -n "$context" ] && [ "$context" != "null" ] && full_prompt="$full_prompt\n\nContext:\n$context"
314
191
 
315
- # Build inputs JSON
316
192
  inputs_json=$(jq -n \
317
193
  --arg prompt "$full_prompt" \
318
194
  --arg system_prompt "$system_prompt" \
@@ -325,71 +201,49 @@ jobs:
325
201
  | if $model != "" and $model != "null" then . + {model: $model} else . end
326
202
  | if $max_tokens != "" and $max_tokens != "null" then . + {max_tokens: $max_tokens} else . end')
327
203
 
328
- echo " Inputs: $(echo "$inputs_json" | jq -c .)"
329
-
330
- # Dispatch the workflow
331
204
  response=$(curl -s -w "\n%{http_code}" -X POST \
332
205
  -H "Accept: application/vnd.github+json" \
333
206
  -H "Authorization: Bearer $TOKEN" \
334
207
  -H "X-GitHub-Api-Version: 2022-11-28" \
335
- "https://api.github.com/repos/${{ github.repository }}/actions/workflows/agent.yml/dispatches" \
208
+ "https://api.github.com/repos/${DISPATCH_REPO}/actions/workflows/${DISPATCH_WORKFLOW}/dispatches" \
336
209
  -d "{\"ref\": \"main\", \"inputs\": $inputs_json}")
337
210
 
338
211
  http_code=$(echo "$response" | tail -n1)
339
212
  body=$(echo "$response" | sed '$d')
340
213
 
341
214
  if [ "$http_code" = "204" ]; then
342
- echo " ✅ Dispatched successfully"
215
+ echo " ✅ Dispatched to $DISPATCH_REPO/$DISPATCH_WORKFLOW"
343
216
  DISPATCHED_JOBS="$DISPATCHED_JOBS $job_id"
344
217
  else
345
- echo " ❌ Failed to dispatch: HTTP $http_code"
346
- echo " Response: $body"
218
+ echo " ❌ Failed ($DISPATCH_REPO/$DISPATCH_WORKFLOW): HTTP $http_code"
219
+ echo " $body"
347
220
  fi
348
221
  done
349
222
 
350
- # Update AGENT_SCHEDULES with last_triggered timestamps and remove once=true jobs
223
+ # Update timestamps and cleanup
351
224
  updated_schedules="$AGENT_SCHEDULES"
352
225
 
353
- # Update last_triggered for dispatched cron jobs
354
226
  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"
227
+ echo "$JOBS_TO_UPDATE" | grep -qw "$job_id" && \
359
228
  updated_schedules=$(echo "$updated_schedules" | jq ".jobs[\"$job_id\"].last_triggered = $CURRENT_EPOCH")
360
- fi
361
229
  done
362
230
 
363
- # Remove once=true jobs that have been dispatched
364
231
  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"
232
+ echo "$DISPATCHED_JOBS" | grep -qw "$job_id" && \
368
233
  updated_schedules=$(echo "$updated_schedules" | jq "del(.jobs[\"$job_id\"])")
369
- fi
370
234
  done
371
235
 
372
- # Save updated schedules if any changes
373
236
  if [ "$updated_schedules" != "$AGENT_SCHEDULES" ]; then
374
237
  echo ""
375
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
376
238
  echo "💾 Saving updated schedule..."
377
-
378
- update_response=$(curl -s -w "\n%{http_code}" -X PATCH \
239
+ curl -s -o /dev/null -w "%{http_code}" -X PATCH \
379
240
  -H "Accept: application/vnd.github+json" \
380
241
  -H "Authorization: Bearer $TOKEN" \
381
242
  -H "X-GitHub-Api-Version: 2022-11-28" \
382
243
  "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
244
+ -d "{\"name\": \"AGENT_SCHEDULES\", \"value\": $(echo "$updated_schedules" | jq -c . | jq -Rs .)}" \
245
+ | xargs -I{} echo " Schedule save: HTTP {}"
391
246
  fi
392
247
 
393
248
  echo ""
394
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
395
249
  echo "🏁 Control loop complete - dispatched $JOB_COUNT job(s)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strands-coder
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Add an AI assistant to your GitHub repositories that can review code, manage issues, and improve over time.
5
5
  Project-URL: Homepage, https://github.com/cagataycali/strands-coder
6
6
  Project-URL: Bug Tracker, https://github.com/cagataycali/strands-coder/issues
@@ -648,6 +648,11 @@
648
648
  <input class="form-input" id="jobCron" placeholder="0 9 * * *">
649
649
  <div style="font-size:12px;color:rgba(255,255,255,0.4);margin-top:6px;">Format: minute hour day month weekday</div>
650
650
  </div>
651
+ <div class="form-group">
652
+ <label class="form-label">Workflow Target</label>
653
+ <input class="form-input" id="jobWorkflow" placeholder="agent.yml" value="agent.yml">
654
+ <div style="font-size:12px;color:rgba(255,255,255,0.4);margin-top:6px;">🖥️ agent.yml · 🤖 thor.yml · 🎮 isaac-sim.yml · 🌐 owner/repo/file.yml</div>
655
+ </div>
651
656
  <div class="form-group">
652
657
  <label class="form-label">Prompt</label>
653
658
  <textarea class="form-input" id="jobPrompt" rows="4"></textarea>
@@ -1673,13 +1678,15 @@
1673
1678
  container.innerHTML = jobs.map(jobId => {
1674
1679
  const job = scheduleData.jobs[jobId];
1675
1680
  const enabled = job.enabled !== false;
1681
+ const wf = job.workflow || 'agent.yml';
1682
+ const wfIcon = wf.includes('thor') ? '🤖' : wf.includes('isaac') ? '🎮' : wf.includes('/') && wf.split('/').length >= 3 ? '🌐' : '🖥️';
1676
1683
  return `
1677
1684
  <div class="job-item ${enabled ? '' : 'disabled'} ${selectedJob === jobId ? 'selected' : ''}" onclick="window.selectJobDetail('${jobId}')">
1678
1685
  <div class="job-title">
1679
- <span style="color:${enabled ? '#fff' : 'rgba(255,255,255,0.4)'};">${escapeHtml(jobId)}</span>
1686
+ <span style="color:${enabled ? '#fff' : 'rgba(255,255,255,0.4)'};">${wfIcon} ${escapeHtml(jobId)}</span>
1680
1687
  <span class="badge" style="background:${enabled ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.05)'};">${enabled ? 'ON' : 'OFF'}</span>
1681
1688
  </div>
1682
- <div class="job-cron">${job.cron || job.run_at || '-'}</div>
1689
+ <div class="job-cron">${job.cron || job.run_at || '-'} · <span style="opacity:0.6">${wf}</span></div>
1683
1690
  </div>
1684
1691
  `;
1685
1692
  }).join('');
@@ -1700,10 +1707,12 @@
1700
1707
 
1701
1708
  const job = scheduleData.jobs[selectedJob];
1702
1709
  const enabled = job.enabled !== false;
1710
+ const wf = job.workflow || 'agent.yml';
1711
+ const wfIcon = wf.includes('thor') ? '🤖' : wf.includes('isaac') ? '🎮' : wf.includes('/') && wf.split('/').length >= 3 ? '🌐' : '🖥️';
1703
1712
 
1704
1713
  container.innerHTML = `
1705
1714
  <div class="job-detail-header">
1706
- <h4 style="margin:0;">${escapeHtml(selectedJob)}</h4>
1715
+ <h4 style="margin:0;">${wfIcon} ${escapeHtml(selectedJob)}</h4>
1707
1716
  <div class="job-detail-actions">
1708
1717
  <button class="btn btn-sm" onclick="window.toggleJobEnabled('${selectedJob}')">${enabled ? '⏸ Disable' : '▶ Enable'}</button>
1709
1718
  <button class="btn btn-sm" onclick="window.editJob('${selectedJob}')">✏️ Edit</button>
@@ -1711,9 +1720,15 @@
1711
1720
  </div>
1712
1721
  </div>
1713
1722
 
1714
- <div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:16px;margin-bottom:14px;">
1715
- <div style="font-size:12px;color:rgba(255,255,255,0.5);text-transform:uppercase;margin-bottom:6px;">Schedule</div>
1716
- <div style="font-size:16px;font-family:'SF Mono',monospace;color:#fff;">${job.cron || job.run_at || '-'}</div>
1723
+ <div style="display:flex;gap:10px;margin-bottom:14px;">
1724
+ <div style="flex:1;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:16px;">
1725
+ <div style="font-size:12px;color:rgba(255,255,255,0.5);text-transform:uppercase;margin-bottom:6px;">Schedule</div>
1726
+ <div style="font-size:16px;font-family:'SF Mono',monospace;color:#fff;">${job.cron || job.run_at || '-'}</div>
1727
+ </div>
1728
+ <div style="flex:1;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:16px;">
1729
+ <div style="font-size:12px;color:rgba(255,255,255,0.5);text-transform:uppercase;margin-bottom:6px;">Workflow</div>
1730
+ <div style="font-size:14px;font-family:'SF Mono',monospace;color:#fff;">${wfIcon} ${escapeHtml(wf)}</div>
1731
+ </div>
1717
1732
  </div>
1718
1733
 
1719
1734
  <div style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:16px;">
@@ -1729,6 +1744,7 @@
1729
1744
  document.getElementById('jobId').value = '';
1730
1745
  document.getElementById('jobId').disabled = false;
1731
1746
  document.getElementById('jobCron').value = '';
1747
+ document.getElementById('jobWorkflow').value = 'agent.yml';
1732
1748
  document.getElementById('jobPrompt').value = '';
1733
1749
  };
1734
1750
 
@@ -1741,6 +1757,7 @@
1741
1757
  document.getElementById('jobId').value = jobId;
1742
1758
  document.getElementById('jobId').disabled = true;
1743
1759
  document.getElementById('jobCron').value = job.cron || '';
1760
+ document.getElementById('jobWorkflow').value = job.workflow || 'agent.yml';
1744
1761
  document.getElementById('jobPrompt').value = job.prompt || '';
1745
1762
  };
1746
1763
 
@@ -1764,12 +1781,13 @@
1764
1781
  const jobId = document.getElementById('jobId').value.trim();
1765
1782
  const cron = document.getElementById('jobCron').value.trim();
1766
1783
  const prompt = document.getElementById('jobPrompt').value.trim();
1784
+ const workflow = document.getElementById('jobWorkflow').value.trim() || 'agent.yml';
1767
1785
  if (!jobId || !cron || !prompt) return showToast('Fill all');
1768
1786
 
1769
1787
  if (!scheduleData.jobs) scheduleData.jobs = {};
1770
1788
 
1771
1789
  const existingEnabled = scheduleData.jobs[jobId]?.enabled;
1772
- scheduleData.jobs[jobId] = { cron, prompt, enabled: existingEnabled !== undefined ? existingEnabled : true };
1790
+ scheduleData.jobs[jobId] = { cron, prompt, workflow, enabled: existingEnabled !== undefined ? existingEnabled : true };
1773
1791
 
1774
1792
  try {
1775
1793
  await saveScheduleToGithub();
@@ -783,6 +783,74 @@ def extract_user_message() -> str:
783
783
  return ""
784
784
 
785
785
 
786
+ def load_agents_md() -> str:
787
+ """
788
+ Load AGENTS.md from the workspace for project-specific rules and learnings.
789
+
790
+ Searches for AGENTS.md in:
791
+ 1. Current working directory (repo root in CI)
792
+ 2. GITHUB_WORKSPACE env var (GitHub Actions workspace)
793
+ 3. Parent directories up to 3 levels (for monorepo support)
794
+
795
+ AGENTS.md is a living document that contains:
796
+ - Code standards and non-negotiable rules
797
+ - Learnings from past reviews and bugs
798
+ - Patterns to follow for new contributions
799
+ - Self-update protocol for autonomous agents
800
+
801
+ Returns formatted markdown for injection into system prompt.
802
+ """
803
+ search_paths = []
804
+
805
+ # 1. Current working directory
806
+ cwd = Path.cwd()
807
+ search_paths.append(cwd / "AGENTS.md")
808
+
809
+ # 2. GitHub Actions workspace
810
+ workspace = os.environ.get("GITHUB_WORKSPACE")
811
+ if workspace:
812
+ search_paths.append(Path(workspace) / "AGENTS.md")
813
+
814
+ # 3. Parent directories (up to 3 levels for monorepos)
815
+ for i in range(1, 4):
816
+ parent = cwd
817
+ for _ in range(i):
818
+ parent = parent.parent
819
+ search_paths.append(parent / "AGENTS.md")
820
+
821
+ # Deduplicate while preserving order
822
+ seen: set[str] = set()
823
+ unique_paths = []
824
+ for p in search_paths:
825
+ resolved = str(p.resolve())
826
+ if resolved not in seen:
827
+ seen.add(resolved)
828
+ unique_paths.append(p)
829
+
830
+ for agents_path in unique_paths:
831
+ try:
832
+ if agents_path.exists() and agents_path.is_file():
833
+ content = agents_path.read_text(encoding="utf-8", errors="ignore")
834
+ if content.strip():
835
+ print(f"✓ AGENTS.md loaded from {agents_path} ({len(content)} chars)")
836
+ return f"""
837
+ ---
838
+ ## 📋 AGENTS.md — Project Rules & Learnings
839
+
840
+ **Source:** `{agents_path}`
841
+ **Last Modified:** {datetime.fromtimestamp(agents_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")}
842
+
843
+ {content}
844
+
845
+ ---
846
+ """
847
+ except Exception as e:
848
+ print(f"⚠ Failed to read {agents_path}: {e}")
849
+ continue
850
+
851
+ return ""
852
+
853
+
786
854
  def build_system_prompt() -> str:
787
855
  """Build comprehensive system prompt from environment variables and context."""
788
856
  # Base system prompt
@@ -797,6 +865,11 @@ def build_system_prompt() -> str:
797
865
  if input_system_prompt:
798
866
  base_prompt = f"{base_prompt}\n\n{input_system_prompt}"
799
867
 
868
+ # Add AGENTS.md project rules and learnings (loaded EARLY so all context is framed by rules)
869
+ agents_md = load_agents_md()
870
+ if agents_md:
871
+ base_prompt = f"{base_prompt}\n\n{agents_md}"
872
+
800
873
  # Add rich GitHub event context (issue threads, PR reviews, etc.)
801
874
  github_event_context = fetch_github_event_context()
802
875
  if github_event_context:
File without changes
File without changes
File without changes
File without changes
File without changes