vibe-forge 0.1.0 → 0.2.1

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.
@@ -11,28 +11,110 @@
11
11
 
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
  FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
14
+
15
+ # =============================================================================
16
+ # Load Shared Libraries
17
+ # =============================================================================
18
+
19
+ # shellcheck source=lib/colors.sh
20
+ source "$SCRIPT_DIR/lib/colors.sh"
21
+ # shellcheck source=lib/constants.sh
22
+ source "$SCRIPT_DIR/lib/constants.sh"
23
+ # shellcheck source=lib/config.sh
24
+ source "$SCRIPT_DIR/lib/config.sh"
25
+
26
+ # =============================================================================
27
+ # Daemon Configuration
28
+ # =============================================================================
29
+
14
30
  CONFIG_FILE="$FORGE_ROOT/.forge/config.json"
15
31
  PID_FILE="$FORGE_ROOT/.forge/daemon.pid"
16
32
  LOG_FILE="$FORGE_ROOT/.forge/daemon.log"
17
33
  NOTIFY_FILE="$FORGE_ROOT/.forge/notifications.log"
18
34
  NOTIFIED_FILE="$FORGE_ROOT/.forge/notified-tasks.txt"
19
- STATE_FILE="$FORGE_ROOT/context/forge-state.yaml"
35
+ STATE_FILE="$FORGE_ROOT/$CONTEXT_DIR/forge-state.yaml"
36
+ LOCK_FILE="$FORGE_ROOT/.forge/daemon.lock"
20
37
 
21
- POLL_INTERVAL=2 # seconds
38
+ # Log file rotation settings
39
+ MAX_LOG_SIZE=1048576 # 1MB
40
+ MAX_NOTIFY_ENTRIES=1000
22
41
 
23
- # Load terminal type from config
42
+ # Load terminal type from config (safe parsing)
24
43
  TERMINAL_TYPE="manual"
25
44
  if [[ -f "$CONFIG_FILE" ]]; then
26
- TERMINAL_TYPE=$(grep -o '"terminal_type": *"[^"]*"' "$CONFIG_FILE" 2>/dev/null | cut -d'"' -f4)
45
+ TERMINAL_TYPE=$(json_get_string "$CONFIG_FILE" "terminal_type") || TERMINAL_TYPE="manual"
27
46
  fi
28
47
 
48
+ # =============================================================================
49
+ # Utility Functions
50
+ # =============================================================================
51
+
52
+ # Rotate log file if it gets too large
53
+ rotate_log_if_needed() {
54
+ local log="$1"
55
+ if [[ -f "$log" ]]; then
56
+ local size
57
+ size=$(stat -f%z "$log" 2>/dev/null || stat --format=%s "$log" 2>/dev/null || echo 0)
58
+ if [[ "$size" -gt "$MAX_LOG_SIZE" ]]; then
59
+ mv "$log" "${log}.old"
60
+ touch "$log"
61
+ fi
62
+ fi
63
+ }
64
+
65
+ # Trim notification entries to prevent unbounded growth
66
+ trim_notified_file() {
67
+ if [[ -f "$NOTIFIED_FILE" ]]; then
68
+ local count
69
+ count=$(wc -l < "$NOTIFIED_FILE" 2>/dev/null || echo 0)
70
+ if [[ "$count" -gt "$MAX_NOTIFY_ENTRIES" ]]; then
71
+ # Keep last 500 entries
72
+ tail -500 "$NOTIFIED_FILE" > "${NOTIFIED_FILE}.tmp"
73
+ mv "${NOTIFIED_FILE}.tmp" "$NOTIFIED_FILE"
74
+ fi
75
+ fi
76
+ }
77
+
78
+ # Safe file move with symlink protection
79
+ safe_move_task() {
80
+ local src="$1"
81
+ local dest_dir="$2"
82
+
83
+ # SECURITY: Skip symlinks to prevent symlink attacks
84
+ if [[ -L "$src" ]]; then
85
+ echo "[$(date -Iseconds)] WARNING: Skipping symlink: $src" >> "$LOG_FILE"
86
+ return 1
87
+ fi
88
+
89
+ # SECURITY: Verify source is a regular file
90
+ if [[ ! -f "$src" ]]; then
91
+ return 1
92
+ fi
93
+
94
+ # SECURITY: Verify destination is within FORGE_ROOT
95
+ local real_dest
96
+ real_dest=$(cd "$dest_dir" 2>/dev/null && pwd)
97
+ local forge_root_real
98
+ forge_root_real=$(cd "$FORGE_ROOT" 2>/dev/null && pwd)
99
+
100
+ if [[ "$real_dest" != "$forge_root_real"/* ]]; then
101
+ echo "[$(date -Iseconds)] ERROR: Destination outside FORGE_ROOT: $dest_dir" >> "$LOG_FILE"
102
+ return 1
103
+ fi
104
+
105
+ local filename
106
+ filename=$(basename "$src")
107
+ mv "$src" "$dest_dir/$filename"
108
+ }
109
+
29
110
  # =============================================================================
30
111
  # Notification Functions
31
112
  # =============================================================================
32
113
 
33
114
  notify() {
34
115
  local message="$1"
35
- local timestamp=$(date -Iseconds)
116
+ local timestamp
117
+ timestamp=$(date -Iseconds)
36
118
 
37
119
  # Log to notifications file
38
120
  echo "[$timestamp] $message" >> "$NOTIFY_FILE"
@@ -43,13 +125,7 @@ notify() {
43
125
  # Terminal bell (works in most terminals)
44
126
  printf '\a'
45
127
 
46
- # Terminal-specific notifications
47
- case "$TERMINAL_TYPE" in
48
- "windows-terminal")
49
- # Windows Terminal doesn't have a native notify, but bell works
50
- # Could potentially use PowerShell toast notification in future
51
- ;;
52
- esac
128
+ # Terminal-specific notifications could be added here
53
129
  }
54
130
 
55
131
  check_new_pending_tasks() {
@@ -57,16 +133,20 @@ check_new_pending_tasks() {
57
133
  touch "$NOTIFIED_FILE"
58
134
 
59
135
  # Check for new pending tasks
60
- for task in "$FORGE_ROOT/tasks/pending"/*.md; do
61
- if [[ -f "$task" ]]; then
62
- local filename=$(basename "$task")
136
+ for task in "$FORGE_ROOT/$TASKS_PENDING"/*.md; do
137
+ if [[ -f "$task" && ! -L "$task" ]]; then
138
+ local filename
139
+ filename=$(basename "$task")
63
140
 
64
141
  # Check if we've already notified about this task
65
- if ! grep -q "^$filename$" "$NOTIFIED_FILE" 2>/dev/null; then
66
- # Extract task info from frontmatter
67
- local task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | xargs)
68
- local task_title=$(grep -m1 "^title:" "$task" 2>/dev/null | cut -d':' -f2- | xargs)
69
- local assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | xargs)
142
+ if ! grep -qF "$filename" "$NOTIFIED_FILE" 2>/dev/null; then
143
+ # Extract task info from frontmatter safely
144
+ local task_id task_title assigned_to
145
+
146
+ # Use head to limit read and tr to sanitize
147
+ task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 100)
148
+ task_title=$(grep -m1 "^title:" "$task" 2>/dev/null | cut -d':' -f2- | tr -d '"' | head -c 200)
149
+ assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
70
150
 
71
151
  # Use filename as fallback
72
152
  task_id="${task_id:-$filename}"
@@ -79,21 +159,23 @@ check_new_pending_tasks() {
79
159
  notify "New pending task: $task_title ($task_id)"
80
160
  fi
81
161
 
82
- # Mark as notified
162
+ # Mark as notified (atomic append)
83
163
  echo "$filename" >> "$NOTIFIED_FILE"
84
164
  fi
85
165
  fi
86
166
  done
87
167
 
88
168
  # Also check needs-changes for tasks that need rework
89
- for task in "$FORGE_ROOT/tasks/needs-changes"/*.md; do
90
- if [[ -f "$task" ]]; then
91
- local filename=$(basename "$task")
169
+ for task in "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"/*.md; do
170
+ if [[ -f "$task" && ! -L "$task" ]]; then
171
+ local filename
172
+ filename=$(basename "$task")
92
173
  local notified_key="needs-changes:$filename"
93
174
 
94
- if ! grep -q "^$notified_key$" "$NOTIFIED_FILE" 2>/dev/null; then
95
- local task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | xargs)
96
- local assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | xargs)
175
+ if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
176
+ local task_id assigned_to
177
+ task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 100)
178
+ assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
97
179
 
98
180
  task_id="${task_id:-$filename}"
99
181
 
@@ -114,20 +196,21 @@ check_new_pending_tasks() {
114
196
  # =============================================================================
115
197
 
116
198
  update_state() {
117
- # Count tasks in each folder
118
- local pending=$(find "$FORGE_ROOT/tasks/pending" -name "*.md" 2>/dev/null | wc -l)
119
- local in_progress=$(find "$FORGE_ROOT/tasks/in-progress" -name "*.md" 2>/dev/null | wc -l)
120
- local completed=$(find "$FORGE_ROOT/tasks/completed" -name "*.md" 2>/dev/null | wc -l)
121
- local review=$(find "$FORGE_ROOT/tasks/review" -name "*.md" 2>/dev/null | wc -l)
122
- local approved=$(find "$FORGE_ROOT/tasks/approved" -name "*.md" 2>/dev/null | wc -l)
123
- local needs_changes=$(find "$FORGE_ROOT/tasks/needs-changes" -name "*.md" 2>/dev/null | wc -l)
124
- local merged=$(find "$FORGE_ROOT/tasks/merged" -name "*.md" 2>/dev/null | wc -l)
125
-
126
- # Calculate blocked (tasks with blocked_by in frontmatter that aren't resolved)
199
+ # Count tasks in each folder (using find with -maxdepth for safety)
200
+ local pending in_progress completed review approved needs_changes merged
201
+ pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
202
+ in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
203
+ completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
204
+ review=$(find "$FORGE_ROOT/$TASKS_REVIEW" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
205
+ approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
206
+ needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
207
+ merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
208
+
127
209
  local blocked=0
128
210
 
129
- # Write state file
130
- cat > "$STATE_FILE" << EOF
211
+ # Write state file atomically (write to temp, then move)
212
+ local temp_state="${STATE_FILE}.tmp.$$"
213
+ cat > "$temp_state" << EOF
131
214
  # Vibe Forge State
132
215
  # Auto-updated by forge-daemon
133
216
  # Last updated: $(date -Iseconds)
@@ -148,26 +231,29 @@ tasks:
148
231
 
149
232
  last_updated: $(date -Iseconds)
150
233
  EOF
234
+ mv "$temp_state" "$STATE_FILE"
151
235
  }
152
236
 
153
237
  route_completed_to_review() {
154
238
  # Move completed tasks to review queue
155
- for task in "$FORGE_ROOT/tasks/completed"/*.md; do
156
- if [[ -f "$task" ]]; then
157
- local filename=$(basename "$task")
239
+ for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
240
+ if [[ -f "$task" && ! -L "$task" ]]; then
241
+ local filename
242
+ filename=$(basename "$task")
158
243
  echo "[$(date -Iseconds)] Routing $filename to review" >> "$LOG_FILE"
159
- mv "$task" "$FORGE_ROOT/tasks/review/"
244
+ safe_move_task "$task" "$FORGE_ROOT/$TASKS_REVIEW"
160
245
  fi
161
246
  done
162
247
  }
163
248
 
164
249
  route_approved_to_merged() {
165
250
  # Move approved tasks to merged archive
166
- for task in "$FORGE_ROOT/tasks/approved"/*.md; do
167
- if [[ -f "$task" ]]; then
168
- local filename=$(basename "$task")
251
+ for task in "$FORGE_ROOT/$TASKS_APPROVED"/*.md; do
252
+ if [[ -f "$task" && ! -L "$task" ]]; then
253
+ local filename
254
+ filename=$(basename "$task")
169
255
  echo "[$(date -Iseconds)] Archiving $filename to merged" >> "$LOG_FILE"
170
- mv "$task" "$FORGE_ROOT/tasks/merged/"
256
+ safe_move_task "$task" "$FORGE_ROOT/$TASKS_MERGED"
171
257
  fi
172
258
  done
173
259
  }
@@ -175,7 +261,17 @@ route_approved_to_merged() {
175
261
  daemon_loop() {
176
262
  echo "[$(date -Iseconds)] Forge daemon started (PID: $$)" >> "$LOG_FILE"
177
263
 
264
+ # Create lock file
265
+ echo $$ > "$LOCK_FILE"
266
+
267
+ # Cleanup on exit
268
+ trap 'rm -f "$LOCK_FILE"; echo "[$(date -Iseconds)] Daemon exiting" >> "$LOG_FILE"' EXIT
269
+
270
+ local iteration=0
178
271
  while true; do
272
+ # Increment iteration counter
273
+ ((iteration++)) || true
274
+
179
275
  # Check for new tasks and notify
180
276
  check_new_pending_tasks
181
277
 
@@ -186,6 +282,13 @@ daemon_loop() {
186
282
  # Update state
187
283
  update_state
188
284
 
285
+ # Periodic maintenance (every 100 iterations = ~3 minutes)
286
+ if [[ $((iteration % 100)) -eq 0 ]]; then
287
+ rotate_log_if_needed "$LOG_FILE"
288
+ rotate_log_if_needed "$NOTIFY_FILE"
289
+ trim_notified_file
290
+ fi
291
+
189
292
  sleep "$POLL_INTERVAL"
190
293
  done
191
294
  }
@@ -197,7 +300,8 @@ daemon_loop() {
197
300
  cmd_start() {
198
301
  # Check if already running
199
302
  if [[ -f "$PID_FILE" ]]; then
200
- local pid=$(cat "$PID_FILE")
303
+ local pid
304
+ pid=$(cat "$PID_FILE")
201
305
  if kill -0 "$pid" 2>/dev/null; then
202
306
  echo "Daemon already running (PID: $pid)"
203
307
  return 0
@@ -207,22 +311,36 @@ cmd_start() {
207
311
  fi
208
312
  fi
209
313
 
210
- # Create directories if needed
314
+ # Check for lock file (another instance check)
315
+ if [[ -f "$LOCK_FILE" ]]; then
316
+ local lock_pid
317
+ lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
318
+ if kill -0 "$lock_pid" 2>/dev/null; then
319
+ echo "Another daemon instance is running (PID: $lock_pid)"
320
+ return 1
321
+ else
322
+ rm -f "$LOCK_FILE"
323
+ fi
324
+ fi
325
+
326
+ # Create directories if needed (with secure permissions)
211
327
  mkdir -p "$FORGE_ROOT/.forge"
212
- mkdir -p "$FORGE_ROOT/tasks/pending"
213
- mkdir -p "$FORGE_ROOT/tasks/in-progress"
214
- mkdir -p "$FORGE_ROOT/tasks/completed"
215
- mkdir -p "$FORGE_ROOT/tasks/review"
216
- mkdir -p "$FORGE_ROOT/tasks/approved"
217
- mkdir -p "$FORGE_ROOT/tasks/needs-changes"
218
- mkdir -p "$FORGE_ROOT/tasks/merged"
328
+ chmod 700 "$FORGE_ROOT/.forge"
329
+
330
+ mkdir -p "$FORGE_ROOT/$TASKS_PENDING"
331
+ mkdir -p "$FORGE_ROOT/$TASKS_IN_PROGRESS"
332
+ mkdir -p "$FORGE_ROOT/$TASKS_COMPLETED"
333
+ mkdir -p "$FORGE_ROOT/$TASKS_REVIEW"
334
+ mkdir -p "$FORGE_ROOT/$TASKS_APPROVED"
335
+ mkdir -p "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"
336
+ mkdir -p "$FORGE_ROOT/$TASKS_MERGED"
219
337
 
220
338
  # Start daemon in background
221
339
  daemon_loop &
222
340
  local pid=$!
223
341
  echo "$pid" > "$PID_FILE"
224
342
 
225
- echo "🔥 Forge daemon started (PID: $pid)"
343
+ log_success "Forge daemon started (PID: $pid)"
226
344
  echo " Log: $LOG_FILE"
227
345
  }
228
346
 
@@ -232,35 +350,41 @@ cmd_stop() {
232
350
  return 0
233
351
  fi
234
352
 
235
- local pid=$(cat "$PID_FILE")
353
+ local pid
354
+ pid=$(cat "$PID_FILE")
236
355
 
237
356
  if kill -0 "$pid" 2>/dev/null; then
238
357
  kill "$pid"
239
- rm -f "$PID_FILE"
358
+ rm -f "$PID_FILE" "$LOCK_FILE"
240
359
  echo "[$(date -Iseconds)] Forge daemon stopped" >> "$LOG_FILE"
241
- echo "Daemon stopped"
360
+ log_success "Daemon stopped"
242
361
  else
243
- rm -f "$PID_FILE"
362
+ rm -f "$PID_FILE" "$LOCK_FILE"
244
363
  echo "Daemon was not running (stale PID file removed)"
245
364
  fi
246
365
 
247
366
  # Update state to show inactive
248
367
  if [[ -f "$STATE_FILE" ]]; then
249
- sed -i 's/status: active/status: stopped/' "$STATE_FILE" 2>/dev/null || true
368
+ # Use sed carefully - different syntax on macOS vs Linux
369
+ if [[ "$(uname)" == "Darwin" ]]; then
370
+ sed -i '' 's/status: active/status: stopped/' "$STATE_FILE" 2>/dev/null || true
371
+ else
372
+ sed -i 's/status: active/status: stopped/' "$STATE_FILE" 2>/dev/null || true
373
+ fi
250
374
  fi
251
375
  }
252
376
 
253
377
  cmd_status() {
254
378
  echo ""
255
- echo "🔥 Forge Daemon Status"
256
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
379
+ log_header "🔥 Forge Daemon Status"
257
380
 
258
381
  if [[ -f "$PID_FILE" ]]; then
259
- local pid=$(cat "$PID_FILE")
382
+ local pid
383
+ pid=$(cat "$PID_FILE")
260
384
  if kill -0 "$pid" 2>/dev/null; then
261
- echo "Status: Running (PID: $pid)"
385
+ log_success "Running (PID: $pid)"
262
386
  else
263
- echo "Status: Stopped (stale PID file)"
387
+ log_warn "Stopped (stale PID file)"
264
388
  fi
265
389
  else
266
390
  echo "Status: Stopped"
@@ -277,7 +401,8 @@ cmd_status() {
277
401
 
278
402
  # Show recent notifications
279
403
  if [[ -f "$NOTIFY_FILE" ]]; then
280
- local notify_count=$(wc -l < "$NOTIFY_FILE" 2>/dev/null || echo "0")
404
+ local notify_count
405
+ notify_count=$(wc -l < "$NOTIFY_FILE" 2>/dev/null || echo "0")
281
406
  if [[ "$notify_count" -gt 0 ]]; then
282
407
  echo "Recent Notifications (last 5):"
283
408
  tail -5 "$NOTIFY_FILE" | sed 's/^/ /'
@@ -290,8 +415,7 @@ cmd_status() {
290
415
 
291
416
  cmd_notifications() {
292
417
  echo ""
293
- echo "🔔 Forge Notifications"
294
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
418
+ log_header "🔔 Forge Notifications"
295
419
 
296
420
  if [[ -f "$NOTIFY_FILE" ]]; then
297
421
  local count="${1:-10}"
@@ -308,7 +432,7 @@ cmd_notifications() {
308
432
 
309
433
  cmd_clear_notifications() {
310
434
  rm -f "$NOTIFY_FILE" "$NOTIFIED_FILE"
311
- echo "Notifications cleared."
435
+ log_success "Notifications cleared"
312
436
  }
313
437
 
314
438
  # =============================================================================
@@ -11,12 +11,12 @@ set -e
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
13
13
 
14
- # Colors for output
15
- RED='\033[0;31m'
16
- GREEN='\033[0;32m'
17
- YELLOW='\033[1;33m'
18
- BLUE='\033[0;34m'
19
- NC='\033[0m' # No Color
14
+ # =============================================================================
15
+ # Load Shared Libraries
16
+ # =============================================================================
17
+
18
+ # shellcheck source=lib/colors.sh
19
+ source "$SCRIPT_DIR/lib/colors.sh"
20
20
 
21
21
  # Parse arguments
22
22
  NON_INTERACTIVE=false
@@ -16,33 +16,23 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
17
17
  CONFIG_FILE="$FORGE_ROOT/.forge/config.json"
18
18
 
19
- # Colors
20
- RED='\033[0;31m'
21
- GREEN='\033[0;32m'
22
- YELLOW='\033[1;33m'
23
- BLUE='\033[0;34m'
24
- NC='\033[0m'
25
-
26
19
  # =============================================================================
27
- # Load Configuration
20
+ # Load Shared Libraries
28
21
  # =============================================================================
29
22
 
30
- load_config() {
31
- if [[ ! -f "$CONFIG_FILE" ]]; then
32
- echo -e "${RED}Error: Vibe Forge not initialized.${NC}"
33
- echo "Run 'forge init' first."
34
- exit 1
35
- fi
36
-
37
- PLATFORM=$(grep -o '"platform": *"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
38
- TERMINAL_TYPE=$(grep -o '"terminal_type": *"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
39
- GIT_BASH_PATH=$(grep -o '"git_bash_path": *"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
23
+ # shellcheck source=lib/colors.sh
24
+ source "$SCRIPT_DIR/lib/colors.sh"
25
+ # shellcheck source=lib/constants.sh
26
+ source "$SCRIPT_DIR/lib/constants.sh"
27
+ # shellcheck source=lib/config.sh
28
+ source "$SCRIPT_DIR/lib/config.sh"
29
+ # shellcheck source=lib/agents.sh
30
+ source "$SCRIPT_DIR/lib/agents.sh"
40
31
 
41
- # Default to manual if not set
42
- if [[ -z "$TERMINAL_TYPE" ]]; then
43
- TERMINAL_TYPE="manual"
44
- fi
45
- }
32
+ # Load agent configuration from JSON if available
33
+ if [[ -f "$FORGE_ROOT/$AGENTS_CONFIG" ]]; then
34
+ load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null || true
35
+ fi
46
36
 
47
37
  # =============================================================================
48
38
  # Spawn Functions
@@ -51,20 +41,24 @@ load_config() {
51
41
  spawn_windows_terminal() {
52
42
  local agent="$1"
53
43
 
54
- echo -e "${BLUE}Spawning $agent in Windows Terminal...${NC}"
44
+ log_info "Spawning $agent in Windows Terminal..."
55
45
 
56
46
  if command -v wt &> /dev/null || command -v wt.exe &> /dev/null; then
47
+ # Get display name for tab title
48
+ local display_name
49
+ display_name=$(get_agent_display_name "$agent")
50
+
57
51
  # Windows Terminal: open new tab with forge command
58
- # Use Git Bash if available, otherwise use bash
52
+ # SECURITY: $agent has already been validated through resolve_agent
59
53
  if [[ -n "$GIT_BASH_PATH" ]]; then
60
54
  local bash_path="${GIT_BASH_PATH//\//\\}"
61
- wt.exe new-tab --title "$agent" "$bash_path" -c "cd '$FORGE_ROOT' && ./bin/forge.sh start $agent"
55
+ wt.exe new-tab --title "$display_name" "$bash_path" -c "cd '$FORGE_ROOT' && ./bin/forge.sh start '$agent'"
62
56
  else
63
- wt.exe new-tab --title "$agent" bash -c "cd '$FORGE_ROOT' && ./bin/forge.sh start $agent"
57
+ wt.exe new-tab --title "$display_name" bash -c "cd '$FORGE_ROOT' && ./bin/forge.sh start '$agent'"
64
58
  fi
65
- echo -e "${GREEN}✅ $agent spawned in new Windows Terminal tab${NC}"
59
+ log_success "$display_name spawned in new Windows Terminal tab"
66
60
  else
67
- echo -e "${RED}Error: wt command not found.${NC}"
61
+ log_error "wt command not found."
68
62
  echo "Make sure Windows Terminal is installed."
69
63
  echo ""
70
64
  echo "Manual fallback:"
@@ -75,9 +69,11 @@ spawn_windows_terminal() {
75
69
 
76
70
  print_manual_instructions() {
77
71
  local agent="$1"
72
+ local display_name
73
+ display_name=$(get_agent_display_name "$agent")
78
74
 
79
75
  echo ""
80
- echo -e "${YELLOW}To start $agent manually:${NC}"
76
+ log_info "To start $display_name manually:"
81
77
  echo ""
82
78
  echo " 1. Open a new terminal window/tab"
83
79
  echo " 2. Navigate to: $FORGE_ROOT"
@@ -85,7 +81,7 @@ print_manual_instructions() {
85
81
  echo ""
86
82
  echo "Or copy this command:"
87
83
  echo ""
88
- echo " cd '$FORGE_ROOT' && ./bin/forge.sh start $agent"
84
+ echo " cd '$FORGE_ROOT' && ./bin/forge.sh start '$agent'"
89
85
  echo ""
90
86
  }
91
87
 
@@ -97,34 +93,38 @@ main() {
97
93
  local agent="${1:-}"
98
94
 
99
95
  if [[ -z "$agent" ]]; then
100
- echo -e "${RED}Error: No agent specified.${NC}"
96
+ log_error "No agent specified."
101
97
  echo ""
102
98
  echo "Usage: forge-spawn.sh <agent-name>"
103
99
  echo ""
104
- echo "Available agents:"
105
- echo " anvil - Frontend Developer"
106
- echo " furnace - Backend Developer"
107
- echo " crucible - Tester / QA"
108
- echo " sentinel - Code Reviewer"
109
- echo " scribe - Documentation"
110
- echo " herald - Release Manager"
111
- echo " ember - DevOps"
112
- echo " aegis - Security"
100
+ show_available_agents
113
101
  exit 1
114
102
  fi
115
103
 
116
- load_config
104
+ # Resolve alias to canonical agent name (SECURITY: whitelist validation)
105
+ local resolved
106
+ resolved=$(resolve_agent "$agent") || {
107
+ log_error "Unknown agent: $agent"
108
+ echo ""
109
+ show_available_agents
110
+ exit 1
111
+ }
112
+
113
+ # Load config
114
+ require_forge_config "$FORGE_ROOT"
117
115
 
118
116
  echo ""
119
- echo -e "${YELLOW}🔥 Forge Spawn: $agent${NC}"
120
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
117
+ log_header "🔥 Forge Spawn: $(get_agent_display_name "$resolved")"
118
+ if [[ "$agent" != "$resolved" ]]; then
119
+ echo " (resolved from '$agent')"
120
+ fi
121
121
 
122
122
  case "$TERMINAL_TYPE" in
123
123
  "windows-terminal")
124
- spawn_windows_terminal "$agent"
124
+ spawn_windows_terminal "$resolved"
125
125
  ;;
126
126
  *)
127
- print_manual_instructions "$agent"
127
+ print_manual_instructions "$resolved"
128
128
  ;;
129
129
  esac
130
130
  }