ecoportal-api-v2 3.3.2 → 3.3.3

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,307 @@
1
+ #!/usr/bin/env bash
2
+ # auto-worker-scheduler.sh
3
+ # Scheduler for auto-todo-worker. Called by the Claude Code Stop hook.
4
+ # Reads local config, checks token budget and conflict guard, then
5
+ # launches the worker as a background process if all gates pass.
6
+ #
7
+ # Log format: TIMESTAMP | ACTION | REASON
8
+ # Log file: .ai-assistance/local/auto-worker-changes.log
9
+ #
10
+ # Usage: bash scripts/auto-worker-scheduler.sh
11
+ # (called automatically by Stop hook — no arguments)
12
+ #
13
+ # Requires: jq (for JSON parsing). Falls back to grep/sed on missing jq.
14
+
15
+ set -euo pipefail
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Paths (all relative — script must be run from repo root)
19
+ # ---------------------------------------------------------------------------
20
+ TOKEN_BUDGET_JSON=".ai-assistance/token-budget.json"
21
+ LAST_RUN_JSON=".ai-assistance/local/auto-worker-last-run.json"
22
+ SESSION_LOCK=".ai-assistance/local/session.lock"
23
+ CHANGES_LOG=".ai-assistance/local/auto-worker-changes.log"
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Helpers
27
+ # ---------------------------------------------------------------------------
28
+
29
+ # ai_log_usage <component> <action> <detail> [session_id]
30
+ # Inline copy — scheduler is self-contained; common.sh is not sourced here.
31
+ # Never fails fatally; all errors suppressed.
32
+ ai_log_usage() {
33
+ local component="${1:-unknown}"
34
+ local action="${2:-run}"
35
+ local detail="${3:-}"
36
+ local session_id="${4:-}"
37
+ local log_dir=".ai-assistance/local/kpi"
38
+ local week
39
+ week=$(date +%Y-W%V 2>/dev/null || echo "unknown")
40
+ local log_file="${log_dir}/usage-${week}.jsonl"
41
+ local ts
42
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")
43
+ mkdir -p "$log_dir" 2>/dev/null || true
44
+ printf '{"component":"%s","action":"%s","detail":"%s","ts":"%s","session_id":"%s"}\n' \
45
+ "$component" "$action" "$detail" "$ts" "$session_id" >> "$log_file" 2>/dev/null || true
46
+ }
47
+
48
+ log_action() {
49
+ local action="$1"
50
+ local reason="$2"
51
+ local ts
52
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")
53
+ mkdir -p "$(dirname "$CHANGES_LOG")"
54
+ printf "%s | %s | %s\n" "$ts" "$action" "$reason" >> "$CHANGES_LOG"
55
+ }
56
+
57
+ # Read a JSON field using jq if available, otherwise grep+sed fallback.
58
+ # Usage: json_field <file> <dotted.key> e.g. json_field config.json auto_worker.enabled
59
+ json_field() {
60
+ local file="$1"
61
+ local key="$2"
62
+ if command -v jq >/dev/null 2>&1; then
63
+ jq -r ".${key} // empty" "$file" 2>/dev/null
64
+ else
65
+ # Minimal fallback: grep for the last segment key, extract bare value.
66
+ local leaf
67
+ leaf=$(printf '%s' "$key" | sed 's/.*\.//')
68
+ grep -o "\"${leaf}\"[[:space:]]*:[[:space:]]*[^,}]*" "$file" 2>/dev/null \
69
+ | tail -1 \
70
+ | sed 's/.*:[[:space:]]*//' \
71
+ | tr -d '"' \
72
+ | tr -d ' '
73
+ fi
74
+ }
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Gate 1: auto_worker.enabled
78
+ # ---------------------------------------------------------------------------
79
+
80
+ if [ ! -f "$TOKEN_BUDGET_JSON" ]; then
81
+ log_action "SKIP" "token-budget.json not found — auto_worker not configured"
82
+ ai_log_usage "script/auto-worker-scheduler" "skip" "auto_worker not enabled -- token-budget.json not found"
83
+ exit 0
84
+ fi
85
+
86
+ enabled=$(json_field "$TOKEN_BUDGET_JSON" "auto_worker.enabled")
87
+ if [ "$enabled" != "true" ]; then
88
+ log_action "SKIP" "auto_worker.enabled is false or absent in token-budget.json"
89
+ ai_log_usage "script/auto-worker-scheduler" "skip" "auto_worker not enabled"
90
+ exit 0
91
+ fi
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Gate 2: Conflict guard — check for active Claude session via session.lock
95
+ # ---------------------------------------------------------------------------
96
+
97
+ if [ -f "$SESSION_LOCK" ]; then
98
+ lock_content=""
99
+ if [ -r "$SESSION_LOCK" ]; then
100
+ lock_content=$(cat "$SESSION_LOCK" 2>/dev/null || true)
101
+ fi
102
+ log_action "SKIP" "Active Claude session detected (session.lock present: ${lock_content})"
103
+ ai_log_usage "script/auto-worker-scheduler" "skip" "session.lock present"
104
+ exit 0
105
+ fi
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Gate 3: Token budget threshold check
109
+ # ---------------------------------------------------------------------------
110
+ # Find the current ISO week KPI file. Format: weekly-YYYY-WNN.json
111
+ # We compute ISO week ourselves for portability.
112
+
113
+ iso_week() {
114
+ # Returns YYYY-WNN string for today (ISO 8601 week numbering)
115
+ if command -v python3 >/dev/null 2>&1; then
116
+ python3 -c "
117
+ import datetime
118
+ today = datetime.date.today()
119
+ iso = today.isocalendar()
120
+ print('%04d-W%02d' % (iso[0], iso[1]))
121
+ "
122
+ elif command -v python >/dev/null 2>&1; then
123
+ python -c "
124
+ import datetime
125
+ today = datetime.date.today()
126
+ iso = today.isocalendar()
127
+ print('%04d-W%02d' % (iso[0], iso[1]))
128
+ "
129
+ else
130
+ # Fallback: use date command Week-of-year (not strictly ISO but close enough)
131
+ date +"%Y-W%V" 2>/dev/null || date +"%Y-W%U"
132
+ fi
133
+ }
134
+
135
+ KPI_DIR=".ai-assistance/local/kpi"
136
+ WEEK_KEY=$(iso_week)
137
+ KPI_FILE="${KPI_DIR}/weekly-${WEEK_KEY}.json"
138
+
139
+ # Read quota and usage from KPI file (if present)
140
+ weekly_tokens_used=0
141
+ weekly_quota=0
142
+ window_start_ts="" # ISO timestamp of first session start in current 5-hour window
143
+
144
+ if [ -f "$KPI_FILE" ] && command -v jq >/dev/null 2>&1; then
145
+ # Sum tokens_used across all sessions
146
+ weekly_tokens_used=$(jq '[.sessions[]?.tokens_used // 0] | add // 0' "$KPI_FILE" 2>/dev/null || echo "0")
147
+ weekly_quota=$(jq -r '.quota // 0' "$KPI_FILE" 2>/dev/null || echo "0")
148
+
149
+ # Find most recent session start to compute reset window
150
+ window_start_ts=$(jq -r '
151
+ [.sessions[]?.session_start // empty] | sort | last // empty
152
+ ' "$KPI_FILE" 2>/dev/null || true)
153
+ fi
154
+
155
+ # Read quota from token-budget.json as fallback
156
+ if [ "$weekly_quota" = "0" ] || [ -z "$weekly_quota" ]; then
157
+ quota_from_config=$(json_field "$TOKEN_BUDGET_JSON" "weekly_quota.total_tokens")
158
+ if [ "$quota_from_config" != "null" ] && [ -n "$quota_from_config" ]; then
159
+ weekly_quota="$quota_from_config"
160
+ fi
161
+ fi
162
+
163
+ # If no quota is set (null), skip the threshold gate entirely
164
+ if [ "$weekly_quota" = "0" ] || [ "$weekly_quota" = "null" ] || [ -z "$weekly_quota" ]; then
165
+ log_action "OK" "No weekly quota set — skipping token threshold gate"
166
+ else
167
+ # Calculate usage percentage
168
+ if command -v python3 >/dev/null 2>&1; then
169
+ usage_pct=$(python3 -c "
170
+ used = float('${weekly_tokens_used}' or '0')
171
+ quota = float('${weekly_quota}')
172
+ print('%.1f' % (used / quota * 100) if quota > 0 else '0')
173
+ ")
174
+ else
175
+ usage_pct=0
176
+ fi
177
+
178
+ # Determine time remaining in current 5-hour reset window
179
+ # 5-hour window = 18000 seconds
180
+ window_seconds_elapsed=0
181
+ if [ -n "$window_start_ts" ] && command -v python3 >/dev/null 2>&1; then
182
+ window_seconds_elapsed=$(python3 -c "
183
+ import datetime, sys
184
+ try:
185
+ ts = '${window_start_ts}'.replace('Z', '+00:00')
186
+ start = datetime.datetime.fromisoformat(ts)
187
+ now = datetime.datetime.now(datetime.timezone.utc)
188
+ elapsed = (now - start).total_seconds()
189
+ print(int(max(0, elapsed)))
190
+ except Exception:
191
+ print(0)
192
+ ")
193
+ fi
194
+
195
+ WINDOW_DURATION=18000 # 5 hours in seconds
196
+ window_remaining=$(( WINDOW_DURATION - window_seconds_elapsed ))
197
+ if [ "$window_remaining" -lt 0 ]; then
198
+ window_remaining=0
199
+ fi
200
+ window_remaining_min=$(( window_remaining / 60 ))
201
+
202
+ # Determine threshold based on time remaining in window (ADR-007 table)
203
+ # >120 min -> 65%; 60-120 -> 75%; 45-60 -> 80%; 30-45 -> 90%; 15-30 -> 95%; <15 -> skip
204
+ if [ "$window_remaining_min" -lt 15 ]; then
205
+ log_action "SKIP" "Less than 15 min remaining in token reset window — will not launch"
206
+ ai_log_usage "script/auto-worker-scheduler" "skip" "token budget -- less than 15 min remaining in reset window"
207
+ exit 0
208
+ elif [ "$window_remaining_min" -lt 30 ]; then
209
+ threshold=95
210
+ elif [ "$window_remaining_min" -lt 45 ]; then
211
+ threshold=90
212
+ elif [ "$window_remaining_min" -lt 60 ]; then
213
+ threshold=80
214
+ elif [ "$window_remaining_min" -lt 120 ]; then
215
+ threshold=75
216
+ else
217
+ threshold=65
218
+ fi
219
+
220
+ # Compare usage_pct against threshold (integer comparison via python3)
221
+ if command -v python3 >/dev/null 2>&1; then
222
+ over_threshold=$(python3 -c "
223
+ pct = float('${usage_pct}' or '0')
224
+ thr = float('${threshold}')
225
+ print('true' if pct >= thr else 'false')
226
+ ")
227
+ else
228
+ over_threshold="false"
229
+ fi
230
+
231
+ if [ "$over_threshold" = "true" ]; then
232
+ log_action "SKIP" "Token usage ${usage_pct}% >= threshold ${threshold}% (${window_remaining_min} min remaining in window)"
233
+ ai_log_usage "script/auto-worker-scheduler" "skip" "token budget at ${usage_pct}% (threshold ${threshold}%)"
234
+ exit 0
235
+ fi
236
+
237
+ log_action "OK" "Token gate passed: ${usage_pct}% used, threshold ${threshold}% (${window_remaining_min} min remaining)"
238
+ fi
239
+
240
+ # ---------------------------------------------------------------------------
241
+ # Gate 4: Determine cadence (informational — actual cron cadence managed by
242
+ # CronCreate; this script just logs the recommendation)
243
+ # ---------------------------------------------------------------------------
244
+
245
+ recommended_cadence="2hr"
246
+ if [ -f "$LAST_RUN_JSON" ] && command -v jq >/dev/null 2>&1; then
247
+ completed_count=$(jq '.completed | length' "$LAST_RUN_JSON" 2>/dev/null || echo "0")
248
+ if [ "$completed_count" -gt "0" ]; then
249
+ recommended_cadence="30min"
250
+ fi
251
+ fi
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # Gate 5: dangerously_skip_permissions opt-in
255
+ # ---------------------------------------------------------------------------
256
+
257
+ skip_perms=$(json_field "$TOKEN_BUDGET_JSON" "auto_worker.dangerously_skip_permissions")
258
+ AUTO_WORKER_PROMPT="Run the auto-worker for this repo.
259
+
260
+ Instructions:
261
+ 1. Read TODO.md and collect all items tagged \`[auto]\` or \`[auto:last]\`.
262
+ 2. If no \`[auto]\` items exist, exit cleanly with a brief log entry.
263
+ 3. Plan execution order as a dependency graph — do NOT follow TODO list order.
264
+ - Items without dependencies on each other may run in parallel (branch out when the work is self-contained enough to isolate).
265
+ - \`[auto:last]\` items run only after all \`[auto]\` items complete or are skipped.
266
+ 4. For each item:
267
+ - Execute the task.
268
+ - Mark it \`[x]\` in TODO.md on completion.
269
+ - Add any discovered follow-up tasks as new TODO items; tag \`[auto]\` only if they meet the eligibility criteria in standards/tooling/auto-worker-eligibility.md.
270
+ 5. Do NOT touch items that are not tagged \`[auto]\` or \`[auto:last]\`.
271
+ 6. At the end, write a one-paragraph summary of what was done, skipped, and added to TODO."
272
+
273
+ CLAUDE_FLAGS="--print \"${AUTO_WORKER_PROMPT}\""
274
+ if [ "$skip_perms" = "true" ]; then
275
+ CLAUDE_FLAGS="--dangerously-skip-permissions --print \"${AUTO_WORKER_PROMPT}\""
276
+ fi
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # Launch: run auto-todo-worker as a background process
280
+ # ---------------------------------------------------------------------------
281
+
282
+ log_action "LAUNCH" "Launching auto-todo-worker (cadence recommendation: ${recommended_cadence})"
283
+ ai_log_usage "script/auto-worker-scheduler" "launch" "launching auto-todo-worker"
284
+
285
+ if [[ "${OSTYPE:-}" == "msys" ]] || [[ "${OSTYPE:-}" == "cygwin" ]] || \
286
+ [[ "${OS:-}" == "Windows_NT" ]]; then
287
+ # Windows via Git Bash / WSL
288
+ # Use PowerShell Start-Process to launch detached from the current terminal
289
+ powershell -Command "Start-Process -FilePath 'claude' \
290
+ -ArgumentList '${CLAUDE_FLAGS}' \
291
+ -WorkingDirectory '${PWD}' \
292
+ -NoNewWindow" &
293
+ launch_pid=$!
294
+ else
295
+ # Unix / macOS / WSL2
296
+ if [ "$skip_perms" = "true" ]; then
297
+ nohup claude --dangerously-skip-permissions --print "run auto worker" \
298
+ > ".ai-assistance/local/auto-worker-stdout.log" 2>&1 &
299
+ else
300
+ nohup claude --print "run auto worker" \
301
+ > ".ai-assistance/local/auto-worker-stdout.log" 2>&1 &
302
+ fi
303
+ launch_pid=$!
304
+ fi
305
+
306
+ log_action "LAUNCHED" "Background PID ${launch_pid:-unknown} — flags: ${CLAUDE_FLAGS}"
307
+ exit 0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecoportal-api-v2
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.2
4
+ version: 3.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oscar Segura
@@ -171,6 +171,14 @@ executables: []
171
171
  extensions: []
172
172
  extra_rdoc_files: []
173
173
  files:
174
+ - ".ai-assistance/code/double_model_patterns.md"
175
+ - ".ai-assistance/conventions/code-working-tree-protocol.md"
176
+ - ".ai-assistance/scripts/token-logger.js"
177
+ - ".ai-assistance/scripts/token-report.ts"
178
+ - ".ai-assistance/scripts/token-session-start.js"
179
+ - ".ai-assistance/skills/eP_AI_Manager/SKILL.md"
180
+ - ".ai-assistance/token-budget.json"
181
+ - ".claude/settings.json"
174
182
  - ".gitignore"
175
183
  - ".markdownlint.json"
176
184
  - ".rspec"
@@ -328,6 +336,7 @@ files:
328
336
  - lib/ecoportal/api/v2/s3/files/poll_status.rb
329
337
  - lib/ecoportal/api/v2/s3/upload.rb
330
338
  - lib/ecoportal/api/v2_version.rb
339
+ - scripts/auto-worker-scheduler.sh
331
340
  homepage: https://www.ecoportal.com
332
341
  licenses:
333
342
  - MIT