aether-colony 2.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.
Potentially problematic release.
This version of aether-colony might be problematic. Click here for more details.
- package/.claude/commands/ant/ant.md +89 -0
- package/.claude/commands/ant/build.md +504 -0
- package/.claude/commands/ant/colonize.md +94 -0
- package/.claude/commands/ant/continue.md +674 -0
- package/.claude/commands/ant/feedback.md +65 -0
- package/.claude/commands/ant/flag.md +108 -0
- package/.claude/commands/ant/flags.md +139 -0
- package/.claude/commands/ant/focus.md +42 -0
- package/.claude/commands/ant/init.md +129 -0
- package/.claude/commands/ant/migrate-state.md +140 -0
- package/.claude/commands/ant/organize.md +210 -0
- package/.claude/commands/ant/pause-colony.md +87 -0
- package/.claude/commands/ant/phase.md +86 -0
- package/.claude/commands/ant/plan.md +409 -0
- package/.claude/commands/ant/redirect.md +53 -0
- package/.claude/commands/ant/resume-colony.md +83 -0
- package/.claude/commands/ant/status.md +122 -0
- package/.claude/commands/ant/watch.md +220 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/bin/cli.js +196 -0
- package/package.json +35 -0
- package/runtime/DISCIPLINES.md +93 -0
- package/runtime/QUEEN_ANT_ARCHITECTURE.md +347 -0
- package/runtime/aether-utils.sh +760 -0
- package/runtime/coding-standards.md +197 -0
- package/runtime/debugging.md +207 -0
- package/runtime/docs/pheromones.md +213 -0
- package/runtime/learning.md +254 -0
- package/runtime/planning.md +159 -0
- package/runtime/tdd.md +257 -0
- package/runtime/utils/atomic-write.sh +213 -0
- package/runtime/utils/colorize-log.sh +132 -0
- package/runtime/utils/file-lock.sh +122 -0
- package/runtime/utils/watch-spawn-tree.sh +185 -0
- package/runtime/verification-loop.md +159 -0
- package/runtime/verification.md +116 -0
- package/runtime/workers.md +671 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Aether Atomic Write Utility
|
|
3
|
+
# Implements atomic write pattern (temp file + rename) for corruption safety
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# source ~/.aether/utils/atomic-write.sh
|
|
7
|
+
# atomic_write /path/to/file.json "content"
|
|
8
|
+
# atomic_write_from_file /path/to/target.json /path/to/temp.json
|
|
9
|
+
|
|
10
|
+
# Source required utilities
|
|
11
|
+
# Get the directory where this script is located
|
|
12
|
+
_AETHER_UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
# If BASH_SOURCE[0] is empty (can happen in some contexts), use relative path
|
|
14
|
+
if [ -z "$_AETHER_UTILS_DIR" ] || [ "$_AETHER_UTILS_DIR" = "$(pwd)" ]; then
|
|
15
|
+
_AETHER_UTILS_DIR="$HOME/.aether/utils"
|
|
16
|
+
fi
|
|
17
|
+
# Verify the path exists and file-lock.sh is there
|
|
18
|
+
if [ ! -f "$_AETHER_UTILS_DIR/file-lock.sh" ]; then
|
|
19
|
+
# Try one more fallback - relative to script location
|
|
20
|
+
_AETHER_UTILS_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
|
21
|
+
fi
|
|
22
|
+
source "$_AETHER_UTILS_DIR/file-lock.sh"
|
|
23
|
+
|
|
24
|
+
# Aether root detection - use git root if available, otherwise use current directory
|
|
25
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
26
|
+
AETHER_ROOT="$(git rev-parse --show-toplevel)"
|
|
27
|
+
else
|
|
28
|
+
AETHER_ROOT="$(pwd)"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
TEMP_DIR="$AETHER_ROOT/.aether/temp"
|
|
32
|
+
BACKUP_DIR="$AETHER_ROOT/.aether/data/backups"
|
|
33
|
+
|
|
34
|
+
# Create directories
|
|
35
|
+
mkdir -p "$TEMP_DIR" "$BACKUP_DIR"
|
|
36
|
+
|
|
37
|
+
# Number of backups to keep
|
|
38
|
+
MAX_BACKUPS=3
|
|
39
|
+
|
|
40
|
+
# Atomic write: write content to file via temporary file
|
|
41
|
+
# Arguments: target_file, content
|
|
42
|
+
# Returns: 0 on success, 1 on failure
|
|
43
|
+
atomic_write() {
|
|
44
|
+
local target_file="$1"
|
|
45
|
+
local content="$2"
|
|
46
|
+
|
|
47
|
+
# Ensure target directory exists
|
|
48
|
+
local target_dir=$(dirname "$target_file")
|
|
49
|
+
mkdir -p "$target_dir"
|
|
50
|
+
|
|
51
|
+
# Create unique temp file
|
|
52
|
+
local temp_file="${TEMP_DIR}/$(basename "$target_file").$$.$(date +%s%N).tmp"
|
|
53
|
+
|
|
54
|
+
# Write content to temp file
|
|
55
|
+
if ! echo "$content" > "$temp_file"; then
|
|
56
|
+
echo "Failed to write to temp file: $temp_file"
|
|
57
|
+
rm -f "$temp_file"
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Validate JSON if it's a JSON file
|
|
62
|
+
if [[ "$target_file" == *.json ]]; then
|
|
63
|
+
if ! python3 -c "import json; json.load(open('$temp_file'))" 2>/dev/null; then
|
|
64
|
+
echo "Invalid JSON in temp file: $temp_file"
|
|
65
|
+
rm -f "$temp_file"
|
|
66
|
+
return 1
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Create backup if target exists
|
|
71
|
+
if [ -f "$target_file" ]; then
|
|
72
|
+
create_backup "$target_file"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Atomic rename (overwrites target if exists)
|
|
76
|
+
if ! mv "$temp_file" "$target_file"; then
|
|
77
|
+
echo "Failed to rename temp file to target: $target_file"
|
|
78
|
+
rm -f "$temp_file"
|
|
79
|
+
return 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Sync to disk
|
|
83
|
+
if command -v sync >/dev/null 2>&1; then
|
|
84
|
+
sync "$target_file" 2>/dev/null || true
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
return 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Atomic write from source file to target
|
|
91
|
+
# Arguments: target_file, source_file
|
|
92
|
+
# Returns: 0 on success, 1 on failure
|
|
93
|
+
atomic_write_from_file() {
|
|
94
|
+
local target_file="$1"
|
|
95
|
+
local source_file="$2"
|
|
96
|
+
|
|
97
|
+
if [ ! -f "$source_file" ]; then
|
|
98
|
+
echo "Source file does not exist: $source_file"
|
|
99
|
+
return 1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Ensure target directory exists
|
|
103
|
+
local target_dir=$(dirname "$target_file")
|
|
104
|
+
mkdir -p "$target_dir"
|
|
105
|
+
|
|
106
|
+
# Create unique temp file
|
|
107
|
+
local temp_file="${TEMP_DIR}/$(basename "$target_file").$$.$(date +%s%N).tmp"
|
|
108
|
+
|
|
109
|
+
# Copy source to temp
|
|
110
|
+
if ! cp "$source_file" "$temp_file"; then
|
|
111
|
+
echo "Failed to copy source to temp: $source_file -> $temp_file"
|
|
112
|
+
rm -f "$temp_file"
|
|
113
|
+
return 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Validate JSON if it's a JSON file
|
|
117
|
+
if [[ "$target_file" == *.json ]]; then
|
|
118
|
+
if ! python3 -c "import json; json.load(open('$temp_file'))" 2>/dev/null; then
|
|
119
|
+
echo "Invalid JSON in temp file: $temp_file"
|
|
120
|
+
rm -f "$temp_file"
|
|
121
|
+
return 1
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Create backup if target exists
|
|
126
|
+
if [ -f "$target_file" ]; then
|
|
127
|
+
create_backup "$target_file"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Atomic rename
|
|
131
|
+
if ! mv "$temp_file" "$target_file"; then
|
|
132
|
+
echo "Failed to rename temp file to target: $target_file"
|
|
133
|
+
rm -f "$temp_file"
|
|
134
|
+
return 1
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Sync to disk
|
|
138
|
+
if command -v sync >/dev/null 2>&1; then
|
|
139
|
+
sync "$target_file" 2>/dev/null || true
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
return 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Create backup of file
|
|
146
|
+
# Arguments: file_path
|
|
147
|
+
create_backup() {
|
|
148
|
+
local file_path="$1"
|
|
149
|
+
local base_name=$(basename "$file_path")
|
|
150
|
+
local timestamp=$(date +%Y%m%d_%H%M%S)
|
|
151
|
+
local backup_file="${BACKUP_DIR}/${base_name}.${timestamp}.backup"
|
|
152
|
+
|
|
153
|
+
cp "$file_path" "$backup_file" 2>/dev/null || return 1
|
|
154
|
+
|
|
155
|
+
# Rotate old backups
|
|
156
|
+
rotate_backups "$base_name"
|
|
157
|
+
|
|
158
|
+
return 0
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Rotate backups, keeping only MAX_BACKUPS
|
|
162
|
+
# Arguments: base_name
|
|
163
|
+
rotate_backups() {
|
|
164
|
+
local base_name="$1"
|
|
165
|
+
local backups=$(ls -t "${BACKUP_DIR}/${base_name}".*.backup 2>/dev/null | wc -l)
|
|
166
|
+
|
|
167
|
+
if [ "$backups" -gt "$MAX_BACKUPS" ]; then
|
|
168
|
+
ls -t "${BACKUP_DIR}/${base_name}".*.backup | tail -n +$((MAX_BACKUPS + 1)) | xargs rm -f
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Restore from backup
|
|
173
|
+
# Arguments: target_file, [backup_number]
|
|
174
|
+
# Returns: 0 on success, 1 on failure
|
|
175
|
+
restore_backup() {
|
|
176
|
+
local target_file="$1"
|
|
177
|
+
local backup_num="${2:-1}" # Default to most recent backup
|
|
178
|
+
local base_name=$(basename "$target_file")
|
|
179
|
+
|
|
180
|
+
local backup_file=$(ls -t "${BACKUP_DIR}/${base_name}".*.backup 2>/dev/null | sed -n "${backup_num}p")
|
|
181
|
+
|
|
182
|
+
if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
|
|
183
|
+
echo "No backup found for: $target_file"
|
|
184
|
+
return 1
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
if ! atomic_write_from_file "$target_file" "$backup_file"; then
|
|
188
|
+
echo "Failed to restore from backup: $backup_file"
|
|
189
|
+
return 1
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
echo "Restored from: $backup_file"
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# List available backups
|
|
197
|
+
# Arguments: file_path
|
|
198
|
+
list_backups() {
|
|
199
|
+
local file_path="$1"
|
|
200
|
+
local base_name=$(basename "$file_path")
|
|
201
|
+
|
|
202
|
+
echo "Available backups for $base_name:"
|
|
203
|
+
ls -lh "${BACKUP_DIR}/${base_name}".*.backup 2>/dev/null || echo "No backups found"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Cleanup temp files older than 1 hour
|
|
207
|
+
cleanup_temp_files() {
|
|
208
|
+
find "$TEMP_DIR" -name "*.tmp" -mtime +1/24 -delete 2>/dev/null || true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Export functions
|
|
212
|
+
export -f atomic_write atomic_write_from_file create_backup rotate_backups
|
|
213
|
+
export -f restore_backup list_backups cleanup_temp_files
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Colorized activity log stream for tmux watch pane
|
|
3
|
+
# Usage: bash colorize-log.sh [log_file]
|
|
4
|
+
|
|
5
|
+
LOG_FILE="${1:-.aether/data/activity.log}"
|
|
6
|
+
|
|
7
|
+
# ANSI color codes by caste (as per V2 improvement plan)
|
|
8
|
+
QUEEN='\033[35m' # Magenta
|
|
9
|
+
BUILDER='\033[33m' # Yellow
|
|
10
|
+
WATCHER='\033[36m' # Cyan
|
|
11
|
+
SCOUT='\033[32m' # Green
|
|
12
|
+
COLONIZER='\033[34m' # Blue
|
|
13
|
+
ARCHITECT='\033[37m' # White
|
|
14
|
+
|
|
15
|
+
# Action colors (bright variants)
|
|
16
|
+
SPAWN='\033[93m' # Bright Yellow
|
|
17
|
+
COMPLETE='\033[92m' # Bright Green
|
|
18
|
+
ERROR='\033[91m' # Bright Red
|
|
19
|
+
CREATED='\033[96m' # Bright Cyan
|
|
20
|
+
MODIFIED='\033[94m' # Bright Blue
|
|
21
|
+
|
|
22
|
+
# Base colors
|
|
23
|
+
YELLOW='\033[33m'
|
|
24
|
+
GREEN='\033[32m'
|
|
25
|
+
RED='\033[31m'
|
|
26
|
+
CYAN='\033[36m'
|
|
27
|
+
MAGENTA='\033[35m'
|
|
28
|
+
BLUE='\033[34m'
|
|
29
|
+
BOLD='\033[1m'
|
|
30
|
+
DIM='\033[2m'
|
|
31
|
+
RESET='\033[0m'
|
|
32
|
+
|
|
33
|
+
# Get caste color from ant name patterns
|
|
34
|
+
get_caste_color() {
|
|
35
|
+
case "$1" in
|
|
36
|
+
*Queen*|*QUEEN*)
|
|
37
|
+
echo "$QUEEN"
|
|
38
|
+
;;
|
|
39
|
+
*Builder*|*Bolt*|*Hammer*|*Forge*|*Mason*|*Brick*|*Anvil*|*Weld*)
|
|
40
|
+
echo "$BUILDER"
|
|
41
|
+
;;
|
|
42
|
+
*Watcher*|*Vigil*|*Sentinel*|*Guard*|*Keen*|*Sharp*|*Hawk*|*Watch*|*Alert*)
|
|
43
|
+
echo "$WATCHER"
|
|
44
|
+
;;
|
|
45
|
+
*Scout*|*Swift*|*Dash*|*Ranger*|*Track*|*Seek*|*Path*|*Roam*|*Quest*)
|
|
46
|
+
echo "$SCOUT"
|
|
47
|
+
;;
|
|
48
|
+
*Colonizer*|*Pioneer*|*Map*|*Chart*|*Venture*|*Explore*|*Compass*|*Atlas*|*Trek*)
|
|
49
|
+
echo "$COLONIZER"
|
|
50
|
+
;;
|
|
51
|
+
*Architect*|*Blueprint*|*Draft*|*Design*|*Plan*|*Schema*|*Frame*|*Sketch*|*Model*)
|
|
52
|
+
echo "$ARCHITECT"
|
|
53
|
+
;;
|
|
54
|
+
*Prime*|*Alpha*|*Lead*|*Chief*|*First*|*Core*|*Apex*|*Crown*)
|
|
55
|
+
echo "$MAGENTA"
|
|
56
|
+
;;
|
|
57
|
+
*)
|
|
58
|
+
echo "$RESET"
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Get emoji for caste
|
|
64
|
+
get_emoji() {
|
|
65
|
+
case "$1" in
|
|
66
|
+
*Queen*|*QUEEN*) echo "👑" ;;
|
|
67
|
+
*Builder*|*Bolt*|*Hammer*|*Forge*|*Mason*|*Brick*|*Anvil*|*Weld*) echo "🔨" ;;
|
|
68
|
+
*Watcher*|*Vigil*|*Sentinel*|*Guard*|*Keen*|*Sharp*|*Hawk*|*Watch*|*Alert*) echo "👁️" ;;
|
|
69
|
+
*Scout*|*Swift*|*Dash*|*Ranger*|*Track*|*Seek*|*Path*|*Roam*|*Quest*) echo "🔍" ;;
|
|
70
|
+
*Colonizer*|*Pioneer*|*Map*|*Chart*|*Venture*|*Explore*|*Compass*|*Atlas*|*Trek*) echo "🗺️" ;;
|
|
71
|
+
*Architect*|*Blueprint*|*Draft*|*Design*|*Plan*|*Schema*|*Frame*|*Sketch*|*Model*) echo "🏛️" ;;
|
|
72
|
+
*Prime*|*Alpha*|*Lead*|*Chief*|*First*|*Core*|*Apex*|*Crown*) echo "👑" ;;
|
|
73
|
+
*) echo "🐜" ;;
|
|
74
|
+
esac
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Colony header
|
|
78
|
+
echo -e "${BOLD}${MAGENTA}"
|
|
79
|
+
cat << 'EOF'
|
|
80
|
+
.-.
|
|
81
|
+
(o o) AETHER COLONY
|
|
82
|
+
| O | Activity Stream
|
|
83
|
+
`-`
|
|
84
|
+
EOF
|
|
85
|
+
echo -e "${RESET}"
|
|
86
|
+
echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
87
|
+
echo ""
|
|
88
|
+
|
|
89
|
+
# Stream and colorize log entries
|
|
90
|
+
tail -f "$LOG_FILE" 2>/dev/null | while IFS= read -r line; do
|
|
91
|
+
# Extract caste color if ant name is in the line
|
|
92
|
+
caste_color=$(get_caste_color "$line")
|
|
93
|
+
emoji=$(get_emoji "$line")
|
|
94
|
+
|
|
95
|
+
case "$line" in
|
|
96
|
+
*SPAWN*)
|
|
97
|
+
printf "${SPAWN}⚡ %s${RESET}\n" "$line"
|
|
98
|
+
;;
|
|
99
|
+
*COMPLETE*|*completed*)
|
|
100
|
+
printf "${COMPLETE}✅ %s${RESET}\n" "$line"
|
|
101
|
+
;;
|
|
102
|
+
*ERROR*|*FAILED*|*failed*)
|
|
103
|
+
printf "${ERROR}${BOLD}❌ %s${RESET}\n" "$line"
|
|
104
|
+
;;
|
|
105
|
+
*CREATED*)
|
|
106
|
+
printf "${caste_color}✨ %s${RESET}\n" "$line"
|
|
107
|
+
;;
|
|
108
|
+
*MODIFIED*)
|
|
109
|
+
printf "${caste_color}📝 %s${RESET}\n" "$line"
|
|
110
|
+
;;
|
|
111
|
+
*RESEARCH*|*EXPLORING*)
|
|
112
|
+
printf "${caste_color}🔬 %s${RESET}\n" "$line"
|
|
113
|
+
;;
|
|
114
|
+
*EXECUTING*|*BUILDING*)
|
|
115
|
+
printf "${caste_color}⚙️ %s${RESET}\n" "$line"
|
|
116
|
+
;;
|
|
117
|
+
"# Phase"*)
|
|
118
|
+
# Phase headers get special treatment
|
|
119
|
+
printf "\n${BOLD}${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
|
120
|
+
printf "${BOLD}${MAGENTA}🐜 %s${RESET}\n" "$line"
|
|
121
|
+
printf "${BOLD}${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"
|
|
122
|
+
;;
|
|
123
|
+
*)
|
|
124
|
+
# Apply caste color if detected, otherwise default
|
|
125
|
+
if [[ "$caste_color" != "$RESET" ]]; then
|
|
126
|
+
printf "${caste_color}%s %s${RESET}\n" "$emoji" "$line"
|
|
127
|
+
else
|
|
128
|
+
echo "$line"
|
|
129
|
+
fi
|
|
130
|
+
;;
|
|
131
|
+
esac
|
|
132
|
+
done
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Aether File Lock Utility
|
|
3
|
+
# Implements file locking for concurrent colony access prevention
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# source ~/.aether/utils/file-lock.sh
|
|
7
|
+
# acquire_lock /path/to/file.lock
|
|
8
|
+
# # ... critical section ...
|
|
9
|
+
# release_lock /path/to/file.lock
|
|
10
|
+
|
|
11
|
+
# Aether root detection - use git root if available, otherwise use current directory
|
|
12
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
13
|
+
AETHER_ROOT="$(git rev-parse --show-toplevel)"
|
|
14
|
+
else
|
|
15
|
+
AETHER_ROOT="$(pwd)"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
LOCK_DIR="$AETHER_ROOT/.aether/locks"
|
|
19
|
+
LOCK_TIMEOUT=300 # 5 minutes max lock time
|
|
20
|
+
LOCK_RETRY_INTERVAL=0.5 # Wait 500ms between retries
|
|
21
|
+
LOCK_MAX_RETRIES=100 # Total 50 seconds max wait
|
|
22
|
+
|
|
23
|
+
# Create lock directory if it doesn't exist
|
|
24
|
+
mkdir -p "$LOCK_DIR"
|
|
25
|
+
|
|
26
|
+
# Acquire a file lock using flock
|
|
27
|
+
# Arguments: file_path (the resource to lock)
|
|
28
|
+
# Returns: 0 on success, 1 on failure
|
|
29
|
+
# Globals: LOCK_ACQUIRED (set to true when lock acquired)
|
|
30
|
+
acquire_lock() {
|
|
31
|
+
local file_path="$1"
|
|
32
|
+
local lock_file="${LOCK_DIR}/$(basename "$file_path").lock"
|
|
33
|
+
local lock_pid_file="${lock_file}.pid"
|
|
34
|
+
|
|
35
|
+
# Check if lock file exists and is stale
|
|
36
|
+
if [ -f "$lock_file" ]; then
|
|
37
|
+
local lock_pid=$(cat "$lock_pid_file" 2>/dev/null || echo "")
|
|
38
|
+
if [ -n "$lock_pid" ]; then
|
|
39
|
+
# Check if process is still running
|
|
40
|
+
if ! kill -0 "$lock_pid" 2>/dev/null; then
|
|
41
|
+
echo "Lock stale (PID $lock_pid not running), cleaning up..."
|
|
42
|
+
rm -f "$lock_file" "$lock_pid_file"
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Try to acquire lock with timeout
|
|
48
|
+
local retry_count=0
|
|
49
|
+
while [ $retry_count -lt $LOCK_MAX_RETRIES ]; do
|
|
50
|
+
# Try to create lock file atomically
|
|
51
|
+
if (set -o noclobber; echo $$ > "$lock_file") 2>/dev/null; then
|
|
52
|
+
echo $$ > "$lock_pid_file"
|
|
53
|
+
export LOCK_ACQUIRED=true
|
|
54
|
+
export CURRENT_LOCK="$lock_file"
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
retry_count=$((retry_count + 1))
|
|
59
|
+
if [ $retry_count -lt $LOCK_MAX_RETRIES ]; then
|
|
60
|
+
sleep $LOCK_RETRY_INTERVAL
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
echo "Failed to acquire lock for $file_path after $LOCK_MAX_RETRIES attempts"
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Release a file lock
|
|
69
|
+
# Arguments: None (uses CURRENT_LOCK from acquire_lock)
|
|
70
|
+
release_lock() {
|
|
71
|
+
if [ "$LOCK_ACQUIRED" = "true" ] && [ -n "$CURRENT_LOCK" ]; then
|
|
72
|
+
rm -f "$CURRENT_LOCK" "${CURRENT_LOCK}.pid"
|
|
73
|
+
export LOCK_ACQUIRED=false
|
|
74
|
+
export CURRENT_LOCK=""
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
return 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Cleanup function for script exit
|
|
81
|
+
cleanup_locks() {
|
|
82
|
+
if [ "$LOCK_ACQUIRED" = "true" ]; then
|
|
83
|
+
release_lock
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Register cleanup on exit
|
|
88
|
+
trap cleanup_locks EXIT TERM INT
|
|
89
|
+
|
|
90
|
+
# Check if a file is currently locked
|
|
91
|
+
is_locked() {
|
|
92
|
+
local file_path="$1"
|
|
93
|
+
local lock_file="${LOCK_DIR}/$(basename "$file_path").lock"
|
|
94
|
+
[ -f "$lock_file" ]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Get PID of process holding lock
|
|
98
|
+
get_lock_holder() {
|
|
99
|
+
local file_path="$1"
|
|
100
|
+
local lock_file="${LOCK_DIR}/$(basename "$file_path").lock.pid"
|
|
101
|
+
cat "$lock_file" 2>/dev/null || echo ""
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Wait for lock to be released
|
|
105
|
+
wait_for_lock() {
|
|
106
|
+
local file_path="$1"
|
|
107
|
+
local max_wait=${2:-$LOCK_TIMEOUT}
|
|
108
|
+
local waited=0
|
|
109
|
+
|
|
110
|
+
while is_locked "$file_path" && [ $waited -lt $max_wait ]; do
|
|
111
|
+
sleep 1
|
|
112
|
+
waited=$((waited + 1))
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
if [ $waited -ge $max_wait ]; then
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Export functions for use in other scripts
|
|
122
|
+
export -f acquire_lock release_lock is_locked get_lock_holder wait_for_lock cleanup_locks
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Live spawn tree visualization for tmux watch pane
|
|
3
|
+
# Usage: bash watch-spawn-tree.sh [data_dir]
|
|
4
|
+
|
|
5
|
+
DATA_DIR="${1:-.aether/data}"
|
|
6
|
+
SPAWN_FILE="$DATA_DIR/spawn-tree.txt"
|
|
7
|
+
|
|
8
|
+
# ANSI colors
|
|
9
|
+
YELLOW='\033[33m'
|
|
10
|
+
GREEN='\033[32m'
|
|
11
|
+
RED='\033[31m'
|
|
12
|
+
CYAN='\033[36m'
|
|
13
|
+
MAGENTA='\033[35m'
|
|
14
|
+
BOLD='\033[1m'
|
|
15
|
+
DIM='\033[2m'
|
|
16
|
+
RESET='\033[0m'
|
|
17
|
+
|
|
18
|
+
# Caste emojis
|
|
19
|
+
get_emoji() {
|
|
20
|
+
case "$1" in
|
|
21
|
+
builder) echo "🔨" ;;
|
|
22
|
+
watcher) echo "👁️ " ;;
|
|
23
|
+
scout) echo "🔍" ;;
|
|
24
|
+
colonizer) echo "🗺️ " ;;
|
|
25
|
+
architect) echo "🏛️ " ;;
|
|
26
|
+
prime) echo "👑" ;;
|
|
27
|
+
*) echo "🐜" ;;
|
|
28
|
+
esac
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Status colors
|
|
32
|
+
get_status_color() {
|
|
33
|
+
case "$1" in
|
|
34
|
+
completed) echo "$GREEN" ;;
|
|
35
|
+
failed) echo "$RED" ;;
|
|
36
|
+
spawned) echo "$YELLOW" ;;
|
|
37
|
+
*) echo "$CYAN" ;;
|
|
38
|
+
esac
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render_tree() {
|
|
42
|
+
clear
|
|
43
|
+
|
|
44
|
+
# Header
|
|
45
|
+
echo -e "${BOLD}${CYAN}"
|
|
46
|
+
cat << 'EOF'
|
|
47
|
+
.-.
|
|
48
|
+
(o o) AETHER COLONY
|
|
49
|
+
| O | Spawn Tree
|
|
50
|
+
`-`
|
|
51
|
+
EOF
|
|
52
|
+
echo -e "${RESET}"
|
|
53
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
54
|
+
echo ""
|
|
55
|
+
|
|
56
|
+
# Always show Queen at depth 0
|
|
57
|
+
echo -e " ${BOLD}👑 Queen${RESET} ${DIM}(depth 0)${RESET}"
|
|
58
|
+
echo -e " ${DIM}│${RESET}"
|
|
59
|
+
|
|
60
|
+
if [[ ! -f "$SPAWN_FILE" ]]; then
|
|
61
|
+
echo -e " ${DIM}└── (no workers spawned yet)${RESET}"
|
|
62
|
+
return
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Parse spawn tree file
|
|
66
|
+
# Format: timestamp|parent_id|child_caste|child_name|task_summary|status
|
|
67
|
+
declare -A workers
|
|
68
|
+
declare -A worker_status
|
|
69
|
+
declare -A worker_task
|
|
70
|
+
declare -A worker_caste
|
|
71
|
+
declare -a roots
|
|
72
|
+
|
|
73
|
+
while IFS='|' read -r ts parent caste name task status rest; do
|
|
74
|
+
[[ -z "$name" ]] && continue
|
|
75
|
+
|
|
76
|
+
# Check if this is a status update (only 4 fields)
|
|
77
|
+
if [[ -z "$task" && -n "$caste" ]]; then
|
|
78
|
+
# This is a status update: ts|name|status|summary
|
|
79
|
+
worker_status["$parent"]="$caste"
|
|
80
|
+
continue
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
workers["$name"]="$parent"
|
|
84
|
+
worker_caste["$name"]="$caste"
|
|
85
|
+
worker_task["$name"]="$task"
|
|
86
|
+
worker_status["$name"]="${status:-spawned}"
|
|
87
|
+
|
|
88
|
+
# Track root workers (spawned by Prime or Queen)
|
|
89
|
+
if [[ "$parent" == "Prime"* || "$parent" == "prime"* || "$parent" == "Queen" ]]; then
|
|
90
|
+
roots+=("$name")
|
|
91
|
+
fi
|
|
92
|
+
done < "$SPAWN_FILE"
|
|
93
|
+
|
|
94
|
+
# Render workers in tree structure
|
|
95
|
+
# Group by parent to show hierarchy
|
|
96
|
+
printed=()
|
|
97
|
+
|
|
98
|
+
# Function to render a worker and its children
|
|
99
|
+
render_worker() {
|
|
100
|
+
local name="$1"
|
|
101
|
+
local indent="$2"
|
|
102
|
+
local depth="$3"
|
|
103
|
+
local is_last="$4"
|
|
104
|
+
|
|
105
|
+
[[ " ${printed[*]} " =~ " $name " ]] && return
|
|
106
|
+
printed+=("$name")
|
|
107
|
+
|
|
108
|
+
emoji=$(get_emoji "${worker_caste[$name]}")
|
|
109
|
+
status="${worker_status[$name]}"
|
|
110
|
+
color=$(get_status_color "$status")
|
|
111
|
+
task="${worker_task[$name]}"
|
|
112
|
+
|
|
113
|
+
# Truncate task for display
|
|
114
|
+
[[ ${#task} -gt 30 ]] && task="${task:0:27}..."
|
|
115
|
+
|
|
116
|
+
# Tree connectors
|
|
117
|
+
if [[ "$is_last" == "true" ]]; then
|
|
118
|
+
connector="└──"
|
|
119
|
+
else
|
|
120
|
+
connector="├──"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
echo -e "${indent}${DIM}${connector}${RESET} ${emoji} ${color}${name}${RESET}: ${task} ${DIM}[depth $depth]${RESET}"
|
|
124
|
+
|
|
125
|
+
# Find children of this worker
|
|
126
|
+
local children=()
|
|
127
|
+
for child in "${!workers[@]}"; do
|
|
128
|
+
if [[ "${workers[$child]}" == "$name" ]]; then
|
|
129
|
+
children+=("$child")
|
|
130
|
+
fi
|
|
131
|
+
done
|
|
132
|
+
|
|
133
|
+
# Render children
|
|
134
|
+
local child_indent="${indent} "
|
|
135
|
+
if [[ "$is_last" != "true" ]]; then
|
|
136
|
+
child_indent="${indent}${DIM}│${RESET} "
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
local child_count=${#children[@]}
|
|
140
|
+
local child_idx=0
|
|
141
|
+
for child in "${children[@]}"; do
|
|
142
|
+
child_idx=$((child_idx + 1))
|
|
143
|
+
local child_is_last="false"
|
|
144
|
+
[[ $child_idx -eq $child_count ]] && child_is_last="true"
|
|
145
|
+
render_worker "$child" "$child_indent" $((depth + 1)) "$child_is_last"
|
|
146
|
+
done
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Render root workers (spawned by Queen) at depth 1
|
|
150
|
+
local root_count=${#roots[@]}
|
|
151
|
+
local root_idx=0
|
|
152
|
+
for name in "${roots[@]}"; do
|
|
153
|
+
root_idx=$((root_idx + 1))
|
|
154
|
+
local is_last="false"
|
|
155
|
+
[[ $root_idx -eq $root_count ]] && is_last="true"
|
|
156
|
+
render_worker "$name" " " 1 "$is_last"
|
|
157
|
+
done
|
|
158
|
+
|
|
159
|
+
# Summary
|
|
160
|
+
echo ""
|
|
161
|
+
echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
162
|
+
completed=$(grep -c "completed" "$SPAWN_FILE" 2>/dev/null || echo "0")
|
|
163
|
+
active=$(grep -c "spawned" "$SPAWN_FILE" 2>/dev/null || echo "0")
|
|
164
|
+
echo -e "Workers: ${GREEN}$completed completed${RESET} | ${YELLOW}$active active${RESET}"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Initial render
|
|
168
|
+
render_tree
|
|
169
|
+
|
|
170
|
+
# Watch for changes and re-render
|
|
171
|
+
if command -v fswatch &>/dev/null; then
|
|
172
|
+
fswatch -o "$SPAWN_FILE" 2>/dev/null | while read; do
|
|
173
|
+
render_tree
|
|
174
|
+
done
|
|
175
|
+
elif command -v inotifywait &>/dev/null; then
|
|
176
|
+
while inotifywait -q -e modify "$SPAWN_FILE" 2>/dev/null; do
|
|
177
|
+
render_tree
|
|
178
|
+
done
|
|
179
|
+
else
|
|
180
|
+
# Fallback: poll every 2 seconds
|
|
181
|
+
while true; do
|
|
182
|
+
sleep 2
|
|
183
|
+
render_tree
|
|
184
|
+
done
|
|
185
|
+
fi
|