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.

Files changed (38) hide show
  1. package/.claude/commands/ant/ant.md +89 -0
  2. package/.claude/commands/ant/build.md +504 -0
  3. package/.claude/commands/ant/colonize.md +94 -0
  4. package/.claude/commands/ant/continue.md +674 -0
  5. package/.claude/commands/ant/feedback.md +65 -0
  6. package/.claude/commands/ant/flag.md +108 -0
  7. package/.claude/commands/ant/flags.md +139 -0
  8. package/.claude/commands/ant/focus.md +42 -0
  9. package/.claude/commands/ant/init.md +129 -0
  10. package/.claude/commands/ant/migrate-state.md +140 -0
  11. package/.claude/commands/ant/organize.md +210 -0
  12. package/.claude/commands/ant/pause-colony.md +87 -0
  13. package/.claude/commands/ant/phase.md +86 -0
  14. package/.claude/commands/ant/plan.md +409 -0
  15. package/.claude/commands/ant/redirect.md +53 -0
  16. package/.claude/commands/ant/resume-colony.md +83 -0
  17. package/.claude/commands/ant/status.md +122 -0
  18. package/.claude/commands/ant/watch.md +220 -0
  19. package/LICENSE +21 -0
  20. package/README.md +258 -0
  21. package/bin/cli.js +196 -0
  22. package/package.json +35 -0
  23. package/runtime/DISCIPLINES.md +93 -0
  24. package/runtime/QUEEN_ANT_ARCHITECTURE.md +347 -0
  25. package/runtime/aether-utils.sh +760 -0
  26. package/runtime/coding-standards.md +197 -0
  27. package/runtime/debugging.md +207 -0
  28. package/runtime/docs/pheromones.md +213 -0
  29. package/runtime/learning.md +254 -0
  30. package/runtime/planning.md +159 -0
  31. package/runtime/tdd.md +257 -0
  32. package/runtime/utils/atomic-write.sh +213 -0
  33. package/runtime/utils/colorize-log.sh +132 -0
  34. package/runtime/utils/file-lock.sh +122 -0
  35. package/runtime/utils/watch-spawn-tree.sh +185 -0
  36. package/runtime/verification-loop.md +159 -0
  37. package/runtime/verification.md +116 -0
  38. 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