@jxtools/atlas 3.0.0

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.
package/atlas.sh ADDED
@@ -0,0 +1,997 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Resolve ATLAS_HOME from script location (works with npm symlinks)
5
+ ATLAS_SOURCE="${BASH_SOURCE[0]}"
6
+ while [[ -L "$ATLAS_SOURCE" ]]; do
7
+ ATLAS_LINK_DIR="$(cd "$(dirname "$ATLAS_SOURCE")" && pwd)"
8
+ ATLAS_SOURCE="$(readlink "$ATLAS_SOURCE")"
9
+ [[ "$ATLAS_SOURCE" != /* ]] && ATLAS_SOURCE="$ATLAS_LINK_DIR/$ATLAS_SOURCE"
10
+ done
11
+ ATLAS_HOME="$(cd "$(dirname "$ATLAS_SOURCE")" && pwd)"
12
+ PROJECT_DIR="$(pwd)"
13
+ PROJECT_NAME="$(basename "$PROJECT_DIR")"
14
+ NOTIFY_TELEGRAM="${ATLAS_NOTIFY_TELEGRAM:-true}"
15
+
16
+ # Atlas version
17
+ ATLAS_VERSION="3.0.0"
18
+
19
+ # AI Provider configuration (claudecode | opencode | codex)
20
+ # Priority: --cli flag > ATLAS_CLI env var > default (claudecode)
21
+ ATLAS_CLI="${ATLAS_CLI:-claudecode}"
22
+
23
+ SHOW_HELP=false
24
+ SHOW_VERSION=false
25
+ REVIEW_DRY_RUN=false
26
+ CLEAN_ALL=false
27
+ POSITIONAL_ARGS=()
28
+
29
+ # Parse global flags from any position
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --cli)
33
+ if [[ $# -lt 2 ]]; then
34
+ echo "Error: --cli requires an argument (claudecode, opencode, or codex)"
35
+ exit 1
36
+ fi
37
+ case "$2" in
38
+ claudecode|opencode|codex)
39
+ ATLAS_CLI="$2"
40
+ ;;
41
+ *)
42
+ echo "Error: --cli requires 'claudecode', 'opencode', or 'codex', got '$2'"
43
+ exit 1
44
+ ;;
45
+ esac
46
+ shift 2
47
+ continue
48
+ ;;
49
+ --dry-run)
50
+ REVIEW_DRY_RUN=true
51
+ ;;
52
+ --all)
53
+ CLEAN_ALL=true
54
+ ;;
55
+ --version|-v)
56
+ SHOW_VERSION=true
57
+ ;;
58
+ --help|-h)
59
+ SHOW_HELP=true
60
+ ;;
61
+ --)
62
+ shift
63
+ while [[ $# -gt 0 ]]; do
64
+ POSITIONAL_ARGS+=("$1")
65
+ shift
66
+ done
67
+ break
68
+ ;;
69
+ *)
70
+ POSITIONAL_ARGS+=("$1")
71
+ ;;
72
+ esac
73
+ shift
74
+ done
75
+
76
+ DEFAULT_MAX_ITERATIONS=25
77
+ DEFAULT_STALE_SECONDS=7200
78
+ DEFAULT_TIMEOUT=1200
79
+
80
+ ATLAS_DIR=".atlas"
81
+ RUNS_DIR="$ATLAS_DIR/runs"
82
+ ACTIVITY_LOG="$ATLAS_DIR/activity.log"
83
+ ERRORS_LOG="$ATLAS_DIR/errors.log"
84
+ PROGRESS_FILE="$ATLAS_DIR/progress.txt"
85
+ GUARDRAILS_FILE="$ATLAS_DIR/guardrails.md"
86
+ BACKLOG_FILE="$ATLAS_DIR/backlog.md"
87
+
88
+ # Portable JSON value extractor (handles both string and integer values)
89
+ json_get() {
90
+ local key="$1" file="$2"
91
+ [[ ! -f "$file" ]] && return
92
+ awk -v k="$key" '
93
+ $0 ~ ("\"" k "\"") {
94
+ sub(".*\"" k "\"[[:space:]]*:[[:space:]]*", "")
95
+ sub(/^"/, "")
96
+ sub(/".*/, "")
97
+ sub(/,.*/, "")
98
+ sub(/[[:space:]]*$/, "")
99
+ print; exit
100
+ }
101
+ ' "$file"
102
+ }
103
+
104
+ # Install Atlas skills to all available AI providers
105
+ install_skills() {
106
+ [[ ! -d "$ATLAS_HOME/skills" ]] && return
107
+ local providers=()
108
+ command -v claude >/dev/null 2>&1 && providers+=("${HOME}/.claude/skills")
109
+ command -v opencode >/dev/null 2>&1 && providers+=("${HOME}/.config/opencode/skills")
110
+ command -v codex >/dev/null 2>&1 && providers+=("${HOME}/.codex/skills")
111
+ for target_dir in "${providers[@]}"; do
112
+ mkdir -p "$target_dir"
113
+ for skill_dir in "$ATLAS_HOME/skills"/atlas-*; do
114
+ [[ ! -d "$skill_dir" ]] && continue
115
+ local skill_name
116
+ skill_name=$(basename "$skill_dir")
117
+ mkdir -p "$target_dir/$skill_name"
118
+ cp -r "$skill_dir"/* "$target_dir/$skill_name/" 2>/dev/null || true
119
+ done
120
+ : # silent
121
+ done
122
+ }
123
+
124
+ # Invoke the selected AI provider with a prompt
125
+ # Usage: run_provider <mode> <prompt>
126
+ # Modes: plan, review, build (opencode agent names; ignored by codex/claude)
127
+ run_provider() {
128
+ local mode="$1" prompt="$2"
129
+ case "$ATLAS_CLI" in
130
+ opencode)
131
+ export OPENCODE_PERMISSION='{"*":"allow"}'
132
+ opencode run --agent "$mode" "$prompt"
133
+ ;;
134
+ codex)
135
+ codex exec --yolo "$prompt"
136
+ ;;
137
+ *)
138
+ claude --dangerously-skip-permissions "$prompt"
139
+ ;;
140
+ esac
141
+ }
142
+
143
+ # Cross-platform timeout function
144
+ run_with_timeout() {
145
+ local timeout_seconds=$1
146
+ shift
147
+
148
+ # Try GNU timeout (Linux)
149
+ if command -v timeout >/dev/null 2>&1; then
150
+ timeout --foreground "$timeout_seconds" "$@"
151
+ return $?
152
+ fi
153
+
154
+ # Try gtimeout (macOS with brew install coreutils)
155
+ if command -v gtimeout >/dev/null 2>&1; then
156
+ gtimeout --foreground "$timeout_seconds" "$@"
157
+ return $?
158
+ fi
159
+
160
+ # Fallback: run without timeout (macOS without coreutils)
161
+ "$@"
162
+ return $?
163
+ }
164
+
165
+ print_help() {
166
+ echo "Atlas - Autonomous Task Loop Agent System v$ATLAS_VERSION"
167
+ echo ""
168
+ echo "Usage: atlas [options] [command]"
169
+ echo ""
170
+ echo "Commands:"
171
+ echo " atlas init Initialize .atlas/ in current project"
172
+ echo " atlas plan <description> Interview and plan a feature"
173
+ echo " atlas review [--dry-run] Audit issues (and optionally auto-fix)"
174
+ echo " atlas resume [iterations] Resume interrupted integration session"
175
+ echo " atlas clean [--all] Clean runtime artifacts from .atlas/"
176
+ echo " atlas status Show task counts and session info"
177
+ echo " atlas doctor Check Atlas installation and dependencies"
178
+ echo " atlas update Show how to update via NPM"
179
+ echo " atlas [iterations] Run N iterations autonomously (default: 25)"
180
+ echo ""
181
+ echo "Options:"
182
+ echo " --cli <provider> AI provider: claudecode (default) | opencode | codex"
183
+ echo " --dry-run Review mode: report only (no auto-fixes)"
184
+ echo " --all Clean mode: also reset activity/errors logs and session"
185
+ echo " --version, -v Show version information"
186
+ echo " --help, -h Show this help message"
187
+ echo ""
188
+ echo "Environment variables:"
189
+ echo " ATLAS_CLI=claudecode Default AI provider (claudecode | opencode | codex)"
190
+ echo " ATLAS_MAX_ITERATIONS=25 Max iterations per run"
191
+ echo " ATLAS_TIMEOUT=1200 Timeout per iteration in seconds (20 min)"
192
+ echo " ATLAS_STALE_SECONDS=7200 Reset stuck tasks after N seconds (2 hours)"
193
+ echo " ATLAS_NOTIFY_TELEGRAM=true Enable Telegram notifications"
194
+ echo " ATLAS_TELEGRAM_BOT=... Telegram bot token"
195
+ echo " ATLAS_TELEGRAM_CHAT=... Telegram chat ID"
196
+ echo ""
197
+ echo "Examples:"
198
+ echo " atlas 25 # Run with Claude Code (default)"
199
+ echo " atlas --cli opencode review # Review with OpenCode"
200
+ echo " atlas review --dry-run # Report-only review"
201
+ echo " atlas --cli codex 25 # Run with Codex"
202
+ echo " atlas clean # Remove .atlas/runs logs"
203
+ echo " atlas clean --all # Reset logs + session metadata"
204
+ }
205
+
206
+ if [[ "$SHOW_VERSION" == "true" ]]; then
207
+ echo "Atlas v$ATLAS_VERSION"
208
+ exit 0
209
+ fi
210
+
211
+ COMMAND=""
212
+ COMMAND_ARGS=()
213
+ if [[ "$SHOW_HELP" == "true" ]]; then
214
+ COMMAND="help"
215
+ elif [[ ${#POSITIONAL_ARGS[@]} -eq 0 ]]; then
216
+ COMMAND="run"
217
+ elif [[ "${POSITIONAL_ARGS[0]}" == "init" || "${POSITIONAL_ARGS[0]}" == "update" || "${POSITIONAL_ARGS[0]}" == "plan" || "${POSITIONAL_ARGS[0]}" == "resume" || "${POSITIONAL_ARGS[0]}" == "review" || "${POSITIONAL_ARGS[0]}" == "clean" || "${POSITIONAL_ARGS[0]}" == "status" || "${POSITIONAL_ARGS[0]}" == "doctor" || "${POSITIONAL_ARGS[0]}" == "help" ]]; then
218
+ COMMAND="${POSITIONAL_ARGS[0]}"
219
+ COMMAND_ARGS=("${POSITIONAL_ARGS[@]:1}")
220
+ elif [[ "${POSITIONAL_ARGS[0]}" =~ ^[0-9]+$ ]]; then
221
+ COMMAND="run"
222
+ COMMAND_ARGS=("${POSITIONAL_ARGS[@]}")
223
+ else
224
+ echo "Error: Unknown command '${POSITIONAL_ARGS[0]}'"
225
+ echo "Run 'atlas help' for usage"
226
+ exit 1
227
+ fi
228
+
229
+ if [[ "$REVIEW_DRY_RUN" == "true" && "$COMMAND" != "review" && "$COMMAND" != "help" ]]; then
230
+ echo "Error: --dry-run is only valid with 'atlas review'"
231
+ exit 1
232
+ fi
233
+
234
+ if [[ "$CLEAN_ALL" == "true" && "$COMMAND" != "clean" && "$COMMAND" != "help" ]]; then
235
+ echo "Error: --all is only valid with 'atlas clean'"
236
+ exit 1
237
+ fi
238
+
239
+ case "$COMMAND" in
240
+ init)
241
+ mkdir -p "$ATLAS_DIR" "$RUNS_DIR"
242
+ if [[ ! -f "$BACKLOG_FILE" ]]; then
243
+ sed "s/\[PROJECT_NAME\]/$PROJECT_NAME/" "$ATLAS_HOME/templates/backlog.md" > "$BACKLOG_FILE"
244
+ fi
245
+ [[ ! -f "$PROGRESS_FILE" ]] && cp "$ATLAS_HOME/templates/progress.txt" "$PROGRESS_FILE"
246
+ [[ ! -f "$GUARDRAILS_FILE" ]] && cp "$ATLAS_HOME/templates/guardrails.md" "$GUARDRAILS_FILE"
247
+ [[ ! -f "$ACTIVITY_LOG" ]] && { echo "# Activity Log"; echo "Started: $(date '+%Y-%m-%d %H:%M:%S')"; echo ""; } > "$ACTIVITY_LOG"
248
+ [[ ! -f "$ERRORS_LOG" ]] && { echo "# Error Log"; echo ""; } > "$ERRORS_LOG"
249
+ if [[ ! -f "$ATLAS_DIR/.gitignore" ]]; then
250
+ cat > "$ATLAS_DIR/.gitignore" << 'GITIGNORE'
251
+ # Atlas session logs (local debugging files)
252
+ activity.log
253
+ errors.log
254
+ runs/
255
+
256
+ # Keep integration session tracked (needed for PR workflow)
257
+ !integration-session.json
258
+ GITIGNORE
259
+ fi
260
+ [[ -d "$ATLAS_HOME/references" ]] && [[ ! -d "$ATLAS_DIR/references" ]] && cp -r "$ATLAS_HOME/references" "$ATLAS_DIR/"
261
+
262
+ install_skills
263
+
264
+ echo "✓ Initialized .atlas/ in $PROJECT_DIR"
265
+ exit 0
266
+ ;;
267
+ update)
268
+ echo "Atlas is now distributed via NPM."
269
+ echo ""
270
+ echo "To update, run:"
271
+ echo " npm update -g @jxtools/atlas"
272
+ echo ""
273
+ echo "To check your current version:"
274
+ echo " atlas --version"
275
+ echo ""
276
+ echo "To check the latest available version:"
277
+ echo " npm view @jxtools/atlas version"
278
+ exit 0
279
+ ;;
280
+ plan)
281
+ FEATURE_PROMPT="${COMMAND_ARGS[*]}"
282
+ [[ ${#COMMAND_ARGS[@]} -eq 0 ]] && { echo "Usage: atlas plan <feature description>"; exit 1; }
283
+ [[ ! -d "$ATLAS_DIR" ]] && { echo "Error: .atlas/ not found. Run 'atlas init' first."; exit 1; }
284
+
285
+ if [[ "$ATLAS_CLI" != "claudecode" ]]; then
286
+ echo "Warning: 'atlas plan' works best with Claude Code (interactive mode)."
287
+ echo "Current provider: $ATLAS_CLI"
288
+ read -r -p "Continue anyway? [y/N] " confirm
289
+ [[ "$confirm" != [yY] ]] && { echo "Aborted."; exit 0; }
290
+ fi
291
+
292
+ mkdir -p "$ATLAS_DIR/specs"
293
+ SPEC_FILE="$ATLAS_DIR/specs/spec-$(date +%Y%m%d-%H%M%S).md"
294
+
295
+ export FEATURE_REQUEST="$FEATURE_PROMPT"
296
+ export PROJECT_DIR PROJECT_NAME SPEC_FILE BACKLOG_FILE
297
+ PLAN_PROMPT=$(envsubst '$FEATURE_REQUEST $PROJECT_DIR $PROJECT_NAME $SPEC_FILE $BACKLOG_FILE' < "$ATLAS_HOME/plan_prompt.md")
298
+
299
+ echo "╔═══════════════════════════════════════════════════════╗"
300
+ echo "║ Atlas Plan - Feature Interview ║"
301
+ echo "╠═══════════════════════════════════════════════════════╣"
302
+ echo "║ Feature: $FEATURE_PROMPT"
303
+ echo "║ Output: $SPEC_FILE"
304
+ echo "╚═══════════════════════════════════════════════════════╝"
305
+
306
+ log_activity() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$ACTIVITY_LOG"; }
307
+ log_activity "PLAN: $FEATURE_PROMPT -> $SPEC_FILE"
308
+
309
+ run_provider plan "$PLAN_PROMPT"
310
+
311
+ exit 0
312
+ ;;
313
+ resume)
314
+ [[ ${#COMMAND_ARGS[@]} -gt 1 ]] && { echo "Usage: atlas resume [iterations]"; exit 1; }
315
+ [[ ${#COMMAND_ARGS[@]} -eq 1 && ! "${COMMAND_ARGS[0]}" =~ ^[0-9]+$ ]] && { echo "Error: iterations must be a number"; exit 1; }
316
+ RESUME_ITERATIONS="${COMMAND_ARGS[0]:-$DEFAULT_MAX_ITERATIONS}"
317
+
318
+ [[ ! -d "$ATLAS_DIR" ]] && { echo "Error: .atlas/ not found. Run 'atlas init' first."; exit 1; }
319
+
320
+ SESSION_FILE=".atlas/integration-session.json"
321
+ if [[ ! -f "$SESSION_FILE" ]]; then
322
+ echo "❌ No active integration session found."
323
+ echo ""
324
+ echo "There is no session to resume. To start a new session:"
325
+ echo " atlas [N] Start new session with N iterations"
326
+ exit 1
327
+ fi
328
+
329
+ SESSION_BRANCH=$(json_get "branch" "$SESSION_FILE")
330
+ SESSION_PR=$(json_get "pr_number" "$SESSION_FILE")
331
+ SESSION_NAME=$(json_get "session_name" "$SESSION_FILE")
332
+
333
+ [[ -z "$SESSION_PR" || "$SESSION_PR" == "null" ]] && { echo "❌ Invalid session file: missing pr_number"; exit 1; }
334
+ [[ -z "$SESSION_BRANCH" || "$SESSION_BRANCH" == "null" ]] && { echo "❌ Invalid session file: missing branch"; exit 1; }
335
+
336
+ echo "🔍 Checking session status..."
337
+ PR_STATE=$(gh pr view "$SESSION_PR" --json state -q '.state' 2>/dev/null || echo "UNKNOWN")
338
+
339
+ case "$PR_STATE" in
340
+ MERGED)
341
+ echo "❌ Session already merged (PR #$SESSION_PR)."
342
+ echo " Use 'atlas' to start a new session."
343
+ exit 1 ;;
344
+ CLOSED)
345
+ echo "❌ Session PR was closed (PR #$SESSION_PR)."
346
+ echo " Reopen on GitHub or delete .atlas/integration-session.json"
347
+ exit 1 ;;
348
+ UNKNOWN)
349
+ echo "⚠️ Could not verify PR status (offline?). Continuing..." ;;
350
+ OPEN)
351
+ echo "✓ Session active: $SESSION_NAME (PR #$SESSION_PR)" ;;
352
+ esac
353
+
354
+ echo "📍 Switching to: $SESSION_BRANCH"
355
+ if ! git checkout "$SESSION_BRANCH" 2>/dev/null; then
356
+ if git show-ref --verify --quiet "refs/remotes/origin/$SESSION_BRANCH"; then
357
+ git checkout -b "$SESSION_BRANCH" "origin/$SESSION_BRANCH" || { echo "❌ Failed to checkout"; exit 1; }
358
+ else
359
+ echo "❌ Branch not found: $SESSION_BRANCH"
360
+ exit 1
361
+ fi
362
+ fi
363
+ git pull origin "$SESSION_BRANCH" 2>/dev/null || echo " ⚠️ Could not pull"
364
+
365
+ export MAX_ITERATIONS="$RESUME_ITERATIONS"
366
+ export RESUME_MODE="true"
367
+ echo ""
368
+ ;;
369
+ review)
370
+ [[ ${#COMMAND_ARGS[@]} -gt 0 ]] && { echo "Usage: atlas review [--dry-run]"; exit 1; }
371
+ [[ ! -d "$ATLAS_DIR" ]] && { echo "Error: .atlas/ not found. Run 'atlas init' first."; exit 1; }
372
+ [[ ! -f "$ATLAS_HOME/review_prompt.md" ]] && { echo "Error: review_prompt.md not found in $ATLAS_HOME. Run 'npm update -g @jxtools/atlas' to fix."; exit 1; }
373
+
374
+ echo "╔═══════════════════════════════════════════════════════╗"
375
+ echo "║ Atlas Review - AI Audit & Repair ║"
376
+ echo "╚═══════════════════════════════════════════════════════╝"
377
+ echo ""
378
+
379
+ GIT_MODE="false"
380
+ [[ -d ".git" ]] && GIT_MODE="true"
381
+
382
+ CLAUDE_MD="CLAUDE.md"
383
+ [[ ! -f "$CLAUDE_MD" ]] && CLAUDE_MD=""
384
+
385
+ SESSION_FILE="$ATLAS_DIR/integration-session.json"
386
+ [[ ! -f "$SESSION_FILE" ]] && SESSION_FILE=""
387
+
388
+ export PROJECT_DIR PROJECT_NAME GIT_MODE
389
+ export BACKLOG_FILE GUARDRAILS_FILE PROGRESS_FILE ERRORS_LOG ACTIVITY_LOG
390
+ export CLAUDE_MD SESSION_FILE
391
+
392
+ REVIEW_PROMPT=$(envsubst '$PROJECT_DIR $PROJECT_NAME $GIT_MODE $BACKLOG_FILE $GUARDRAILS_FILE $PROGRESS_FILE $ERRORS_LOG $CLAUDE_MD $ACTIVITY_LOG $SESSION_FILE' < "$ATLAS_HOME/review_prompt.md")
393
+
394
+ if [[ "$REVIEW_DRY_RUN" == "true" ]]; then
395
+ REVIEW_PROMPT="$REVIEW_PROMPT
396
+
397
+ ## DRY RUN MODE (STRICT)
398
+ - You are in report-only mode.
399
+ - DO NOT modify files.
400
+ - DO NOT run git commit/push/merge/rebase commands.
401
+ - DO NOT run commands that mutate project state.
402
+ - Only inspect and report findings + recommended fixes."
403
+ fi
404
+
405
+ REVIEW_MODE_LABEL="APPLY FIXES"
406
+ [[ "$REVIEW_DRY_RUN" == "true" ]] && REVIEW_MODE_LABEL="DRY-RUN (report only)"
407
+
408
+ echo "🔍 Analyzing project state with AI..."
409
+ echo " Provider: $ATLAS_CLI"
410
+ echo " Git mode: $GIT_MODE"
411
+ echo " Mode: $REVIEW_MODE_LABEL"
412
+ echo ""
413
+
414
+ if [[ "$ATLAS_CLI" == "codex" ]]; then
415
+ mkdir -p "$RUNS_DIR"
416
+ REVIEW_RUN_TAG="$(date +%Y%m%d-%H%M%S)-$$"
417
+ REVIEW_LOG_FILE="$RUNS_DIR/review-$REVIEW_RUN_TAG.log"
418
+ REVIEW_LAST_MESSAGE_FILE="$RUNS_DIR/review-$REVIEW_RUN_TAG-last-message.txt"
419
+
420
+ if codex exec --yolo -o "$REVIEW_LAST_MESSAGE_FILE" "$REVIEW_PROMPT" > "$REVIEW_LOG_FILE" 2>&1; then
421
+ echo "✅ Codex review completed (non-interactive mode)"
422
+ echo " Log: $REVIEW_LOG_FILE"
423
+ else
424
+ echo "❌ Codex review failed"
425
+ echo " Log: $REVIEW_LOG_FILE"
426
+ tail -n 40 "$REVIEW_LOG_FILE" 2>/dev/null || true
427
+ exit 1
428
+ fi
429
+ else
430
+ run_provider review "$REVIEW_PROMPT"
431
+ fi
432
+
433
+ exit 0
434
+ ;;
435
+ clean)
436
+ [[ ${#COMMAND_ARGS[@]} -gt 0 ]] && { echo "Usage: atlas clean [--all]"; exit 1; }
437
+ [[ ! -d "$ATLAS_DIR" ]] && { echo "Error: .atlas/ not found. Run 'atlas init' first."; exit 1; }
438
+
439
+ mkdir -p "$RUNS_DIR"
440
+ RUN_LOGS_REMOVED=$(find "$RUNS_DIR" -maxdepth 1 -type f -name '*.log' | wc -l | tr -d ' ')
441
+ if [[ "$RUN_LOGS_REMOVED" -gt 0 ]]; then
442
+ find "$RUNS_DIR" -maxdepth 1 -type f -name '*.log' -delete
443
+ fi
444
+
445
+ TMP_FILES_REMOVED=$(find "$ATLAS_DIR" -maxdepth 1 -type f -name '*.tmp' | wc -l | tr -d ' ')
446
+ if [[ "$TMP_FILES_REMOVED" -gt 0 ]]; then
447
+ find "$ATLAS_DIR" -maxdepth 1 -type f -name '*.tmp' -delete
448
+ fi
449
+
450
+ SESSION_STATUS="not-found"
451
+ if [[ -f "$ATLAS_DIR/integration-session.json" ]]; then
452
+ SESSION_STATUS="kept"
453
+ SESSION_PR=$(json_get "pr_number" "$ATLAS_DIR/integration-session.json")
454
+ SESSION_BRANCH=$(json_get "branch" "$ATLAS_DIR/integration-session.json")
455
+ PR_STATE="UNKNOWN"
456
+ if command -v gh >/dev/null 2>&1 && [[ -n "$SESSION_PR" && "$SESSION_PR" != "null" ]]; then
457
+ PR_STATE=$(gh pr view "$SESSION_PR" --json state -q '.state' 2>/dev/null || echo "UNKNOWN")
458
+ fi
459
+
460
+ if [[ "$CLEAN_ALL" == "true" || "$PR_STATE" == "MERGED" || "$PR_STATE" == "CLOSED" ]]; then
461
+ rm -f "$ATLAS_DIR/integration-session.json"
462
+ SESSION_STATUS="removed"
463
+ if [[ -n "$SESSION_BRANCH" ]] && git show-ref --verify --quiet "refs/heads/$SESSION_BRANCH" 2>/dev/null; then
464
+ git branch -D "$SESSION_BRANCH" 2>/dev/null || true
465
+ fi
466
+ fi
467
+ fi
468
+
469
+ if [[ "$CLEAN_ALL" == "true" ]]; then
470
+ { echo "# Activity Log"; echo "Started: $(date '+%Y-%m-%d %H:%M:%S')"; echo ""; } > "$ACTIVITY_LOG"
471
+ { echo "# Error Log"; echo ""; } > "$ERRORS_LOG"
472
+ fi
473
+
474
+ echo "🧹 Atlas clean completed"
475
+ echo " Removed run logs: $RUN_LOGS_REMOVED"
476
+ echo " Removed temp files: $TMP_FILES_REMOVED"
477
+ echo " Integration session: $SESSION_STATUS"
478
+ [[ "$CLEAN_ALL" == "true" ]] && echo " Reset activity/errors logs: yes"
479
+ exit 0
480
+ ;;
481
+ status)
482
+ [[ ! -d "$ATLAS_DIR" ]] && { echo "Error: .atlas/ not found. Run 'atlas init' first."; exit 1; }
483
+
484
+ # Reuse count_tasks (defined later, but we need it here)
485
+ local_count() {
486
+ local file="$1" in_section="" todo=0 ip=0 done=0
487
+ while IFS= read -r line; do
488
+ if [[ "$line" =~ ^##[[:space:]]+(TODO|IN_PROGRESS|IN\ PROGRESS|DONE|DELAYED) ]]; then
489
+ in_section="${BASH_REMATCH[1]}"
490
+ [[ "$in_section" == "IN PROGRESS" ]] && in_section="IN_PROGRESS"
491
+ elif [[ "$line" =~ ^###[[:space:]] ]]; then
492
+ case "$in_section" in TODO) ((todo++)) ;; IN_PROGRESS) ((ip++)) ;; DONE) ((done++)) ;; esac
493
+ fi
494
+ done < "$file"
495
+ echo "$todo $ip $done"
496
+ }
497
+
498
+ read TODO_N IP_N DONE_N <<< $(local_count "$BACKLOG_FILE")
499
+
500
+ echo "Atlas Status - $PROJECT_NAME"
501
+ echo ""
502
+ echo "Tasks: TODO=$TODO_N IN_PROGRESS=$IP_N DONE=$DONE_N"
503
+
504
+ if [[ -f "$ATLAS_DIR/integration-session.json" ]]; then
505
+ S_NAME=$(json_get "session_name" "$ATLAS_DIR/integration-session.json")
506
+ S_PR=$(json_get "pr_number" "$ATLAS_DIR/integration-session.json")
507
+ echo "Session: $S_NAME (PR #$S_PR)"
508
+ else
509
+ echo "Session: none"
510
+ fi
511
+
512
+ if [[ -d ".git" ]]; then
513
+ echo "Branch: $(git branch --show-current 2>/dev/null || echo 'N/A')"
514
+ fi
515
+ exit 0
516
+ ;;
517
+ doctor)
518
+ echo "=== Atlas Doctor ==="
519
+ echo ""
520
+ OK="[OK]" WARN="[WARN]" FAIL="[FAIL]"
521
+
522
+ # Check AI CLI
523
+ case "$ATLAS_CLI" in
524
+ claudecode) CLI_BIN="claude" ;;
525
+ opencode) CLI_BIN="opencode" ;;
526
+ codex) CLI_BIN="codex" ;;
527
+ esac
528
+ if command -v "$CLI_BIN" >/dev/null 2>&1; then
529
+ echo "$OK $ATLAS_CLI ($CLI_BIN found)"
530
+ else
531
+ echo "$FAIL $ATLAS_CLI ($CLI_BIN not found)"
532
+ fi
533
+
534
+ # Check prompts
535
+ for p in prompt.md plan_prompt.md review_prompt.md; do
536
+ if [[ -f "$ATLAS_HOME/$p" ]]; then
537
+ echo "$OK $p"
538
+ else
539
+ echo "$FAIL $p missing in $ATLAS_HOME"
540
+ fi
541
+ done
542
+
543
+ # Check envsubst
544
+ if command -v envsubst >/dev/null 2>&1; then
545
+ echo "$OK envsubst"
546
+ else
547
+ echo "$WARN envsubst not found (install gettext)"
548
+ fi
549
+
550
+ # Check git
551
+ if command -v git >/dev/null 2>&1; then
552
+ echo "$OK git ($(git --version | awk '{print $3}'))"
553
+ else
554
+ echo "$WARN git not found (local mode only)"
555
+ fi
556
+
557
+ # Check gh CLI
558
+ if command -v gh >/dev/null 2>&1; then
559
+ echo "$OK gh CLI ($(gh --version | head -1 | awk '{print $3}'))"
560
+ else
561
+ echo "$WARN gh CLI not found (no PR workflow)"
562
+ fi
563
+
564
+ exit 0
565
+ ;;
566
+ help)
567
+ print_help
568
+ exit 0
569
+ ;;
570
+ run)
571
+ ;;
572
+ esac
573
+
574
+ if [[ "$COMMAND" == "run" ]]; then
575
+ [[ ${#COMMAND_ARGS[@]} -gt 1 ]] && { echo "Usage: atlas [iterations]"; exit 1; }
576
+ if [[ ${#COMMAND_ARGS[@]} -eq 1 ]]; then
577
+ [[ "${COMMAND_ARGS[0]}" =~ ^[0-9]+$ ]] || { echo "Error: iterations must be a number"; exit 1; }
578
+ RUN_ITERATIONS_OVERRIDE="${COMMAND_ARGS[0]}"
579
+ fi
580
+ fi
581
+
582
+ # Validate that the selected AI CLI is installed
583
+ case "$ATLAS_CLI" in
584
+ claudecode)
585
+ if ! command -v claude >/dev/null 2>&1; then
586
+ echo "❌ Error: Claude Code not found"
587
+ echo " Install it: https://docs.anthropic.com/en/docs/claude-code"
588
+ echo " Or use OpenCode: curl -fsSL https://opencode.ai/install | bash"
589
+ exit 1
590
+ fi
591
+ ;;
592
+ opencode)
593
+ if ! command -v opencode >/dev/null 2>&1; then
594
+ echo "❌ Error: OpenCode not found"
595
+ echo " Install it: curl -fsSL https://opencode.ai/install | bash"
596
+ echo " Or use Claude Code: https://docs.anthropic.com/en/docs/claude-code"
597
+ exit 1
598
+ fi
599
+ ;;
600
+ codex)
601
+ if ! command -v codex >/dev/null 2>&1; then
602
+ echo "❌ Error: Codex CLI not found"
603
+ echo " Install it: npm install -g @openai/codex"
604
+ echo " Or use Claude Code: https://docs.anthropic.com/en/docs/claude-code"
605
+ exit 1
606
+ fi
607
+ ;;
608
+ esac
609
+
610
+ if [[ -z "${MAX_ITERATIONS:-}" ]]; then
611
+ MAX_ITERATIONS="${ATLAS_MAX_ITERATIONS:-$DEFAULT_MAX_ITERATIONS}"
612
+ fi
613
+ STALE_SECONDS="${ATLAS_STALE_SECONDS:-$DEFAULT_STALE_SECONDS}"
614
+ TIMEOUT_SECONDS="${ATLAS_TIMEOUT:-$DEFAULT_TIMEOUT}"
615
+ [[ -n "${RUN_ITERATIONS_OVERRIDE:-}" ]] && MAX_ITERATIONS="$RUN_ITERATIONS_OVERRIDE"
616
+
617
+ [[ ! -d "$ATLAS_DIR" ]] && { echo "Error: .atlas/ not found. Run 'atlas init' first."; exit 1; }
618
+ [[ ! -f "$BACKLOG_FILE" ]] && { echo "Error: .atlas/backlog.md not found. Run 'atlas init' or create it manually."; exit 1; }
619
+ [[ ! -f "$ATLAS_HOME/prompt.md" ]] && { echo "Error: prompt.md not found in $ATLAS_HOME. Run 'npm update -g @jxtools/atlas' to fix."; exit 1; }
620
+ mkdir -p "$RUNS_DIR"
621
+
622
+ log_activity() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$ACTIVITY_LOG"; }
623
+
624
+ # Check for stale tasks in IN_PROGRESS and move back to TODO
625
+ reset_stale_tasks() {
626
+ [[ "$STALE_SECONDS" -eq 0 ]] && return
627
+
628
+ # Quick check: any ### task under IN_PROGRESS?
629
+ local in_progress_task
630
+ in_progress_task=$(awk '/^## IN.PROGRESS/{f=1;next} /^## /{f=0} f && /^### /{print;exit}' "$BACKLOG_FILE")
631
+ [[ -z "$in_progress_task" ]] && return
632
+
633
+ # Check staleness via most recent run log
634
+ local latest_run
635
+ latest_run=$(ls -t "$RUNS_DIR"/*.log 2>/dev/null | head -1)
636
+ [[ -z "$latest_run" ]] && return
637
+
638
+ local last_mod now age
639
+ last_mod=$(stat -c %Y "$latest_run" 2>/dev/null || stat -f %m "$latest_run" 2>/dev/null)
640
+ now=$(date +%s)
641
+ age=$((now - last_mod))
642
+
643
+ [[ "$age" -le "$STALE_SECONDS" ]] && return
644
+
645
+ echo "⚠️ Stale task in IN_PROGRESS (${age}s old, threshold: ${STALE_SECONDS}s)"
646
+ echo " Resetting to TODO..."
647
+
648
+ # Block-based rewrite: collect IN_PROGRESS content and insert before its header
649
+ awk '
650
+ /^## IN.PROGRESS/ { ip_header = $0; in_ip = 1; next }
651
+ in_ip && /^## / {
652
+ printf "%s", ip_content
653
+ print ip_header
654
+ print ""
655
+ in_ip = 0
656
+ print; next
657
+ }
658
+ in_ip { ip_content = ip_content $0 "\n"; next }
659
+ { print }
660
+ END { if (in_ip) { printf "%s", ip_content; print ip_header } }
661
+ ' "$BACKLOG_FILE" > "$BACKLOG_FILE.tmp" && mv "$BACKLOG_FILE.tmp" "$BACKLOG_FILE"
662
+
663
+ log_activity "STALE RESET: Moved task back to TODO after ${age}s"
664
+ echo "✓ Task moved back to TODO"
665
+ }
666
+
667
+ git_head() { git rev-parse --short HEAD 2>/dev/null || echo ""; }
668
+
669
+ # Count tasks in backlog (bash-verified, not model-dependent)
670
+ count_tasks() {
671
+ local file="$1"
672
+ [[ ! -f "$file" ]] && echo "0 0 0" && return
673
+
674
+ local in_section=""
675
+ local todo=0 in_progress=0 done=0
676
+
677
+ while IFS= read -r line; do
678
+ # Detect section headers
679
+ if [[ "$line" =~ ^##[[:space:]]+(TODO|IN_PROGRESS|IN\ PROGRESS|DONE|DELAYED) ]]; then
680
+ in_section="${BASH_REMATCH[1]}"
681
+ [[ "$in_section" == "IN PROGRESS" ]] && in_section="IN_PROGRESS"
682
+ # Count tasks (lines starting with ### )
683
+ elif [[ "$line" =~ ^###[[:space:]] ]]; then
684
+ case "$in_section" in
685
+ TODO) ((todo++)) ;;
686
+ IN_PROGRESS) ((in_progress++)) ;;
687
+ DONE) ((done++)) ;;
688
+ esac
689
+ fi
690
+ done < "$file"
691
+
692
+ echo "$todo $in_progress $done"
693
+ }
694
+
695
+ send_notification() {
696
+ [[ "$NOTIFY_TELEGRAM" == "true" ]] && [[ -x "$ATLAS_HOME/notify-telegram.sh" ]] && "$ATLAS_HOME/notify-telegram.sh" "$1" "$MAX_ITERATIONS" "$PROJECT_NAME" "$2" &
697
+ }
698
+
699
+ RUN_TAG="$(date +%Y%m%d-%H%M%S)-$$"
700
+ CONSECUTIVE_ERRORS=0
701
+ MAX_CONSECUTIVE_ERRORS=3
702
+
703
+ # Handle Ctrl+C gracefully
704
+ cleanup() {
705
+ echo ""
706
+ echo "⛔ Interrupted by user"
707
+ log_activity "RUN INTERRUPTED run=$RUN_TAG"
708
+ [[ "$GIT_MODE" == "true" ]] && git checkout "${DEFAULT_BRANCH:-main}" 2>/dev/null || true
709
+ exit 130
710
+ }
711
+ trap cleanup SIGINT SIGTERM
712
+
713
+ echo "╔═══════════════════════════════════════════════════════╗"
714
+ echo "║ Atlas - Autonomous Task Loop Agent System ║"
715
+ echo "╠═══════════════════════════════════════════════════════╣"
716
+ echo "║ Project: $PROJECT_NAME"
717
+ echo "║ AI Provider: $ATLAS_CLI"
718
+ echo "║ Iterations: $MAX_ITERATIONS"
719
+ echo "║ Run: $RUN_TAG"
720
+ echo "╚═══════════════════════════════════════════════════════╝"
721
+ echo ""
722
+
723
+ log_activity "RUN START run=$RUN_TAG iterations=$MAX_ITERATIONS"
724
+
725
+ reset_stale_tasks
726
+
727
+ # Si estamos en resume mode, saltar setup de branch (ya hicimos checkout)
728
+ if [[ "${RESUME_MODE:-false}" == "true" ]]; then
729
+ export GIT_MODE="true"
730
+ export DEFAULT_BRANCH="${ATLAS_DEFAULT_BRANCH:-}"
731
+ if [[ -z "$DEFAULT_BRANCH" ]]; then
732
+ DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') || DEFAULT_BRANCH="main"
733
+ [[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
734
+ fi
735
+ echo "📍 Resumed session - skipping branch setup"
736
+ echo ""
737
+ elif [[ -d "$PROJECT_DIR/.git" ]]; then
738
+ export GIT_MODE="true"
739
+
740
+ # Detect default branch (main, master, or configured)
741
+ export DEFAULT_BRANCH="${ATLAS_DEFAULT_BRANCH:-}"
742
+ if [[ -z "$DEFAULT_BRANCH" ]]; then
743
+ DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') || DEFAULT_BRANCH="main"
744
+ [[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
745
+ fi
746
+
747
+ # CRITICAL: Always start from default branch to ensure clean state
748
+ echo "📍 Ensuring clean git state..."
749
+ CURRENT_BRANCH=$(git branch --show-current)
750
+
751
+ # Handle empty repo (no commits yet) - skip branch switching
752
+ if [[ -z "$CURRENT_BRANCH" ]] && ! git rev-parse HEAD &>/dev/null; then
753
+ echo " ⚠️ New repository with no commits yet - skipping branch check"
754
+ elif [[ "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]]; then
755
+ echo " Switching from '$CURRENT_BRANCH' to $DEFAULT_BRANCH..."
756
+
757
+ # Check for uncommitted changes that would block checkout
758
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
759
+ echo "❌ Cannot switch branches: you have uncommitted changes"
760
+ echo " Please commit or stash your changes first:"
761
+ git status --short
762
+ exit 1
763
+ fi
764
+
765
+ # Try checkout, showing the actual error if it fails
766
+ if ! git checkout "$DEFAULT_BRANCH" 2>&1; then
767
+ # Branch might not exist locally - try to create from remote
768
+ if git show-ref --verify --quiet "refs/remotes/origin/$DEFAULT_BRANCH" 2>/dev/null; then
769
+ echo " Creating local branch from origin/$DEFAULT_BRANCH..."
770
+ git checkout -b "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH" || {
771
+ echo "❌ Failed to create local branch $DEFAULT_BRANCH"
772
+ exit 1
773
+ }
774
+ else
775
+ echo "❌ Branch '$DEFAULT_BRANCH' does not exist locally or on remote"
776
+ echo " Available branches:"
777
+ git branch -a
778
+ echo ""
779
+ echo " Hint: Set ATLAS_DEFAULT_BRANCH to your main branch name, or create the branch first"
780
+ exit 1
781
+ fi
782
+ fi
783
+ fi
784
+ git pull origin "$DEFAULT_BRANCH" 2>/dev/null || echo " ⚠️ Could not pull (offline or no remote)"
785
+
786
+ # Check for merged integration session and cleanup
787
+ if [[ -f ".atlas/integration-session.json" ]]; then
788
+ SESSION_PR=$(json_get "pr_number" .atlas/integration-session.json)
789
+ if [[ -n "$SESSION_PR" && "$SESSION_PR" != "null" ]]; then
790
+ PR_STATE=$(gh pr view "$SESSION_PR" --json state -q '.state' 2>/dev/null || echo "UNKNOWN")
791
+ if [[ "$PR_STATE" == "MERGED" ]]; then
792
+ echo " 🧹 Cleaning up merged integration session (PR #$SESSION_PR)..."
793
+ OLD_BRANCH=$(json_get "branch" .atlas/integration-session.json)
794
+ [[ -n "$OLD_BRANCH" ]] && git branch -D "$OLD_BRANCH" 2>/dev/null || true
795
+ rm -f .atlas/integration-session.json
796
+ echo " ✓ Cleaned up. New session will be created."
797
+ fi
798
+ fi
799
+ fi
800
+ echo ""
801
+ else
802
+ export GIT_MODE="false"
803
+ echo "⚠️ No git repository detected - running in LOCAL MODE"
804
+ echo " (no branches, commits, or PRs will be created)"
805
+ echo ""
806
+ fi
807
+
808
+ for i in $(seq 1 $MAX_ITERATIONS); do
809
+ echo "═══ ITERATION $i/$MAX_ITERATIONS ═══"
810
+
811
+ ITER_START=$(date +%s)
812
+ HEAD_BEFORE=$(git_head)
813
+ LOG_FILE="$RUNS_DIR/run-$RUN_TAG-iter-$i.log"
814
+
815
+ log_activity "ITERATION $i START"
816
+
817
+ # Build list of context files that exist
818
+ CONTEXT_FILES=""
819
+ [[ -f "$BACKLOG_FILE" ]] && CONTEXT_FILES="$CONTEXT_FILES
820
+ - $BACKLOG_FILE (REQUIRED - task queue)"
821
+ [[ -f "$GUARDRAILS_FILE" ]] && CONTEXT_FILES="$CONTEXT_FILES
822
+ - $GUARDRAILS_FILE (rules from past errors)"
823
+ [[ -f "$PROGRESS_FILE" ]] && CONTEXT_FILES="$CONTEXT_FILES
824
+ - $PROGRESS_FILE (history of completed tasks)"
825
+ [[ -f "$ERRORS_LOG" ]] && CONTEXT_FILES="$CONTEXT_FILES
826
+ - $ERRORS_LOG (recent failures)"
827
+ [[ -f "CLAUDE.md" ]] && CONTEXT_FILES="$CONTEXT_FILES
828
+ - CLAUDE.md (project rules and quality gates)"
829
+ [[ -f ".atlas/integration-session.json" ]] && CONTEXT_FILES="$CONTEXT_FILES
830
+ - .atlas/integration-session.json (INTEGRATION SESSION - use branch as BASE_BRANCH)"
831
+
832
+ # Extract spec file from CURRENT task only (IN_PROGRESS first, then first TODO)
833
+ SPEC_FILE=""
834
+ CURRENT_TASK_SPEC=""
835
+
836
+ # Get the current task block (IN_PROGRESS first, then TODO if empty)
837
+ CURRENT_TASK_BLOCK=$(awk '
838
+ /^## IN_PROGRESS/ { section="IP"; next }
839
+ /^## TODO/ { if (section != "IP" || !found) section="TODO"; next }
840
+ /^## / { section=""; next }
841
+ section && /^### / {
842
+ found=1
843
+ print
844
+ while ((getline line) > 0) {
845
+ if (line ~ /^### / || line ~ /^## /) break
846
+ print line
847
+ }
848
+ exit
849
+ }
850
+ ' "$BACKLOG_FILE")
851
+
852
+ # Extract spec from the current task block only
853
+ if [[ -n "$CURRENT_TASK_BLOCK" ]]; then
854
+ CURRENT_TASK_SPEC=$(echo "$CURRENT_TASK_BLOCK" | grep "^\- \*\*Spec:\*\*" | sed 's/.*Spec:\*\* //' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
855
+ fi
856
+
857
+ if [[ -n "$CURRENT_TASK_SPEC" && -f "$CURRENT_TASK_SPEC" ]]; then
858
+ SPEC_FILE="$CURRENT_TASK_SPEC"
859
+ CONTEXT_FILES="$CONTEXT_FILES
860
+ - $CURRENT_TASK_SPEC (INTEGRAL VIEW - full feature spec)"
861
+ echo " 📋 Spec: $CURRENT_TASK_SPEC"
862
+ fi
863
+
864
+ # Export variables for envsubst
865
+ export PROJECT_DIR PROJECT_NAME
866
+ export RUN_ID="$RUN_TAG"
867
+ export ITERATION="$i"
868
+
869
+ # Process prompt.md with variable substitution
870
+ PROMPT_CONTENT=$(envsubst '$PROJECT_DIR $PROJECT_NAME $RUN_ID $ITERATION $GIT_MODE' < "$ATLAS_HOME/prompt.md")
871
+
872
+ # Build prompt with processed instructions inline
873
+ PROMPT="CONTEXT_FILES:$CONTEXT_FILES
874
+
875
+ ---
876
+
877
+ $PROMPT_CONTENT"
878
+
879
+ set +e
880
+ # Write prompt to temp file to avoid pipe issues with signals
881
+ PROMPT_FILE_TMP=$(mktemp)
882
+ echo "$PROMPT" > "$PROMPT_FILE_TMP"
883
+
884
+ # Retry loop for transient CLI errors (rate limits, timeouts, network)
885
+ MAX_RETRIES=3
886
+ RETRY_DELAY=10
887
+ RETRY_COUNT=0
888
+ CLI_SUCCESS=false
889
+ CLI_ERROR=""
890
+
891
+ while [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do
892
+ RETRY_COUNT=$((RETRY_COUNT + 1))
893
+
894
+ # Run AI CLI and capture output
895
+ if [[ "$ATLAS_CLI" == "opencode" ]]; then
896
+ export OPENCODE_PERMISSION='{"*":"allow"}'
897
+ OUTPUT=$(run_with_timeout "$TIMEOUT_SECONDS" opencode run --agent build "$(cat "$PROMPT_FILE_TMP")" 2>&1) || true
898
+ elif [[ "$ATLAS_CLI" == "codex" ]]; then
899
+ OUTPUT=$(run_with_timeout "$TIMEOUT_SECONDS" codex exec --yolo "$(cat "$PROMPT_FILE_TMP")" 2>&1) || true
900
+ else
901
+ OUTPUT=$(run_with_timeout "$TIMEOUT_SECONDS" claude --dangerously-skip-permissions -p < "$PROMPT_FILE_TMP" 2>&1) || true
902
+ fi
903
+
904
+ # Check for CLI errors
905
+ CLI_ERROR=""
906
+ if [[ -z "$OUTPUT" ]]; then
907
+ CLI_ERROR="No output captured"
908
+ elif echo "$OUTPUT" | grep -q "Error: No messages returned"; then
909
+ CLI_ERROR="API rate limit or timeout"
910
+ elif echo "$OUTPUT" | grep -q "Error: API"; then
911
+ CLI_ERROR="API error"
912
+ elif echo "$OUTPUT" | grep -q "Error: Network"; then
913
+ CLI_ERROR="Network error"
914
+ fi
915
+
916
+ if [[ -z "$CLI_ERROR" ]]; then
917
+ CLI_SUCCESS=true
918
+ break
919
+ fi
920
+
921
+ # Log retry attempt (console only, no notification yet)
922
+ echo "⚠️ CLI Error: $CLI_ERROR (attempt $RETRY_COUNT/$MAX_RETRIES)"
923
+ log_activity "ITERATION $i RETRY $RETRY_COUNT: $CLI_ERROR"
924
+
925
+ if [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; then
926
+ echo " Retrying in ${RETRY_DELAY}s..."
927
+ sleep $RETRY_DELAY
928
+ fi
929
+ done
930
+
931
+ rm -f "$PROMPT_FILE_TMP"
932
+
933
+ # Handle complete failure after all retries exhausted
934
+ if [[ "$CLI_SUCCESS" == "false" ]]; then
935
+ CONSECUTIVE_ERRORS=$((CONSECUTIVE_ERRORS + 1))
936
+ echo "❌ Failed after $MAX_RETRIES attempts"
937
+ echo "$OUTPUT" > "$LOG_FILE"
938
+
939
+ read ERROR_TODO ERROR_IP ERROR_DONE <<< $(count_tasks "$BACKLOG_FILE")
940
+
941
+ if [[ $CONSECUTIVE_ERRORS -ge $MAX_CONSECUTIVE_ERRORS ]]; then
942
+ echo "🛑 Too many consecutive failed iterations ($CONSECUTIVE_ERRORS). Stopping run."
943
+ send_notification "$i" "Task: CLI Error - $CLI_ERROR
944
+ Status: STOPPED
945
+ Pending: $ERROR_TODO"
946
+ log_activity "RUN STOPPED: $CONSECUTIVE_ERRORS consecutive failed iterations"
947
+ exit 1
948
+ fi
949
+
950
+ # Notify only after all retries failed
951
+ send_notification "$i" "Task: CLI Error - $CLI_ERROR (after $MAX_RETRIES retries)
952
+ Status: SKIPPED
953
+ Pending: $ERROR_TODO"
954
+ log_activity "ITERATION $i FAILED after $MAX_RETRIES retries, moving to next"
955
+ continue
956
+ fi
957
+
958
+ # Reset error counter on successful iteration
959
+ CONSECUTIVE_ERRORS=0
960
+
961
+ # Write output to log file and display to terminal
962
+ echo "$OUTPUT" | tee "$LOG_FILE"
963
+ set -e
964
+
965
+ ITER_END=$(date +%s)
966
+ ITER_DURATION=$((ITER_END - ITER_START))
967
+ HEAD_AFTER=$(git_head)
968
+
969
+ SUMMARY=$(echo "$OUTPUT" | sed -n '/=== SUMMARY ===/,/Loop:/p' | head -10)
970
+ [[ -z "$SUMMARY" ]] && SUMMARY="No summary found"
971
+
972
+ # Bash-verified task count (don't trust model's count)
973
+ read TODO_COUNT IN_PROGRESS_COUNT DONE_COUNT <<< $(count_tasks "$BACKLOG_FILE")
974
+ echo ""
975
+ echo "📊 Pending: $TODO_COUNT"
976
+
977
+ # Append count to summary for Telegram
978
+ SUMMARY="$SUMMARY
979
+ Pending: $TODO_COUNT"
980
+
981
+ log_activity "ITERATION $i END duration=${ITER_DURATION}s pending=$TODO_COUNT"
982
+ send_notification "$i" "$SUMMARY"
983
+
984
+ if echo "$OUTPUT" | grep -q "<promise>COMPLETE</promise>"; then
985
+ echo ""; echo "✅ ALL TASKS COMPLETED!"
986
+ log_activity "RUN COMPLETE run=$RUN_TAG"
987
+ [[ "$GIT_MODE" == "true" ]] && git checkout "${DEFAULT_BRANCH:-main}" 2>/dev/null || true
988
+ exit 0
989
+ fi
990
+
991
+ sleep 2
992
+ done
993
+
994
+ [[ "$GIT_MODE" == "true" ]] && git checkout "${DEFAULT_BRANCH:-main}" 2>/dev/null || true
995
+ echo ""; echo "🤖 MAX ITERATIONS REACHED"
996
+ log_activity "RUN END run=$RUN_TAG (max iterations)"
997
+ exit 0