@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/CHANGELOG.md +527 -0
- package/README.md +280 -0
- package/atlas.sh +997 -0
- package/notify-telegram.sh +99 -0
- package/package.json +50 -0
- package/plan_prompt.md +175 -0
- package/prompt.md +198 -0
- package/references/CONTEXT_ENGINEERING.md +81 -0
- package/references/GUARDRAILS.md +53 -0
- package/review_prompt.md +180 -0
- package/scripts/postinstall.js +51 -0
- package/skills/atlas-branching/SKILL.md +160 -0
- package/skills/atlas-guardrails/SKILL.md +189 -0
- package/skills/atlas-integration-flow/SKILL.md +208 -0
- package/skills/atlas-state/SKILL.md +225 -0
- package/templates/backlog.md +27 -0
- package/templates/guardrails.md +69 -0
- package/templates/progress.txt +14 -0
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
|