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.
- package/.claude/commands/clear-attention.md +63 -63
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +5 -5
- package/.claude/commands/forge.md +50 -3
- package/.claude/commands/need-help.md +77 -77
- package/.claude/commands/update-status.md +64 -64
- package/.claude/commands/worker-loop.md +106 -106
- package/.claude/hooks/worker-loop.js +37 -4
- package/.claude/scripts/setup-worker-loop.sh +45 -45
- package/.claude/settings.json +89 -0
- package/LICENSE +21 -21
- package/README.md +211 -232
- package/agents/aegis/personality.md +35 -1
- package/agents/anvil/personality.md +39 -1
- package/agents/architect/personality.md +26 -0
- package/agents/crucible/personality.md +54 -1
- package/agents/crucible-x/personality.md +210 -0
- package/agents/ember/personality.md +29 -1
- package/agents/flux/personality.md +248 -0
- package/agents/furnace/personality.md +52 -1
- package/agents/herald/personality.md +3 -1
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +284 -0
- package/agents/pixel/personality.md +140 -0
- package/agents/planning-hub/personality.md +222 -0
- package/agents/scribe/personality.md +3 -1
- package/agents/slag/personality.md +268 -0
- package/agents/{sentinel → temper}/personality.md +85 -9
- package/bin/cli.js +77 -30
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +507 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
- package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +645 -0
- package/bin/forge-daemon.sh +176 -550
- package/bin/forge-setup.sh +28 -11
- package/bin/forge-spawn.sh +5 -5
- package/bin/forge.cmd +83 -83
- package/bin/forge.sh +210 -31
- package/config/agent-manifest.yaml +237 -243
- package/config/agents.json +207 -132
- package/config/task-types.yaml +111 -106
- package/context/agent-overrides/README.md +41 -0
- package/context/architecture.md +42 -0
- package/context/modern-conventions.md +129 -129
- package/docs/agents.md +473 -409
- package/docs/architecture.md +194 -162
- package/docs/commands.md +451 -388
- package/docs/security.md +195 -144
- package/package.json +38 -11
- package/src/lib/check-aliases.js +50 -0
- package/{bin → src}/lib/colors.sh +2 -1
- package/src/lib/config.sh +347 -0
- package/{bin → src}/lib/constants.sh +48 -13
- package/src/lib/daemon/budgets.sh +107 -0
- package/src/lib/daemon/dependencies.sh +146 -0
- package/src/lib/daemon/display.sh +128 -0
- package/src/lib/daemon/notifications.sh +273 -0
- package/src/lib/daemon/routing.sh +93 -0
- package/src/lib/daemon/state.sh +163 -0
- package/src/lib/daemon/sync.sh +103 -0
- package/{bin → src}/lib/database.sh +52 -0
- package/src/lib/frontmatter.js +106 -0
- package/src/lib/heimdall-setup.js +113 -0
- package/src/lib/heimdall.js +265 -0
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +7 -1
- package/{bin → src}/lib/terminal.js +7 -1
- package/.claude/settings.local.json +0 -33
- package/agents/forge-master/capabilities.md +0 -144
- package/agents/forge-master/context-template.md +0 -128
- package/agents/forge-master/personality.md +0 -138
- package/bin/lib/config.sh +0 -313
- package/config/task-template.md +0 -87
- package/context/forge-state.yaml +0 -19
- package/docs/TODO.md +0 -150
- package/docs/getting-started.md +0 -243
- package/docs/npm-publishing.md +0 -95
- package/docs/workflows/README.md +0 -32
- package/docs/workflows/azure-devops.md +0 -108
- package/docs/workflows/bitbucket.md +0 -104
- package/docs/workflows/git-only.md +0 -130
- package/docs/workflows/gitea.md +0 -168
- package/docs/workflows/github.md +0 -103
- package/docs/workflows/gitlab.md +0 -105
- package/docs/workflows.md +0 -454
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
- package/tasks/completed/ARCH-009-test-organization.md +0 -78
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
- package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
- package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
- package/tasks/completed/CLEAN-001.md +0 -38
- package/tasks/completed/CLEAN-003.md +0 -47
- package/tasks/completed/CLEAN-004.md +0 -56
- package/tasks/completed/CLEAN-005.md +0 -75
- package/tasks/completed/CLEAN-006.md +0 -47
- package/tasks/completed/CLEAN-007.md +0 -34
- package/tasks/completed/CLEAN-008.md +0 -49
- package/tasks/completed/CLEAN-012.md +0 -58
- package/tasks/completed/CLEAN-013.md +0 -45
- package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
- package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
- package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
- package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
- package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
- package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
- package/tasks/pending/ARCH-006-task-template-location.md +0 -64
- package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
- package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
- package/tasks/pending/CLEAN-002.md +0 -29
- package/tasks/pending/CLEAN-009.md +0 -31
- package/tasks/pending/CLEAN-010.md +0 -30
- package/tasks/pending/CLEAN-011.md +0 -30
- package/tasks/pending/CLEAN-014.md +0 -32
- package/tasks/review/task-001.md +0 -78
- /package/{bin → src}/lib/agents.sh +0 -0
- /package/{bin → src}/lib/util.sh +0 -0
- /package/{bin → src}/lib/vcs.js +0 -0
- /package/{context → templates}/project-context-template.md +0 -0
package/bin/forge-daemon.sh
CHANGED
|
@@ -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
|
|
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")
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
622
|
-
|
|
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
|