vibe-forge 0.3.6 → 0.3.11

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.
@@ -66,6 +66,24 @@ Where other agents create, Scribe preserves. The README that saves a future deve
66
66
  9. Move to /tasks/completed/
67
67
  ```
68
68
 
69
+ ### Status Reporting
70
+
71
+ Keep the Planning Hub and daemon informed of your status:
72
+
73
+ ```bash
74
+ /update-status idle # When waiting for tasks
75
+ /update-status working TASK-024 # When starting a task
76
+ /update-status blocked TASK-024 # When stuck (then /need-help if needed)
77
+ /update-status idle # When task complete
78
+ ```
79
+
80
+ Update status at key moments:
81
+
82
+ 1. **Startup**: Report `idle` (ready for work)
83
+ 2. **Task pickup**: Report `working` with task ID
84
+ 3. **Blocked**: Report `blocked`, then use `/need-help` if human input needed
85
+ 4. **Completion**: Report `idle` after moving task to completed
86
+
69
87
  ### Output Format
70
88
  ```markdown
71
89
  ## Completion Summary
package/bin/cli.js CHANGED
@@ -21,6 +21,8 @@ const REPO_URL = 'https://github.com/SpasticPalate/vibe-forge.git';
21
21
  const FORGE_DIR = '_vibe-forge';
22
22
 
23
23
  // Colors for terminal output
24
+ // NOTE: Intentionally self-contained - cli.js runs standalone via npx before
25
+ // the rest of Vibe Forge is installed, so it cannot share with colors.sh
24
26
  const colors = {
25
27
  reset: '\x1b[0m',
26
28
  red: '\x1b[31m',
@@ -199,6 +201,9 @@ async function initCommand() {
199
201
 
200
202
  log('');
201
203
 
204
+ // Update project's .gitignore to ignore tool internals but keep project data
205
+ updateGitignore();
206
+
202
207
  // Run the setup script
203
208
  logInfo('Running setup...');
204
209
  log('');
@@ -212,6 +217,50 @@ async function initCommand() {
212
217
  }
213
218
  }
214
219
 
220
+ function updateGitignore() {
221
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
222
+ const forgeIgnoreMarker = '# Vibe Forge';
223
+ const forgeIgnoreBlock = `
224
+ # Vibe Forge
225
+ # Tool internals (regenerated on update)
226
+ _vibe-forge/.git/
227
+ _vibe-forge/bin/
228
+ _vibe-forge/agents/
229
+ _vibe-forge/config/
230
+ _vibe-forge/docs/
231
+ _vibe-forge/tests/
232
+ _vibe-forge/src/
233
+ _vibe-forge/node_modules/
234
+ _vibe-forge/package*.json
235
+ _vibe-forge/*.md
236
+ _vibe-forge/LICENSE
237
+ _vibe-forge/.github/
238
+
239
+ # Keep project data (tasks, context) - these ARE committed
240
+ # !_vibe-forge/tasks/
241
+ # !_vibe-forge/context/
242
+ # !_vibe-forge/.claude/
243
+ `;
244
+
245
+ try {
246
+ let content = '';
247
+ if (fs.existsSync(gitignorePath)) {
248
+ content = fs.readFileSync(gitignorePath, 'utf8');
249
+ // Check if already has forge entries
250
+ if (content.includes(forgeIgnoreMarker)) {
251
+ return; // Already configured
252
+ }
253
+ }
254
+
255
+ // Append forge ignore block
256
+ const newContent = content.trimEnd() + '\n' + forgeIgnoreBlock;
257
+ fs.writeFileSync(gitignorePath, newContent);
258
+ logSuccess('Updated .gitignore for Vibe Forge');
259
+ } catch (err) {
260
+ logError(`Warning: Could not update .gitignore: ${err.message}`);
261
+ }
262
+ }
263
+
215
264
  async function updateCommand() {
216
265
  showBanner();
217
266
 
@@ -113,6 +113,7 @@ safe_move_task() {
113
113
 
114
114
  notify() {
115
115
  local message="$1"
116
+ local urgency="${2:-normal}" # normal or urgent
116
117
  local timestamp
117
118
  timestamp=$(date -Iseconds)
118
119
 
@@ -125,7 +126,41 @@ notify() {
125
126
  # Terminal bell (works in most terminals)
126
127
  printf '\a'
127
128
 
128
- # Terminal-specific notifications could be added here
129
+ # System toast notification for urgent messages
130
+ if [[ "$urgency" == "urgent" ]]; then
131
+ send_system_notification "$message"
132
+ fi
133
+ }
134
+
135
+ # Send system-level notification (platform-specific)
136
+ send_system_notification() {
137
+ local message="$1"
138
+ local title="Vibe Forge"
139
+
140
+ case "$(uname -s)" in
141
+ MINGW*|MSYS*|CYGWIN*)
142
+ # Windows: Use PowerShell toast notification
143
+ powershell.exe -NoProfile -Command "
144
+ \$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
145
+ \$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
146
+ \$textNodes = \$template.GetElementsByTagName('text')
147
+ \$textNodes.Item(0).AppendChild(\$template.CreateTextNode('$title')) | Out-Null
148
+ \$textNodes.Item(1).AppendChild(\$template.CreateTextNode('$message')) | Out-Null
149
+ \$toast = [Windows.UI.Notifications.ToastNotification]::new(\$template)
150
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Vibe Forge').Show(\$toast)
151
+ " 2>/dev/null &
152
+ ;;
153
+ Darwin)
154
+ # macOS: Use osascript
155
+ osascript -e "display notification \"$message\" with title \"$title\"" 2>/dev/null &
156
+ ;;
157
+ Linux)
158
+ # Linux: Use notify-send if available
159
+ if command -v notify-send &>/dev/null; then
160
+ notify-send "$title" "$message" 2>/dev/null &
161
+ fi
162
+ ;;
163
+ esac
129
164
  }
130
165
 
131
166
  check_new_pending_tasks() {
@@ -143,10 +178,10 @@ check_new_pending_tasks() {
143
178
  # Extract task info from frontmatter safely
144
179
  local task_id task_title assigned_to
145
180
 
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)
181
+ # Use head to limit read, tr to sanitize, and strip ANSI escape sequences
182
+ task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | tr -d '\033' | sed 's/\[[0-9;]*m//g' | head -c 100)
183
+ task_title=$(grep -m1 "^title:" "$task" 2>/dev/null | cut -d':' -f2- | tr -d '"' | tr -d '\033' | sed 's/\[[0-9;]*m//g' | head -c 200)
184
+ assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | tr -d '\033' | sed 's/\[[0-9;]*m//g' | head -c 50)
150
185
 
151
186
  # Use filename as fallback
152
187
  task_id="${task_id:-$filename}"
@@ -191,13 +226,93 @@ check_new_pending_tasks() {
191
226
  done
192
227
  }
193
228
 
229
+ check_attention_needed() {
230
+ # Check for workers needing attention (urgent notifications)
231
+ if [[ ! -d "$FORGE_ROOT/$TASKS_ATTENTION" ]]; then
232
+ return 0
233
+ fi
234
+
235
+ for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
236
+ if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
237
+ local filename
238
+ filename=$(basename "$attention_file")
239
+ local notified_key="attention:$filename"
240
+
241
+ if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
242
+ # Extract attention info
243
+ local agent issue
244
+ agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
245
+ issue=$(grep -m1 "^##" "$attention_file" 2>/dev/null | sed 's/^## *//' | head -c 200)
246
+
247
+ agent="${agent:-Unknown}"
248
+ issue="${issue:-Needs attention}"
249
+
250
+ # Ring bell multiple times for attention
251
+ printf '\a\a\a'
252
+
253
+ # Send urgent notification with toast
254
+ notify "🔔 $agent needs help: $issue" "urgent"
255
+
256
+ echo "$notified_key" >> "$NOTIFIED_FILE"
257
+ fi
258
+ fi
259
+ done
260
+ }
261
+
262
+ # Build worker status from agent-status files
263
+ build_worker_status() {
264
+ local status_dir="$FORGE_ROOT/$AGENT_STATUS_DIR"
265
+ local now_epoch
266
+ now_epoch=$(date +%s)
267
+ local stale_threshold=300 # 5 minutes
268
+
269
+ if [[ ! -d "$status_dir" ]]; then
270
+ return 0
271
+ fi
272
+
273
+ echo "workers:"
274
+ for status_file in "$status_dir"/*.json; do
275
+ if [[ -f "$status_file" && ! -L "$status_file" ]]; then
276
+ local agent status task message updated updated_epoch age stale_marker
277
+
278
+ # Parse JSON status file
279
+ agent=$(jq -r '.agent // "unknown"' "$status_file" 2>/dev/null)
280
+ status=$(jq -r '.status // "unknown"' "$status_file" 2>/dev/null)
281
+ task=$(jq -r '.task // ""' "$status_file" 2>/dev/null)
282
+ message=$(jq -r '.message // ""' "$status_file" 2>/dev/null | head -c 80)
283
+ updated=$(jq -r '.updated // ""' "$status_file" 2>/dev/null)
284
+
285
+ # Check if stale (not updated in 5+ minutes)
286
+ stale_marker=""
287
+ if [[ -n "$updated" ]]; then
288
+ # Convert ISO timestamp to epoch (cross-platform)
289
+ updated_epoch=$(date -d "$updated" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${updated%Z}" +%s 2>/dev/null || echo "0")
290
+ age=$((now_epoch - updated_epoch))
291
+ if [[ "$age" -gt "$stale_threshold" ]]; then
292
+ stale_marker=" (stale)"
293
+ fi
294
+ fi
295
+
296
+ echo " - agent: $agent"
297
+ echo " status: $status$stale_marker"
298
+ if [[ -n "$task" ]]; then
299
+ echo " task: $task"
300
+ fi
301
+ if [[ -n "$message" ]]; then
302
+ echo " message: \"$message\""
303
+ fi
304
+ echo " updated: $updated"
305
+ fi
306
+ done
307
+ }
308
+
194
309
  # =============================================================================
195
310
  # Daemon Functions
196
311
  # =============================================================================
197
312
 
198
313
  update_state() {
199
314
  # Count tasks in each folder (using find with -maxdepth for safety)
200
- local pending in_progress completed review approved needs_changes merged
315
+ local pending in_progress completed review approved needs_changes merged attention
201
316
  pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
202
317
  in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
203
318
  completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
@@ -205,9 +320,22 @@ update_state() {
205
320
  approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
206
321
  needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
207
322
  merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
323
+ attention=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
208
324
 
209
325
  local blocked=0
210
326
 
327
+ # Build attention details if any workers need help
328
+ local attention_details=""
329
+ if [[ "$attention" -gt 0 ]]; then
330
+ attention_details=$(build_attention_details)
331
+ fi
332
+
333
+ # Build worker status from agent-status files
334
+ local worker_status=""
335
+ if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
336
+ worker_status=$(build_worker_status)
337
+ fi
338
+
211
339
  # Write state file atomically (write to temp, then move)
212
340
  local temp_state="${STATE_FILE}.tmp.$$"
213
341
  cat > "$temp_state" << EOF
@@ -228,12 +356,33 @@ tasks:
228
356
  needs_changes: $needs_changes
229
357
  merged: $merged
230
358
  blocked: $blocked
359
+ attention_needed: $attention
231
360
 
361
+ $attention_details
362
+ $worker_status
232
363
  last_updated: $(date -Iseconds)
233
364
  EOF
234
365
  mv "$temp_state" "$STATE_FILE"
235
366
  }
236
367
 
368
+ build_attention_details() {
369
+ echo "attention:"
370
+ for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
371
+ if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
372
+ local agent created issue
373
+ agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
374
+ created=$(grep -m1 "^created:" "$attention_file" 2>/dev/null | cut -d':' -f2- | tr -d ' ' | head -c 30)
375
+ # Get the issue line (first ## heading content or fallback)
376
+ issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | tr -d '\n' | head -c 100)
377
+ issue="${issue:-Needs attention}"
378
+
379
+ echo " - agent: $agent"
380
+ echo " since: $created"
381
+ echo " issue: \"$issue\""
382
+ fi
383
+ done
384
+ }
385
+
237
386
  route_completed_to_review() {
238
387
  # Move completed tasks to review queue
239
388
  for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
@@ -275,6 +424,9 @@ daemon_loop() {
275
424
  # Check for new tasks and notify
276
425
  check_new_pending_tasks
277
426
 
427
+ # Check for workers needing attention (urgent)
428
+ check_attention_needed
429
+
278
430
  # Route tasks
279
431
  route_completed_to_review
280
432
  route_approved_to_merged
@@ -334,6 +486,8 @@ cmd_start() {
334
486
  mkdir -p "$FORGE_ROOT/$TASKS_APPROVED"
335
487
  mkdir -p "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"
336
488
  mkdir -p "$FORGE_ROOT/$TASKS_MERGED"
489
+ mkdir -p "$FORGE_ROOT/$TASKS_ATTENTION"
490
+ mkdir -p "$FORGE_ROOT/$AGENT_STATUS_DIR"
337
491
 
338
492
  # Start daemon in background
339
493
  daemon_loop &
@@ -394,11 +548,76 @@ cmd_status() {
394
548
 
395
549
  if [[ -f "$STATE_FILE" ]]; then
396
550
  echo "Task Counts:"
397
- grep -E "pending:|in_progress:|completed:|in_review:|approved:|needs_changes:|merged:" "$STATE_FILE" | sed 's/^/ /'
551
+ grep -E "pending:|in_progress:|completed:|in_review:|approved:|needs_changes:|merged:|attention_needed:" "$STATE_FILE" | sed 's/^/ /'
398
552
  fi
399
553
 
400
554
  echo ""
401
555
 
556
+ # Show attention-needed workers prominently
557
+ local attention_count
558
+ attention_count=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
559
+ if [[ "$attention_count" -gt 0 ]]; then
560
+ echo -e "${RED}🔔 ATTENTION NEEDED:${NC}"
561
+ for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
562
+ if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
563
+ local agent issue
564
+ agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
565
+ issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | head -c 80)
566
+ echo -e " ${YELLOW}$agent${NC}: $issue"
567
+ fi
568
+ done
569
+ echo ""
570
+ fi
571
+
572
+ # Show worker status
573
+ if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
574
+ local status_count
575
+ status_count=$(find "$FORGE_ROOT/$AGENT_STATUS_DIR" -maxdepth 1 -name "*.json" -type f 2>/dev/null | wc -l)
576
+ if [[ "$status_count" -gt 0 ]]; then
577
+ echo "Active Workers:"
578
+ local now_epoch stale_threshold
579
+ now_epoch=$(date +%s)
580
+ stale_threshold=300
581
+ for status_file in "$FORGE_ROOT/$AGENT_STATUS_DIR"/*.json; do
582
+ if [[ -f "$status_file" && ! -L "$status_file" ]]; then
583
+ local agent status task updated stale_marker icon
584
+ agent=$(jq -r '.agent // "unknown"' "$status_file" 2>/dev/null)
585
+ status=$(jq -r '.status // "unknown"' "$status_file" 2>/dev/null)
586
+ task=$(jq -r '.task // ""' "$status_file" 2>/dev/null)
587
+ updated=$(jq -r '.updated // ""' "$status_file" 2>/dev/null)
588
+
589
+ # Check staleness
590
+ stale_marker=""
591
+ if [[ -n "$updated" ]]; then
592
+ local updated_epoch age
593
+ updated_epoch=$(date -d "$updated" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${updated%Z}" +%s 2>/dev/null || echo "0")
594
+ age=$((now_epoch - updated_epoch))
595
+ if [[ "$age" -gt "$stale_threshold" ]]; then
596
+ stale_marker=" ${YELLOW}(stale)${NC}"
597
+ fi
598
+ fi
599
+
600
+ # Status icon
601
+ case "$status" in
602
+ "working") icon="🔨" ;;
603
+ "idle") icon="💤" ;;
604
+ "blocked") icon="🚫" ;;
605
+ "testing") icon="🧪" ;;
606
+ "reviewing") icon="👁️" ;;
607
+ *) icon="❓" ;;
608
+ esac
609
+
610
+ if [[ -n "$task" ]]; then
611
+ echo -e " $icon ${CYAN}$agent${NC}: $status ($task)$stale_marker"
612
+ else
613
+ echo -e " $icon ${CYAN}$agent${NC}: $status$stale_marker"
614
+ fi
615
+ fi
616
+ done
617
+ echo ""
618
+ fi
619
+ fi
620
+
402
621
  # Show recent notifications
403
622
  if [[ -f "$NOTIFY_FILE" ]]; then
404
623
  local notify_count
@@ -304,6 +304,48 @@ configure_daemon() {
304
304
  fi
305
305
  }
306
306
 
307
+ # =============================================================================
308
+ # STEP 7b: Configure Worker Loop (Persistent Mode)
309
+ # =============================================================================
310
+
311
+ configure_worker_loop() {
312
+ echo ""
313
+ echo "Worker Loop Configuration"
314
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
315
+ echo ""
316
+ echo "Worker Loop keeps agents running continuously."
317
+ echo "When a worker finishes a task, it automatically checks for new work"
318
+ echo "instead of exiting. Great for longer coding sessions."
319
+ echo ""
320
+
321
+ local enable_worker_loop="n"
322
+
323
+ if [[ "$NON_INTERACTIVE" == "false" ]]; then
324
+ read -p "Enable persistent worker mode? (y/N): " choice
325
+ if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
326
+ enable_worker_loop="y"
327
+ fi
328
+ fi
329
+
330
+ # Update config with worker loop preference
331
+ local config_file="$FORGE_ROOT/.forge/config.json"
332
+ if [[ -f "$config_file" ]]; then
333
+ # Add worker_loop_enabled to config
334
+ sed -i 's/"daemon_enabled": \(true\|false\)/"daemon_enabled": \1,\n "worker_loop_enabled": '"$( [[ "$enable_worker_loop" == "y" ]] && echo "true" || echo "false" )"'/' "$config_file" 2>/dev/null || true
335
+ fi
336
+
337
+ if [[ "$enable_worker_loop" == "y" ]]; then
338
+ echo ""
339
+ echo -e "${GREEN}✅ Worker Loop enabled${NC}"
340
+ echo " Workers will keep running and check for new tasks"
341
+ else
342
+ echo ""
343
+ echo -e "${YELLOW}ℹ️ Worker Loop disabled${NC}"
344
+ echo " Workers will exit after completing their tasks"
345
+ echo " Enable later with: forge config worker-loop on"
346
+ fi
347
+ }
348
+
307
349
  # =============================================================================
308
350
  # STEP 8: Install Slash Command
309
351
  # =============================================================================
@@ -476,6 +518,7 @@ main() {
476
518
  if validate_setup; then
477
519
  configure_terminal
478
520
  configure_daemon
521
+ configure_worker_loop
479
522
  install_slash_command
480
523
  create_project_context
481
524
  setup_complete
@@ -31,7 +31,9 @@ source "$SCRIPT_DIR/lib/agents.sh"
31
31
 
32
32
  # Load agent configuration from JSON if available
33
33
  if [[ -f "$FORGE_ROOT/$AGENTS_CONFIG" ]]; then
34
- load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null || true
34
+ if ! load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null; then
35
+ log_warn "Failed to load agents.json, using fallback defaults"
36
+ fi
35
37
  fi
36
38
 
37
39
  # =============================================================================
@@ -44,9 +46,17 @@ spawn_windows_terminal() {
44
46
  log_info "Spawning $agent in Windows Terminal..."
45
47
 
46
48
  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
+ # Get display info for tab
50
+ local display_name icon tab_color
49
51
  display_name=$(get_agent_display_name "$agent")
52
+ icon=$(get_agent_icon "$agent")
53
+ tab_color=$(get_agent_tab_color "$agent")
54
+
55
+ # Build tab title with icon
56
+ local tab_title="$display_name"
57
+ if [[ -n "$icon" ]]; then
58
+ tab_title="$icon $display_name"
59
+ fi
50
60
 
51
61
  # Convert FORGE_ROOT to Windows path for wt.exe
52
62
  # Git Bash paths like /c/foo become C:/foo
@@ -57,15 +67,21 @@ spawn_windows_terminal() {
57
67
  win_forge_root="$FORGE_ROOT"
58
68
  fi
59
69
 
70
+ # Build wt command with optional tab color
71
+ local wt_args=(-w 0 new-tab --title "$tab_title")
72
+ if [[ -n "$tab_color" ]]; then
73
+ wt_args+=(--tabColor "$tab_color")
74
+ fi
75
+
60
76
  # Windows Terminal: open new tab with forge command
61
77
  # SECURITY: $agent has already been validated through resolve_agent
62
78
  if [[ -n "$GIT_BASH_PATH" ]]; then
63
79
  local bash_path="${GIT_BASH_PATH//\//\\}"
64
- wt.exe -w 0 new-tab --title "$display_name" "$bash_path" -c "cd \"$win_forge_root\" && ./bin/forge.sh start \"$agent\""
80
+ wt.exe "${wt_args[@]}" "$bash_path" -c "cd \"$win_forge_root\" && ./bin/forge.sh start \"$agent\""
65
81
  else
66
- wt.exe -w 0 new-tab --title "$display_name" bash -c "cd \"$win_forge_root\" && ./bin/forge.sh start \"$agent\""
82
+ wt.exe "${wt_args[@]}" bash -c "cd \"$win_forge_root\" && ./bin/forge.sh start \"$agent\""
67
83
  fi
68
- log_success "$display_name spawned in new Windows Terminal tab"
84
+ log_success "$tab_title spawned in new Windows Terminal tab"
69
85
  else
70
86
  log_error "wt command not found."
71
87
  echo "Make sure Windows Terminal is installed."
@@ -123,7 +139,9 @@ main() {
123
139
  require_forge_config "$FORGE_ROOT"
124
140
 
125
141
  echo ""
126
- log_header "🔥 Forge Spawn: $(get_agent_display_name "$resolved")"
142
+ local icon
143
+ icon=$(get_agent_icon "$resolved")
144
+ log_header "${icon:-🔥} Forge Spawn: $(get_agent_display_name "$resolved")"
127
145
  if [[ "$agent" != "$resolved" ]]; then
128
146
  echo " (resolved from '$agent')"
129
147
  fi
package/bin/forge.sh CHANGED
@@ -41,7 +41,9 @@ source "$SCRIPT_DIR/lib/agents.sh"
41
41
  # Load agent configuration from JSON if available
42
42
  # This overwrites the fallback values in constants.sh
43
43
  if [[ -f "$FORGE_ROOT/$AGENTS_CONFIG" ]]; then
44
- load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null || true
44
+ if ! load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null; then
45
+ log_warn "Failed to load agents.json, using fallback defaults"
46
+ fi
45
47
  fi
46
48
 
47
49
  # =============================================================================
@@ -116,7 +118,13 @@ On startup, you MUST immediately display the team assembly welcome as shown in t
116
118
 
117
119
  # Startup Instructions
118
120
 
119
- On startup: Announce yourself (name, icon, role), check tasks/pending/ and tasks/needs-changes/ for your work, summarize what you find, and ask if you should begin.
121
+ On startup:
122
+ 1. Announce yourself briefly (icon, name, role)
123
+ 2. Check tasks/pending/ and tasks/needs-changes/ for tasks assigned to you
124
+ 3. If you find assigned tasks, IMMEDIATELY begin working on them (no confirmation needed)
125
+ 4. If no tasks found, announce you're idle and ready for work
126
+
127
+ You are autonomous - when assigned work exists, start it without asking permission.
120
128
  "
121
129
  fi
122
130
 
@@ -233,6 +241,73 @@ cmd_daemon() {
233
241
  esac
234
242
  }
235
243
 
244
+ cmd_config() {
245
+ local setting="${1:-}"
246
+ local value="${2:-}"
247
+ local config_file="$FORGE_ROOT/.forge/config.json"
248
+
249
+ if [[ ! -f "$config_file" ]]; then
250
+ log_error "Forge not initialized. Run 'forge init' first."
251
+ exit 1
252
+ fi
253
+
254
+ case "$setting" in
255
+ "worker-loop"|"loop")
256
+ case "$value" in
257
+ "on"|"true"|"1"|"enable"|"enabled")
258
+ # Enable worker loop
259
+ if jq -e '.worker_loop_enabled' "$config_file" > /dev/null 2>&1; then
260
+ jq '.worker_loop_enabled = true' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
261
+ else
262
+ jq '. + {worker_loop_enabled: true}' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
263
+ fi
264
+ log_success "Worker Loop enabled"
265
+ echo "Workers will now keep running to check for new tasks."
266
+ ;;
267
+ "off"|"false"|"0"|"disable"|"disabled")
268
+ # Disable worker loop
269
+ if jq -e '.worker_loop_enabled' "$config_file" > /dev/null 2>&1; then
270
+ jq '.worker_loop_enabled = false' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
271
+ else
272
+ jq '. + {worker_loop_enabled: false}' "$config_file" > "$config_file.tmp" && mv "$config_file.tmp" "$config_file"
273
+ fi
274
+ log_success "Worker Loop disabled"
275
+ echo "Workers will exit after completing their tasks."
276
+ ;;
277
+ ""|"status")
278
+ # Show current status
279
+ local current
280
+ current=$(jq -r '.worker_loop_enabled // false' "$config_file")
281
+ if [[ "$current" == "true" ]]; then
282
+ echo "Worker Loop: enabled"
283
+ else
284
+ echo "Worker Loop: disabled"
285
+ fi
286
+ ;;
287
+ *)
288
+ echo "Usage: forge config worker-loop [on|off|status]"
289
+ ;;
290
+ esac
291
+ ;;
292
+ "")
293
+ # Show all config
294
+ echo ""
295
+ log_header "Forge Configuration"
296
+ echo ""
297
+ jq '.' "$config_file"
298
+ echo ""
299
+ ;;
300
+ *)
301
+ log_error "Unknown setting: $setting"
302
+ echo ""
303
+ echo "Available settings:"
304
+ echo " worker-loop Toggle persistent worker mode (on/off)"
305
+ echo ""
306
+ echo "Usage: forge config <setting> [value]"
307
+ ;;
308
+ esac
309
+ }
310
+
236
311
  cmd_help() {
237
312
  echo ""
238
313
  log_header "🔥 Vibe Forge"
@@ -247,17 +322,15 @@ cmd_help() {
247
322
  echo " status Show current forge status"
248
323
  echo " test Validate setup is working"
249
324
  echo " daemon <action> Manage background daemon (start|stop|status|notifications|clear)"
325
+ echo " config <setting> View or change configuration settings"
250
326
  echo " help Show this help message"
251
327
  echo ""
252
- echo "Agents (with aliases):"
253
- echo " anvil (frontend, ui, fe) - Frontend Developer"
254
- echo " furnace (backend, api, be) - Backend Developer"
255
- echo " crucible (test, testing, qa) - Tester / QA"
256
- echo " sentinel (review, reviewer, cr) - Code Reviewer"
257
- echo " scribe (docs, documentation) - Documentation"
258
- echo " herald (release, deploy) - Release Manager"
259
- echo " ember (devops, ops, infra) - DevOps"
260
- echo " aegis (security, sec, appsec) - Security"
328
+ show_available_agents
329
+ echo ""
330
+ echo "Configuration:"
331
+ echo " forge config Show all settings"
332
+ echo " forge config worker-loop on Enable persistent worker mode"
333
+ echo " forge config worker-loop off Disable persistent worker mode"
261
334
  echo ""
262
335
  echo "Examples:"
263
336
  echo " forge Start Planning Hub"
@@ -298,6 +371,10 @@ main() {
298
371
  shift
299
372
  cmd_daemon "$@"
300
373
  ;;
374
+ "config")
375
+ shift
376
+ cmd_config "$@"
377
+ ;;
301
378
  "help"|"--help"|"-h")
302
379
  cmd_help
303
380
  ;;