vibe-forge 0.4.0 → 0.8.2

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.
Files changed (129) hide show
  1. package/.claude/commands/clear-attention.md +63 -63
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +5 -5
  4. package/.claude/commands/forge.md +50 -3
  5. package/.claude/commands/need-help.md +77 -77
  6. package/.claude/commands/update-status.md +64 -64
  7. package/.claude/commands/worker-loop.md +106 -106
  8. package/.claude/hooks/worker-loop.js +37 -4
  9. package/.claude/scripts/setup-worker-loop.sh +45 -45
  10. package/.claude/settings.json +89 -0
  11. package/LICENSE +21 -21
  12. package/README.md +211 -232
  13. package/agents/aegis/personality.md +35 -1
  14. package/agents/anvil/personality.md +39 -1
  15. package/agents/architect/personality.md +26 -0
  16. package/agents/crucible/personality.md +54 -1
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +29 -1
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +52 -1
  21. package/agents/herald/personality.md +3 -1
  22. package/agents/loki/personality.md +108 -0
  23. package/agents/oracle/personality.md +284 -0
  24. package/agents/pixel/personality.md +140 -0
  25. package/agents/planning-hub/personality.md +222 -0
  26. package/agents/scribe/personality.md +3 -1
  27. package/agents/slag/personality.md +268 -0
  28. package/agents/{sentinel → temper}/personality.md +85 -9
  29. package/bin/cli.js +77 -30
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +507 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
  34. package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
  35. package/bin/dashboard/public/index.html +14 -0
  36. package/bin/dashboard/server.js +645 -0
  37. package/bin/forge-daemon.sh +176 -550
  38. package/bin/forge-setup.sh +28 -11
  39. package/bin/forge-spawn.sh +5 -5
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +210 -31
  42. package/config/agent-manifest.yaml +237 -243
  43. package/config/agents.json +207 -132
  44. package/config/task-types.yaml +111 -106
  45. package/context/agent-overrides/README.md +41 -0
  46. package/context/architecture.md +42 -0
  47. package/context/modern-conventions.md +129 -129
  48. package/docs/agents.md +473 -409
  49. package/docs/architecture.md +194 -162
  50. package/docs/commands.md +451 -388
  51. package/docs/security.md +195 -144
  52. package/package.json +38 -11
  53. package/src/lib/check-aliases.js +50 -0
  54. package/{bin → src}/lib/colors.sh +2 -1
  55. package/src/lib/config.sh +347 -0
  56. package/{bin → src}/lib/constants.sh +48 -13
  57. package/src/lib/daemon/budgets.sh +107 -0
  58. package/src/lib/daemon/dependencies.sh +146 -0
  59. package/src/lib/daemon/display.sh +128 -0
  60. package/src/lib/daemon/notifications.sh +273 -0
  61. package/src/lib/daemon/routing.sh +93 -0
  62. package/src/lib/daemon/state.sh +163 -0
  63. package/src/lib/daemon/sync.sh +103 -0
  64. package/{bin → src}/lib/database.sh +52 -0
  65. package/src/lib/frontmatter.js +106 -0
  66. package/src/lib/heimdall-setup.js +113 -0
  67. package/src/lib/heimdall.js +265 -0
  68. package/src/lib/index.sh +25 -0
  69. package/{bin → src}/lib/json.sh +7 -1
  70. package/{bin → src}/lib/terminal.js +7 -1
  71. package/.claude/settings.local.json +0 -33
  72. package/agents/forge-master/capabilities.md +0 -144
  73. package/agents/forge-master/context-template.md +0 -128
  74. package/agents/forge-master/personality.md +0 -138
  75. package/bin/lib/config.sh +0 -313
  76. package/config/task-template.md +0 -87
  77. package/context/forge-state.yaml +0 -19
  78. package/docs/TODO.md +0 -150
  79. package/docs/getting-started.md +0 -243
  80. package/docs/npm-publishing.md +0 -95
  81. package/docs/workflows/README.md +0 -32
  82. package/docs/workflows/azure-devops.md +0 -108
  83. package/docs/workflows/bitbucket.md +0 -104
  84. package/docs/workflows/git-only.md +0 -130
  85. package/docs/workflows/gitea.md +0 -168
  86. package/docs/workflows/github.md +0 -103
  87. package/docs/workflows/gitlab.md +0 -105
  88. package/docs/workflows.md +0 -454
  89. package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
  90. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
  91. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
  92. package/tasks/completed/ARCH-009-test-organization.md +0 -78
  93. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
  94. package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
  95. package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
  96. package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
  97. package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
  98. package/tasks/completed/CLEAN-001.md +0 -38
  99. package/tasks/completed/CLEAN-003.md +0 -47
  100. package/tasks/completed/CLEAN-004.md +0 -56
  101. package/tasks/completed/CLEAN-005.md +0 -75
  102. package/tasks/completed/CLEAN-006.md +0 -47
  103. package/tasks/completed/CLEAN-007.md +0 -34
  104. package/tasks/completed/CLEAN-008.md +0 -49
  105. package/tasks/completed/CLEAN-012.md +0 -58
  106. package/tasks/completed/CLEAN-013.md +0 -45
  107. package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
  108. package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
  109. package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
  110. package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
  111. package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
  112. package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
  113. package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
  114. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
  115. package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
  116. package/tasks/pending/ARCH-006-task-template-location.md +0 -64
  117. package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
  118. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
  119. package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
  120. package/tasks/pending/CLEAN-002.md +0 -29
  121. package/tasks/pending/CLEAN-009.md +0 -31
  122. package/tasks/pending/CLEAN-010.md +0 -30
  123. package/tasks/pending/CLEAN-011.md +0 -30
  124. package/tasks/pending/CLEAN-014.md +0 -32
  125. package/tasks/review/task-001.md +0 -78
  126. /package/{bin → src}/lib/agents.sh +0 -0
  127. /package/{bin → src}/lib/util.sh +0 -0
  128. /package/{bin → src}/lib/vcs.js +0 -0
  129. /package/{context → templates}/project-context-template.md +0 -0
@@ -17,17 +17,33 @@ FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
17
17
  # =============================================================================
18
18
 
19
19
  # shellcheck source=lib/colors.sh
20
- source "$SCRIPT_DIR/lib/colors.sh"
20
+ source "$SCRIPT_DIR/../src/lib/colors.sh"
21
21
  # shellcheck source=lib/constants.sh
22
- source "$SCRIPT_DIR/lib/constants.sh"
22
+ source "$SCRIPT_DIR/../src/lib/constants.sh"
23
23
  # shellcheck source=lib/config.sh
24
- source "$SCRIPT_DIR/lib/config.sh"
24
+ source "$SCRIPT_DIR/../src/lib/config.sh"
25
25
  # shellcheck source=lib/json.sh
26
- source "$SCRIPT_DIR/lib/json.sh"
26
+ source "$SCRIPT_DIR/../src/lib/json.sh"
27
27
  # shellcheck source=lib/database.sh
28
- source "$SCRIPT_DIR/lib/database.sh"
28
+ source "$SCRIPT_DIR/../src/lib/database.sh"
29
29
  # shellcheck source=lib/util.sh
30
- source "$SCRIPT_DIR/lib/util.sh"
30
+ source "$SCRIPT_DIR/../src/lib/util.sh"
31
+
32
+ # Daemon modules (routing has no deps; sync before state)
33
+ # shellcheck source=lib/daemon/routing.sh
34
+ source "$SCRIPT_DIR/../src/lib/daemon/routing.sh"
35
+ # shellcheck source=lib/daemon/notifications.sh
36
+ source "$SCRIPT_DIR/../src/lib/daemon/notifications.sh"
37
+ # shellcheck source=lib/daemon/sync.sh
38
+ source "$SCRIPT_DIR/../src/lib/daemon/sync.sh"
39
+ # shellcheck source=lib/daemon/state.sh
40
+ source "$SCRIPT_DIR/../src/lib/daemon/state.sh"
41
+ # shellcheck source=lib/daemon/display.sh
42
+ source "$SCRIPT_DIR/../src/lib/daemon/display.sh"
43
+ # shellcheck source=lib/daemon/budgets.sh
44
+ source "$SCRIPT_DIR/../src/lib/daemon/budgets.sh"
45
+ # shellcheck source=lib/daemon/dependencies.sh
46
+ source "$SCRIPT_DIR/../src/lib/daemon/dependencies.sh"
31
47
 
32
48
  # =============================================================================
33
49
  # Daemon Configuration
@@ -41,18 +57,25 @@ NOTIFY_FILE="$FORGE_ROOT/.forge/notifications.log"
41
57
  NOTIFIED_FILE="$FORGE_ROOT/.forge/notified-tasks.txt"
42
58
  STATE_FILE="$FORGE_ROOT/$CONTEXT_DIR/forge-state.yaml"
43
59
  LOCK_FILE="$FORGE_ROOT/.forge/daemon.lock"
60
+ DASHBOARD_PID_FILE="$FORGE_ROOT/.forge/dashboard.pid"
44
61
 
45
62
  # Log file rotation settings (values defined in constants.sh)
46
63
  # MAX_LOG_SIZE, MAX_NOTIFY_ENTRIES are loaded from constants.sh
47
64
 
48
- # Load terminal type from config (safe parsing)
65
+ # Load config (safe parsing via json_get_string)
49
66
  TERMINAL_TYPE="manual"
67
+ DASHBOARD_ENABLED="false"
68
+ DASHBOARD_VOICE="false"
69
+ DASHBOARD_PORT="2800"
50
70
  if [[ -f "$CONFIG_FILE" ]]; then
51
- TERMINAL_TYPE=$(json_get_string "$CONFIG_FILE" "terminal_type") || TERMINAL_TYPE="manual"
71
+ TERMINAL_TYPE=$(json_get_string "$CONFIG_FILE" "terminal_type") || TERMINAL_TYPE="manual"
72
+ DASHBOARD_ENABLED=$(json_get_string "$CONFIG_FILE" "dashboard_enabled") || DASHBOARD_ENABLED="false"
73
+ DASHBOARD_VOICE=$(json_get_string "$CONFIG_FILE" "dashboard_voice") || DASHBOARD_VOICE="false"
74
+ DASHBOARD_PORT=$(json_get_string "$CONFIG_FILE" "dashboard_port") || DASHBOARD_PORT="2800"
52
75
  fi
53
76
 
54
77
  # =============================================================================
55
- # Utility Functions
78
+ # Utility Functions (daemon-local)
56
79
  # =============================================================================
57
80
 
58
81
  # Rotate log file if it gets too large
@@ -81,428 +104,25 @@ trim_notified_file() {
81
104
  fi
82
105
  }
83
106
 
84
- # Safe file move with symlink protection
85
- safe_move_task() {
86
- local src="$1"
87
- local dest_dir="$2"
88
-
89
- # SECURITY: Skip symlinks to prevent symlink attacks
90
- if [[ -L "$src" ]]; then
91
- echo "[$(date -Iseconds)] WARNING: Skipping symlink: $src" >> "$LOG_FILE"
92
- return 1
93
- fi
94
-
95
- # SECURITY: Verify source is a regular file
96
- if [[ ! -f "$src" ]]; then
97
- return 1
98
- fi
99
-
100
- # SECURITY: Verify destination is within FORGE_ROOT
101
- local real_dest
102
- real_dest=$(cd "$dest_dir" 2>/dev/null && pwd)
103
- local forge_root_real
104
- forge_root_real=$(cd "$FORGE_ROOT" 2>/dev/null && pwd)
105
-
106
- if [[ "$real_dest" != "$forge_root_real"/* ]]; then
107
- echo "[$(date -Iseconds)] ERROR: Destination outside FORGE_ROOT: $dest_dir" >> "$LOG_FILE"
108
- return 1
109
- fi
110
-
111
- local filename
112
- filename=$(basename "$src")
113
- mv "$src" "$dest_dir/$filename"
114
- }
115
-
116
107
  # =============================================================================
117
- # Notification Functions
108
+ # Daemon Loop
118
109
  # =============================================================================
119
110
 
120
- # SECURITY: Sanitize message for safe use in shell commands
121
- # Removes/escapes characters that could cause command injection
122
- sanitize_notification_message() {
123
- local msg="$1"
124
- # Remove null bytes
125
- msg="${msg//$'\0'/}"
126
- # Remove characters that could escape out of string contexts
127
- # PowerShell: $, `, ', ", (), {}
128
- # osascript: ', ", \, $
129
- # We allow: alphanumeric, spaces, periods, commas, colons, hyphens, underscores
130
- msg=$(echo "$msg" | tr -cd 'a-zA-Z0-9 .,;:!?_-')
131
- # Limit length to prevent buffer issues
132
- msg="${msg:0:200}"
133
- echo "$msg"
134
- }
135
-
136
- notify() {
137
- local message="$1"
138
- local urgency="${2:-normal}" # normal or urgent
139
- local timestamp
140
- timestamp=$(date -Iseconds)
141
-
142
- # Log to notifications file
143
- echo "[$timestamp] $message" >> "$NOTIFY_FILE"
144
-
145
- # Log to main log
146
- echo "[$timestamp] NOTIFY: $message" >> "$LOG_FILE"
147
-
148
- # Terminal bell (works in most terminals)
149
- printf '\a'
150
-
151
- # System toast notification for urgent messages
152
- if [[ "$urgency" == "urgent" ]]; then
153
- send_system_notification "$message"
154
- fi
155
- }
156
-
157
- # Send system-level notification (platform-specific)
158
- send_system_notification() {
159
- local message="$1"
160
- local title="Vibe Forge"
161
-
162
- # SECURITY: Sanitize message to prevent command injection
163
- message=$(sanitize_notification_message "$message")
164
-
165
- case "$(uname -s)" in
166
- MINGW*|MSYS*|CYGWIN*)
167
- # Windows: Use PowerShell toast notification
168
- # SECURITY: Message is sanitized above, title is hardcoded
169
- powershell.exe -NoProfile -Command "
170
- \$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
171
- \$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
172
- \$textNodes = \$template.GetElementsByTagName('text')
173
- \$textNodes.Item(0).AppendChild(\$template.CreateTextNode('$title')) | Out-Null
174
- \$textNodes.Item(1).AppendChild(\$template.CreateTextNode('$message')) | Out-Null
175
- \$toast = [Windows.UI.Notifications.ToastNotification]::new(\$template)
176
- [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Vibe Forge').Show(\$toast)
177
- " 2>/dev/null &
178
- ;;
179
- Darwin)
180
- # macOS: Use osascript
181
- # SECURITY: Message is sanitized above
182
- osascript -e "display notification \"$message\" with title \"$title\"" 2>/dev/null &
183
- ;;
184
- Linux)
185
- # Linux: Use notify-send if available
186
- # SECURITY: notify-send handles escaping, but message is sanitized anyway
187
- if command -v notify-send &>/dev/null; then
188
- notify-send "$title" "$message" 2>/dev/null &
189
- fi
190
- ;;
191
- esac
192
- }
193
-
194
- check_new_pending_tasks() {
195
- # Create notified file if it doesn't exist
196
- touch "$NOTIFIED_FILE"
197
-
198
- # Check for new pending tasks
199
- for task in "$FORGE_ROOT/$TASKS_PENDING"/*.md; do
200
- if [[ -f "$task" && ! -L "$task" ]]; then
201
- local filename
202
- filename=$(basename "$task")
203
-
204
- # Check if we've already notified about this task
205
- if ! grep -qF "$filename" "$NOTIFIED_FILE" 2>/dev/null; then
206
- # Extract task info from frontmatter safely
207
- local task_id task_title assigned_to
208
-
209
- # Use head to limit read, tr to sanitize, and strip ANSI escape sequences
210
- 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)
211
- 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)
212
- 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)
213
-
214
- # Use filename as fallback
215
- task_id="${task_id:-$filename}"
216
- task_title="${task_title:-New task}"
217
-
218
- # Notify
219
- if [[ -n "$assigned_to" ]]; then
220
- notify "New task for $assigned_to: $task_title ($task_id)"
221
- else
222
- notify "New pending task: $task_title ($task_id)"
223
- fi
224
-
225
- # Mark as notified (atomic append)
226
- echo "$filename" >> "$NOTIFIED_FILE"
227
- fi
228
- fi
229
- done
230
-
231
- # Also check needs-changes for tasks that need rework
232
- for task in "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"/*.md; do
233
- if [[ -f "$task" && ! -L "$task" ]]; then
234
- local filename
235
- filename=$(basename "$task")
236
- local notified_key="needs-changes:$filename"
237
-
238
- if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
239
- local task_id assigned_to
240
- task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 100)
241
- assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
242
-
243
- task_id="${task_id:-$filename}"
244
-
245
- if [[ -n "$assigned_to" ]]; then
246
- notify "Task needs changes ($assigned_to): $task_id"
247
- else
248
- notify "Task needs changes: $task_id"
249
- fi
250
-
251
- echo "$notified_key" >> "$NOTIFIED_FILE"
252
- fi
253
- fi
254
- done
255
- }
256
-
257
- check_attention_needed() {
258
- # Check for workers needing attention (urgent notifications)
259
- if [[ ! -d "$FORGE_ROOT/$TASKS_ATTENTION" ]]; then
260
- return 0
261
- fi
262
-
263
- for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
264
- if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
265
- local filename
266
- filename=$(basename "$attention_file")
267
- local notified_key="attention:$filename"
268
-
269
- if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
270
- # Extract attention info
271
- local agent issue
272
- agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
273
- issue=$(grep -m1 "^##" "$attention_file" 2>/dev/null | sed 's/^## *//' | head -c 200)
274
-
275
- agent="${agent:-Unknown}"
276
- issue="${issue:-Needs attention}"
277
-
278
- # Ring bell multiple times for attention
279
- printf '\a\a\a'
280
-
281
- # Send urgent notification with toast
282
- notify "🔔 $agent needs help: $issue" "urgent"
283
-
284
- echo "$notified_key" >> "$NOTIFIED_FILE"
285
- fi
286
- fi
287
- done
288
- }
289
-
290
- # Sync agent status from JSON files to SQLite (with mtime filtering)
291
- sync_agent_status_to_db() {
292
- local status_dir="$FORGE_ROOT/$AGENT_STATUS_DIR"
293
-
294
- if [[ ! -d "$status_dir" ]]; then
295
- return 0
296
- fi
297
-
298
- for status_file in "$status_dir"/*.json; do
299
- if [[ -f "$status_file" && ! -L "$status_file" ]]; then
300
- # Get file modification time
301
- local file_mtime
302
- file_mtime=$(stat -c %Y "$status_file" 2>/dev/null || stat -f %m "$status_file" 2>/dev/null || echo "0")
303
-
304
- # Get agent name from filename
305
- local agent_name
306
- agent_name=$(basename "$status_file" .json)
307
-
308
- # Check if file has changed since last read
309
- local stored_mtime
310
- stored_mtime=$(db_get_agent_mtime "$agent_name")
311
-
312
- if [[ "$file_mtime" -gt "$stored_mtime" ]]; then
313
- # File changed - parse and update DB
314
- local agent status task message updated
315
- agent=$(json_read "$status_file" "agent" "unknown")
316
- status=$(json_read "$status_file" "status" "unknown")
317
- task=$(json_read "$status_file" "task" "")
318
- message=$(json_read "$status_file" "message" "" | head -c 80)
319
- updated=$(json_read "$status_file" "updated" "")
320
-
321
- # Upsert to database
322
- db_upsert_agent_status "$agent" "$status" "$task" "$message" "$updated" "$file_mtime"
323
-
324
- echo "[$(date -Iseconds)] Synced status for $agent: $status" >> "$LOG_FILE"
325
- fi
326
- fi
327
- done
328
- }
329
-
330
- # Build worker status from SQLite (for YAML output)
331
- build_worker_status() {
332
- local now_epoch
333
- now_epoch=$(date +%s)
334
- local stale_threshold=$STALE_STATUS_THRESHOLD
335
-
336
- # Check if we have any agent status in DB
337
- local agent_count
338
- agent_count=$(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;" 2>/dev/null || echo "0")
339
-
340
- if [[ "$agent_count" -eq 0 ]]; then
341
- return 0
342
- fi
343
-
344
- echo "workers:"
345
-
346
- # Read from database
347
- while IFS='|' read -r agent status task message updated; do
348
- local stale_marker=""
349
-
350
- # Check if stale
351
- if [[ -n "$updated" ]]; then
352
- local updated_epoch age
353
- 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")
354
- age=$((now_epoch - updated_epoch))
355
- if [[ "$age" -gt "$stale_threshold" ]]; then
356
- stale_marker=" (stale)"
357
- fi
358
- fi
359
-
360
- echo " - agent: $agent"
361
- echo " status: $status$stale_marker"
362
- if [[ -n "$task" ]]; then
363
- echo " task: $task"
364
- fi
365
- if [[ -n "$message" ]]; then
366
- echo " message: \"$message\""
367
- fi
368
- echo " updated: $updated"
369
- done < <(db_get_all_agent_statuses)
370
- }
371
-
372
- # =============================================================================
373
- # Daemon Functions
374
- # =============================================================================
375
-
376
- update_state() {
377
- # Count tasks in each folder (using find with -maxdepth for safety)
378
- local pending in_progress completed review approved needs_changes merged attention
379
- pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
380
- in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
381
- completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
382
- review=$(find "$FORGE_ROOT/$TASKS_REVIEW" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
383
- approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
384
- needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
385
- merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
386
- attention=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
387
-
388
- local blocked=0
389
-
390
- # Build attention details if any workers need help
391
- local attention_details=""
392
- if [[ "$attention" -gt 0 ]]; then
393
- attention_details=$(build_attention_details)
394
- fi
395
-
396
- # Build worker status from agent-status files
397
- local worker_status=""
398
- if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
399
- worker_status=$(build_worker_status)
400
- fi
401
-
402
- # Write state file atomically (write to temp, then move)
403
- local temp_state="${STATE_FILE}.tmp.$$"
404
- cat > "$temp_state" << EOF
405
- # Vibe Forge State
406
- # Auto-updated by forge-daemon
407
- # Last updated: $(date -Iseconds)
408
-
409
- forge:
410
- status: active
411
- daemon_pid: $$
412
-
413
- tasks:
414
- pending: $pending
415
- in_progress: $in_progress
416
- completed: $completed
417
- in_review: $review
418
- approved: $approved
419
- needs_changes: $needs_changes
420
- merged: $merged
421
- blocked: $blocked
422
- attention_needed: $attention
423
-
424
- $attention_details
425
- $worker_status
426
- last_updated: $(date -Iseconds)
427
- EOF
428
- mv "$temp_state" "$STATE_FILE"
429
- }
430
-
431
- build_attention_details() {
432
- echo "attention:"
433
- for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
434
- if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
435
- local agent created issue
436
- agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
437
- created=$(grep -m1 "^created:" "$attention_file" 2>/dev/null | cut -d':' -f2- | tr -d ' ' | head -c 30)
438
- # Get the issue line (first ## heading content or fallback)
439
- issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | tr -d '\n' | head -c 100)
440
- issue="${issue:-Needs attention}"
441
-
442
- echo " - agent: $agent"
443
- echo " since: $created"
444
- echo " issue: \"$issue\""
445
- fi
446
- done
447
- }
448
-
449
- route_completed_to_review() {
450
- # Move completed tasks to review queue
451
- for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
452
- if [[ -f "$task" && ! -L "$task" ]]; then
453
- local filename
454
- filename=$(basename "$task")
455
- echo "[$(date -Iseconds)] Routing $filename to review" >> "$LOG_FILE"
456
- safe_move_task "$task" "$FORGE_ROOT/$TASKS_REVIEW"
457
- fi
458
- done
459
- }
460
-
461
- route_approved_to_merged() {
462
- # Move approved tasks to merged archive
463
- for task in "$FORGE_ROOT/$TASKS_APPROVED"/*.md; do
464
- if [[ -f "$task" && ! -L "$task" ]]; then
465
- local filename
466
- filename=$(basename "$task")
467
- echo "[$(date -Iseconds)] Archiving $filename to merged" >> "$LOG_FILE"
468
- safe_move_task "$task" "$FORGE_ROOT/$TASKS_MERGED"
469
- fi
470
- done
471
- }
472
-
473
- # Determine daemon state based on activity (for adaptive polling)
474
- determine_daemon_state() {
475
- # Check if there are in-progress tasks
476
- local in_progress_count
477
- in_progress_count=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
478
-
479
- # Check if there are active workers
480
- local active_workers
481
- active_workers=$(db_count_active_workers 2>/dev/null || echo "0")
482
-
483
- if [[ "$in_progress_count" -gt 0 ]] || [[ "$active_workers" -gt 0 ]]; then
484
- echo "active"
485
- else
486
- echo "idle"
487
- fi
488
- }
489
-
490
- # Get current poll interval in seconds (from DB, with fallback)
491
- get_poll_interval() {
492
- local interval_ms
493
- interval_ms=$(db_get_poll_interval_ms 2>/dev/null || echo "30000")
494
- # Convert ms to seconds (bash integer division)
495
- echo $((interval_ms / 1000))
496
- }
497
-
498
111
  daemon_loop() {
499
112
  echo "[$(date -Iseconds)] Forge daemon started (PID: $$)" >> "$LOG_FILE"
500
113
 
501
- # Create lock file
502
- echo $$ > "$LOCK_FILE"
114
+ # Atomic lock file acquisition (noclobber = O_EXCL, prevents TOCTOU race on systems without flock)
115
+ if ! (set -o noclobber; echo $$ > "$LOCK_FILE") 2>/dev/null; then
116
+ echo "[$(date -Iseconds)] Lock file already held by another daemon instance — exiting" >> "$LOG_FILE"
117
+ exit 1
118
+ fi
503
119
 
504
- # Initialize database
505
- db_init
120
+ # Initialize database (will attempt sqlite3 install if missing)
121
+ if ! db_init; then
122
+ echo "[$(date -Iseconds)] FATAL: Database initialization failed (sqlite3 missing?)" >> "$LOG_FILE"
123
+ rm -f "$LOCK_FILE"
124
+ exit 1
125
+ fi
506
126
  echo "[$(date -Iseconds)] Database initialized at $FORGE_DB" >> "$LOG_FILE"
507
127
 
508
128
  # Cleanup on exit
@@ -525,6 +145,15 @@ daemon_loop() {
525
145
  # Check for workers needing attention (urgent)
526
146
  check_attention_needed
527
147
 
148
+ # Check for Heimdall escalations (lab worker policy violations)
149
+ check_heimdall_escalations "$FORGE_ROOT/_vibe-chain-output/worker-inbox"
150
+
151
+ # Check token budget warnings for long-running agents (T2-F1)
152
+ check_token_budgets
153
+
154
+ # Check task dependencies (T2-H2)
155
+ check_task_dependencies
156
+
528
157
  # Route tasks
529
158
  route_completed_to_review
530
159
  route_approved_to_merged
@@ -557,6 +186,105 @@ daemon_loop() {
557
186
  done
558
187
  }
559
188
 
189
+ # =============================================================================
190
+ # Watchdog
191
+ # =============================================================================
192
+
193
+ # WATCHDOG_MAX_RESTARTS -- how many times the watchdog will restart a crashed
194
+ # daemon_loop before giving up. Override via environment if needed.
195
+ WATCHDOG_MAX_RESTARTS="${WATCHDOG_MAX_RESTARTS:-5}"
196
+
197
+ # watchdog_loop
198
+ # Wraps daemon_loop with automatic restart on unexpected exit.
199
+ # Intentional stops (cmd_stop) are detected via the .stopping sentinel file,
200
+ # which cmd_stop creates before sending SIGTERM to the watchdog PID.
201
+ watchdog_loop() {
202
+ local restart_count=0
203
+ local backoff=5 # seconds; doubles each restart up to 60s
204
+
205
+ while [[ $restart_count -lt $WATCHDOG_MAX_RESTARTS ]]; do
206
+ daemon_loop
207
+ local exit_code=$?
208
+
209
+ # Intentional stop: cmd_stop touches .stopping before killing us
210
+ if [[ -f "$FORGE_ROOT/.forge/daemon.stopping" ]]; then
211
+ rm -f "$FORGE_ROOT/.forge/daemon.stopping"
212
+ echo "[$(date -Iseconds)] WATCHDOG: daemon stopped intentionally" >> "$LOG_FILE"
213
+ return 0
214
+ fi
215
+
216
+ restart_count=$((restart_count + 1))
217
+ echo "[$(date -Iseconds)] WATCHDOG: daemon exited unexpectedly (code $exit_code), restarting in ${backoff}s (attempt $restart_count/$WATCHDOG_MAX_RESTARTS)" >> "$LOG_FILE"
218
+
219
+ sleep "$backoff"
220
+ backoff=$((backoff * 2))
221
+ [[ $backoff -gt 60 ]] && backoff=60
222
+ done
223
+
224
+ echo "[$(date -Iseconds)] WATCHDOG: daemon exceeded $WATCHDOG_MAX_RESTARTS restart attempts — giving up" >> "$LOG_FILE"
225
+ }
226
+
227
+ # =============================================================================
228
+ # Dashboard Commands
229
+ # =============================================================================
230
+
231
+ cmd_dashboard_start() {
232
+ if ! command -v node &>/dev/null; then
233
+ log_warn "Node.js not found — cannot start dashboard"
234
+ return 1
235
+ fi
236
+
237
+ # Check already running
238
+ if [[ -f "$DASHBOARD_PID_FILE" ]]; then
239
+ local pid
240
+ pid=$(cat "$DASHBOARD_PID_FILE")
241
+ if kill -0 "$pid" 2>/dev/null; then
242
+ echo "Dashboard already running (PID: $pid)"
243
+ echo " URL: http://localhost:${DASHBOARD_PORT}"
244
+ return 0
245
+ else
246
+ rm -f "$DASHBOARD_PID_FILE"
247
+ fi
248
+ fi
249
+
250
+ DASHBOARD_PORT="$DASHBOARD_PORT" node "$SCRIPT_DIR/dashboard/server.js" \
251
+ >> "$FORGE_ROOT/.forge/dashboard.log" 2>&1 &
252
+ local pid=$!
253
+ echo "$pid" > "$DASHBOARD_PID_FILE"
254
+
255
+ log_success "Dashboard started (PID: $pid)"
256
+ echo " URL: http://localhost:${DASHBOARD_PORT}"
257
+ echo " Log: $FORGE_ROOT/.forge/dashboard.log"
258
+
259
+ # Open browser (platform-specific, best-effort)
260
+ case "$(uname -s)" in
261
+ MINGW*|MSYS*|CYGWIN*)
262
+ cmd.exe /c start "" "http://localhost:${DASHBOARD_PORT}" 2>/dev/null & ;;
263
+ Darwin)
264
+ open "http://localhost:${DASHBOARD_PORT}" 2>/dev/null & ;;
265
+ Linux)
266
+ xdg-open "http://localhost:${DASHBOARD_PORT}" 2>/dev/null & ;;
267
+ esac
268
+ }
269
+
270
+ cmd_dashboard_stop() {
271
+ if [[ ! -f "$DASHBOARD_PID_FILE" ]]; then
272
+ echo "Dashboard not running"
273
+ return 0
274
+ fi
275
+
276
+ local pid
277
+ pid=$(cat "$DASHBOARD_PID_FILE")
278
+ if kill -0 "$pid" 2>/dev/null; then
279
+ kill "$pid"
280
+ rm -f "$DASHBOARD_PID_FILE"
281
+ log_success "Dashboard stopped"
282
+ else
283
+ rm -f "$DASHBOARD_PID_FILE"
284
+ echo "Dashboard was not running (stale PID file removed)"
285
+ fi
286
+ }
287
+
560
288
  # =============================================================================
561
289
  # Commands
562
290
  # =============================================================================
@@ -618,14 +346,19 @@ cmd_start() {
618
346
  mkdir -p "$FORGE_ROOT/$TASKS_ATTENTION"
619
347
  mkdir -p "$FORGE_ROOT/$AGENT_STATUS_DIR"
620
348
 
621
- # Start daemon in background
622
- daemon_loop &
349
+ # Start daemon under watchdog (auto-restarts on unexpected exit)
350
+ watchdog_loop &
623
351
  local pid=$!
624
352
  echo "$pid" > "$PID_FILE"
625
353
 
626
354
  log_success "Forge daemon started (PID: $pid)"
627
355
  echo " Log: $LOG_FILE"
628
356
 
357
+ # Auto-launch dashboard if voice or dashboard is enabled in config
358
+ if [[ "$DASHBOARD_VOICE" == "true" ]] || [[ "$DASHBOARD_ENABLED" == "true" ]]; then
359
+ cmd_dashboard_start
360
+ fi
361
+
629
362
  # Note: flock is automatically released when the fd is closed (script exits)
630
363
  }
631
364
 
@@ -639,6 +372,8 @@ cmd_stop() {
639
372
  pid=$(cat "$PID_FILE")
640
373
 
641
374
  if kill -0 "$pid" 2>/dev/null; then
375
+ # Signal the watchdog that this is an intentional stop before killing it
376
+ touch "$FORGE_ROOT/.forge/daemon.stopping"
642
377
  kill "$pid"
643
378
  rm -f "$PID_FILE" "$LOCK_FILE"
644
379
  echo "[$(date -Iseconds)] Forge daemon stopped" >> "$LOG_FILE"
@@ -648,6 +383,9 @@ cmd_stop() {
648
383
  echo "Daemon was not running (stale PID file removed)"
649
384
  fi
650
385
 
386
+ # Stop dashboard if running
387
+ cmd_dashboard_stop
388
+
651
389
  # Update state to show inactive
652
390
  if [[ -f "$STATE_FILE" ]]; then
653
391
  # Use cross-platform sed helper
@@ -655,129 +393,6 @@ cmd_stop() {
655
393
  fi
656
394
  }
657
395
 
658
- # =============================================================================
659
- # Status Display Helper Functions
660
- # =============================================================================
661
-
662
- # Display daemon running status (PID check)
663
- display_daemon_status() {
664
- if [[ -f "$PID_FILE" ]]; then
665
- local pid
666
- pid=$(cat "$PID_FILE")
667
- if kill -0 "$pid" 2>/dev/null; then
668
- log_success "Running (PID: $pid)"
669
- else
670
- log_warn "Stopped (stale PID file)"
671
- fi
672
- else
673
- echo "Status: Stopped"
674
- fi
675
- }
676
-
677
- # Display task counts from state file
678
- display_task_counts() {
679
- if [[ -f "$STATE_FILE" ]]; then
680
- echo "Task Counts:"
681
- grep -E "pending:|in_progress:|completed:|in_review:|approved:|needs_changes:|merged:|attention_needed:" "$STATE_FILE" | sed 's/^/ /'
682
- fi
683
- }
684
-
685
- # Display workers needing attention (urgent alerts)
686
- display_attention_needed() {
687
- local attention_count
688
- attention_count=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
689
- if [[ "$attention_count" -gt 0 ]]; then
690
- echo -e "${RED}🔔 ATTENTION NEEDED:${NC}"
691
- for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
692
- if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
693
- local agent issue
694
- agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
695
- issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | head -c 80)
696
- echo -e " ${YELLOW}$agent${NC}: $issue"
697
- fi
698
- done
699
- echo ""
700
- fi
701
- }
702
-
703
- # Get status icon for worker status
704
- get_status_icon() {
705
- local status="$1"
706
- case "$status" in
707
- "working") echo "🔨" ;;
708
- "idle") echo "💤" ;;
709
- "blocked") echo "🚫" ;;
710
- "testing") echo "🧪" ;;
711
- "reviewing") echo "👁️" ;;
712
- *) echo "❓" ;;
713
- esac
714
- }
715
-
716
- # Display active worker statuses with staleness indicators
717
- display_worker_status() {
718
- if [[ ! -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
719
- return
720
- fi
721
-
722
- local status_count
723
- status_count=$(find "$FORGE_ROOT/$AGENT_STATUS_DIR" -maxdepth 1 -name "*.json" -type f 2>/dev/null | wc -l)
724
- if [[ "$status_count" -eq 0 ]]; then
725
- return
726
- fi
727
-
728
- echo "Active Workers:"
729
- local now_epoch stale_threshold
730
- now_epoch=$(date +%s)
731
- stale_threshold=$STALE_STATUS_THRESHOLD
732
-
733
- for status_file in "$FORGE_ROOT/$AGENT_STATUS_DIR"/*.json; do
734
- if [[ -f "$status_file" && ! -L "$status_file" ]]; then
735
- local agent status task updated stale_marker icon
736
- agent=$(json_read "$status_file" "agent" "unknown")
737
- status=$(json_read "$status_file" "status" "unknown")
738
- task=$(json_read "$status_file" "task" "")
739
- updated=$(json_read "$status_file" "updated" "")
740
-
741
- # Check staleness
742
- stale_marker=""
743
- if [[ -n "$updated" ]]; then
744
- local updated_epoch age
745
- 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")
746
- age=$((now_epoch - updated_epoch))
747
- if [[ "$age" -gt "$stale_threshold" ]]; then
748
- stale_marker=" ${YELLOW}(stale)${NC}"
749
- fi
750
- fi
751
-
752
- icon=$(get_status_icon "$status")
753
-
754
- if [[ -n "$task" ]]; then
755
- echo -e " $icon ${CYAN}$agent${NC}: $status ($task)$stale_marker"
756
- else
757
- echo -e " $icon ${CYAN}$agent${NC}: $status$stale_marker"
758
- fi
759
- fi
760
- done
761
- echo ""
762
- }
763
-
764
- # Display recent notifications from log
765
- display_recent_notifications() {
766
- if [[ -f "$NOTIFY_FILE" ]]; then
767
- local notify_count
768
- notify_count=$(wc -l < "$NOTIFY_FILE" 2>/dev/null || echo "0")
769
- if [[ "$notify_count" -gt 0 ]]; then
770
- echo "Recent Notifications (last 5):"
771
- tail -5 "$NOTIFY_FILE" | sed 's/^/ /'
772
- echo ""
773
- fi
774
- fi
775
- }
776
-
777
- # =============================================================================
778
- # Main Status Command
779
- # =============================================================================
780
-
781
396
  cmd_status() {
782
397
  echo ""
783
398
  log_header "🔥 Forge Daemon Status"
@@ -841,8 +456,19 @@ main() {
841
456
  "clear")
842
457
  cmd_clear_notifications
843
458
  ;;
459
+ "dashboard")
460
+ shift
461
+ case "${1:-start}" in
462
+ "start") cmd_dashboard_start ;;
463
+ "stop") cmd_dashboard_stop ;;
464
+ *)
465
+ echo "Usage: forge-daemon.sh dashboard [start|stop]"
466
+ exit $EXIT_INVALID_ARGUMENT
467
+ ;;
468
+ esac
469
+ ;;
844
470
  *)
845
- echo "Usage: forge-daemon.sh [start|stop|status|notifications|clear]"
471
+ echo "Usage: forge-daemon.sh [start|stop|status|notifications|clear|dashboard]"
846
472
  exit $EXIT_INVALID_ARGUMENT
847
473
  ;;
848
474
  esac