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.
- {strands_coder-1.2.0 → strands_coder-1.4.0}/.github/workflows/control.yml +54 -200
- {strands_coder-1.2.0 → strands_coder-1.4.0}/PKG-INFO +1 -1
- {strands_coder-1.2.0 → strands_coder-1.4.0}/docs/index.html +25 -7
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/context.py +73 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/.github/workflows/agent.yml +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/.github/workflows/auto-release.yml +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/.gitignore +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/LICENSE +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/README.md +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/SYSTEM_PROMPT.md +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/action.yml +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/docs/CNAME +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/pyproject.toml +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/setup-aws-oidc.sh +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/__init__.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/agent_runner.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/__init__.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/create_subagent.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/github_tools.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/projects.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/scheduler.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/store_in_kb.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/system_prompt.py +0 -0
- {strands_coder-1.2.0 → strands_coder-1.4.0}/strands_coder/tools/use_github.py +0 -0
- {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')
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
202
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/${
|
|
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
|
|
215
|
+
echo " ✅ Dispatched to $DISPATCH_REPO/$DISPATCH_WORKFLOW"
|
|
343
216
|
DISPATCHED_JOBS="$DISPATCHED_JOBS $job_id"
|
|
344
217
|
else
|
|
345
|
-
echo " ❌ Failed
|
|
346
|
-
echo "
|
|
218
|
+
echo " ❌ Failed ($DISPATCH_REPO/$DISPATCH_WORKFLOW): HTTP $http_code"
|
|
219
|
+
echo " $body"
|
|
347
220
|
fi
|
|
348
221
|
done
|
|
349
222
|
|
|
350
|
-
# Update
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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="
|
|
1715
|
-
<div style="
|
|
1716
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|