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.
- checksums.yaml +4 -4
- data/.ai-assistance/code/double_model_patterns.md +133 -0
- data/.ai-assistance/conventions/code-working-tree-protocol.md +176 -0
- data/.ai-assistance/scripts/token-logger.js +220 -0
- data/.ai-assistance/scripts/token-report.ts +158 -0
- data/.ai-assistance/scripts/token-session-start.js +66 -0
- data/.ai-assistance/skills/eP_AI_Manager/SKILL.md +262 -0
- data/.ai-assistance/token-budget.json +44 -0
- data/.claude/settings.json +107 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +11 -5
- data/lib/ecoportal/api/common/content/double_model/attributable/nesting/cascaded_callback.rb +1 -1
- data/lib/ecoportal/api/v2_version.rb +1 -1
- data/scripts/auto-worker-scheduler.sh +307 -0
- metadata +10 -1
|
@@ -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.
|
|
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
|