@sugar-crash-studios/vibe-forge 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- package/tasks/review/task-001.md +78 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Vibe Forge - Background Daemon
|
|
4
|
+
# Monitors task folders and routes files automatically
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# forge-daemon.sh start - Start the daemon
|
|
8
|
+
# forge-daemon.sh stop - Stop the daemon
|
|
9
|
+
# forge-daemon.sh status - Check daemon status
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Load Shared Libraries
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
# shellcheck source=lib/colors.sh
|
|
20
|
+
source "$SCRIPT_DIR/lib/colors.sh"
|
|
21
|
+
# shellcheck source=lib/constants.sh
|
|
22
|
+
source "$SCRIPT_DIR/lib/constants.sh"
|
|
23
|
+
# shellcheck source=lib/config.sh
|
|
24
|
+
source "$SCRIPT_DIR/lib/config.sh"
|
|
25
|
+
# shellcheck source=lib/json.sh
|
|
26
|
+
source "$SCRIPT_DIR/lib/json.sh"
|
|
27
|
+
# shellcheck source=lib/database.sh
|
|
28
|
+
source "$SCRIPT_DIR/lib/database.sh"
|
|
29
|
+
# shellcheck source=lib/util.sh
|
|
30
|
+
source "$SCRIPT_DIR/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/lib/daemon/routing.sh"
|
|
35
|
+
# shellcheck source=lib/daemon/notifications.sh
|
|
36
|
+
source "$SCRIPT_DIR/lib/daemon/notifications.sh"
|
|
37
|
+
# shellcheck source=lib/daemon/sync.sh
|
|
38
|
+
source "$SCRIPT_DIR/lib/daemon/sync.sh"
|
|
39
|
+
# shellcheck source=lib/daemon/state.sh
|
|
40
|
+
source "$SCRIPT_DIR/lib/daemon/state.sh"
|
|
41
|
+
# shellcheck source=lib/daemon/display.sh
|
|
42
|
+
source "$SCRIPT_DIR/lib/daemon/display.sh"
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Daemon Configuration
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
CONFIG_FILE="$FORGE_ROOT/.forge/config.json"
|
|
49
|
+
FORGE_DB="$FORGE_ROOT/.forge/forge.db"
|
|
50
|
+
PID_FILE="$FORGE_ROOT/.forge/daemon.pid"
|
|
51
|
+
LOG_FILE="$FORGE_ROOT/.forge/daemon.log"
|
|
52
|
+
NOTIFY_FILE="$FORGE_ROOT/.forge/notifications.log"
|
|
53
|
+
NOTIFIED_FILE="$FORGE_ROOT/.forge/notified-tasks.txt"
|
|
54
|
+
STATE_FILE="$FORGE_ROOT/$CONTEXT_DIR/forge-state.yaml"
|
|
55
|
+
LOCK_FILE="$FORGE_ROOT/.forge/daemon.lock"
|
|
56
|
+
DASHBOARD_PID_FILE="$FORGE_ROOT/.forge/dashboard.pid"
|
|
57
|
+
|
|
58
|
+
# Log file rotation settings (values defined in constants.sh)
|
|
59
|
+
# MAX_LOG_SIZE, MAX_NOTIFY_ENTRIES are loaded from constants.sh
|
|
60
|
+
|
|
61
|
+
# Load config (safe parsing via json_get_string)
|
|
62
|
+
TERMINAL_TYPE="manual"
|
|
63
|
+
DASHBOARD_ENABLED="false"
|
|
64
|
+
DASHBOARD_VOICE="false"
|
|
65
|
+
DASHBOARD_PORT="2800"
|
|
66
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
67
|
+
TERMINAL_TYPE=$(json_get_string "$CONFIG_FILE" "terminal_type") || TERMINAL_TYPE="manual"
|
|
68
|
+
DASHBOARD_ENABLED=$(json_get_string "$CONFIG_FILE" "dashboard_enabled") || DASHBOARD_ENABLED="false"
|
|
69
|
+
DASHBOARD_VOICE=$(json_get_string "$CONFIG_FILE" "dashboard_voice") || DASHBOARD_VOICE="false"
|
|
70
|
+
DASHBOARD_PORT=$(json_get_string "$CONFIG_FILE" "dashboard_port") || DASHBOARD_PORT="2800"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Utility Functions (daemon-local)
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
# Rotate log file if it gets too large
|
|
78
|
+
rotate_log_if_needed() {
|
|
79
|
+
local log="$1"
|
|
80
|
+
if [[ -f "$log" ]]; then
|
|
81
|
+
local size
|
|
82
|
+
size=$(stat -f%z "$log" 2>/dev/null || stat --format=%s "$log" 2>/dev/null || echo 0)
|
|
83
|
+
if [[ "$size" -gt "$MAX_LOG_SIZE" ]]; then
|
|
84
|
+
mv "$log" "${log}.old"
|
|
85
|
+
touch "$log"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Trim notification entries to prevent unbounded growth
|
|
91
|
+
trim_notified_file() {
|
|
92
|
+
if [[ -f "$NOTIFIED_FILE" ]]; then
|
|
93
|
+
local count
|
|
94
|
+
count=$(wc -l < "$NOTIFIED_FILE" 2>/dev/null || echo 0)
|
|
95
|
+
if [[ "$count" -gt "$MAX_NOTIFY_ENTRIES" ]]; then
|
|
96
|
+
# Keep last 500 entries
|
|
97
|
+
tail -500 "$NOTIFIED_FILE" > "${NOTIFIED_FILE}.tmp"
|
|
98
|
+
mv "${NOTIFIED_FILE}.tmp" "$NOTIFIED_FILE"
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# Daemon Loop
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
daemon_loop() {
|
|
108
|
+
echo "[$(date -Iseconds)] Forge daemon started (PID: $$)" >> "$LOG_FILE"
|
|
109
|
+
|
|
110
|
+
# Atomic lock file acquisition (noclobber = O_EXCL, prevents TOCTOU race on systems without flock)
|
|
111
|
+
if ! (set -o noclobber; echo $$ > "$LOCK_FILE") 2>/dev/null; then
|
|
112
|
+
echo "[$(date -Iseconds)] Lock file already held by another daemon instance — exiting" >> "$LOG_FILE"
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Initialize database
|
|
117
|
+
db_init
|
|
118
|
+
echo "[$(date -Iseconds)] Database initialized at $FORGE_DB" >> "$LOG_FILE"
|
|
119
|
+
|
|
120
|
+
# Cleanup on exit
|
|
121
|
+
trap 'rm -f "$LOCK_FILE"; echo "[$(date -Iseconds)] Daemon exiting" >> "$LOG_FILE"' EXIT
|
|
122
|
+
|
|
123
|
+
local iteration=0
|
|
124
|
+
local current_state="idle"
|
|
125
|
+
local poll_interval=2 # Start fast, adjust based on activity
|
|
126
|
+
|
|
127
|
+
while true; do
|
|
128
|
+
# Increment iteration counter
|
|
129
|
+
((iteration++)) || true
|
|
130
|
+
|
|
131
|
+
# Sync agent status from JSON files to SQLite (with mtime filtering)
|
|
132
|
+
sync_agent_status_to_db
|
|
133
|
+
|
|
134
|
+
# Check for new tasks and notify
|
|
135
|
+
check_new_pending_tasks
|
|
136
|
+
|
|
137
|
+
# Check for workers needing attention (urgent)
|
|
138
|
+
check_attention_needed
|
|
139
|
+
|
|
140
|
+
# Check for Heimdall escalations (lab worker policy violations)
|
|
141
|
+
check_heimdall_escalations "$FORGE_ROOT/_vibe-chain-output/worker-inbox"
|
|
142
|
+
|
|
143
|
+
# Route tasks
|
|
144
|
+
route_completed_to_review
|
|
145
|
+
route_approved_to_merged
|
|
146
|
+
|
|
147
|
+
# Update state file
|
|
148
|
+
update_state
|
|
149
|
+
|
|
150
|
+
# Adaptive polling: check activity and adjust interval
|
|
151
|
+
local new_state
|
|
152
|
+
new_state=$(determine_daemon_state)
|
|
153
|
+
if [[ "$new_state" != "$current_state" ]]; then
|
|
154
|
+
current_state="$new_state"
|
|
155
|
+
db_set_daemon_state "$current_state"
|
|
156
|
+
poll_interval=$(get_poll_interval)
|
|
157
|
+
echo "[$(date -Iseconds)] State changed to $current_state, poll interval: ${poll_interval}s" >> "$LOG_FILE"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# Periodic maintenance (every MAINTENANCE_INTERVAL iterations)
|
|
161
|
+
if [[ $((iteration % MAINTENANCE_INTERVAL)) -eq 0 ]]; then
|
|
162
|
+
rotate_log_if_needed "$LOG_FILE"
|
|
163
|
+
rotate_log_if_needed "$NOTIFY_FILE"
|
|
164
|
+
trim_notified_file
|
|
165
|
+
# Cleanup stale agent status
|
|
166
|
+
db_cleanup_stale_agents "$STALE_CLEANUP_MINUTES"
|
|
167
|
+
# Prune old history
|
|
168
|
+
db_prune_history "$HISTORY_PRUNE_DAYS"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
sleep "$poll_interval"
|
|
172
|
+
done
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# =============================================================================
|
|
176
|
+
# Watchdog
|
|
177
|
+
# =============================================================================
|
|
178
|
+
|
|
179
|
+
# WATCHDOG_MAX_RESTARTS -- how many times the watchdog will restart a crashed
|
|
180
|
+
# daemon_loop before giving up. Override via environment if needed.
|
|
181
|
+
WATCHDOG_MAX_RESTARTS="${WATCHDOG_MAX_RESTARTS:-5}"
|
|
182
|
+
|
|
183
|
+
# watchdog_loop
|
|
184
|
+
# Wraps daemon_loop with automatic restart on unexpected exit.
|
|
185
|
+
# Intentional stops (cmd_stop) are detected via the .stopping sentinel file,
|
|
186
|
+
# which cmd_stop creates before sending SIGTERM to the watchdog PID.
|
|
187
|
+
watchdog_loop() {
|
|
188
|
+
local restart_count=0
|
|
189
|
+
local backoff=5 # seconds; doubles each restart up to 60s
|
|
190
|
+
|
|
191
|
+
while [[ $restart_count -lt $WATCHDOG_MAX_RESTARTS ]]; do
|
|
192
|
+
daemon_loop
|
|
193
|
+
local exit_code=$?
|
|
194
|
+
|
|
195
|
+
# Intentional stop: cmd_stop touches .stopping before killing us
|
|
196
|
+
if [[ -f "$FORGE_ROOT/.forge/daemon.stopping" ]]; then
|
|
197
|
+
rm -f "$FORGE_ROOT/.forge/daemon.stopping"
|
|
198
|
+
echo "[$(date -Iseconds)] WATCHDOG: daemon stopped intentionally" >> "$LOG_FILE"
|
|
199
|
+
return 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
restart_count=$((restart_count + 1))
|
|
203
|
+
echo "[$(date -Iseconds)] WATCHDOG: daemon exited unexpectedly (code $exit_code), restarting in ${backoff}s (attempt $restart_count/$WATCHDOG_MAX_RESTARTS)" >> "$LOG_FILE"
|
|
204
|
+
|
|
205
|
+
sleep "$backoff"
|
|
206
|
+
backoff=$((backoff * 2))
|
|
207
|
+
[[ $backoff -gt 60 ]] && backoff=60
|
|
208
|
+
done
|
|
209
|
+
|
|
210
|
+
echo "[$(date -Iseconds)] WATCHDOG: daemon exceeded $WATCHDOG_MAX_RESTARTS restart attempts — giving up" >> "$LOG_FILE"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# Dashboard Commands
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
cmd_dashboard_start() {
|
|
218
|
+
if ! command -v node &>/dev/null; then
|
|
219
|
+
log_warn "Node.js not found — cannot start dashboard"
|
|
220
|
+
return 1
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# Check already running
|
|
224
|
+
if [[ -f "$DASHBOARD_PID_FILE" ]]; then
|
|
225
|
+
local pid
|
|
226
|
+
pid=$(cat "$DASHBOARD_PID_FILE")
|
|
227
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
228
|
+
echo "Dashboard already running (PID: $pid)"
|
|
229
|
+
echo " URL: http://localhost:${DASHBOARD_PORT}"
|
|
230
|
+
return 0
|
|
231
|
+
else
|
|
232
|
+
rm -f "$DASHBOARD_PID_FILE"
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
DASHBOARD_PORT="$DASHBOARD_PORT" node "$SCRIPT_DIR/dashboard/server.js" \
|
|
237
|
+
>> "$FORGE_ROOT/.forge/dashboard.log" 2>&1 &
|
|
238
|
+
local pid=$!
|
|
239
|
+
echo "$pid" > "$DASHBOARD_PID_FILE"
|
|
240
|
+
|
|
241
|
+
log_success "Dashboard started (PID: $pid)"
|
|
242
|
+
echo " URL: http://localhost:${DASHBOARD_PORT}"
|
|
243
|
+
echo " Log: $FORGE_ROOT/.forge/dashboard.log"
|
|
244
|
+
|
|
245
|
+
# Open browser (platform-specific, best-effort)
|
|
246
|
+
case "$(uname -s)" in
|
|
247
|
+
MINGW*|MSYS*|CYGWIN*)
|
|
248
|
+
cmd.exe /c start "" "http://localhost:${DASHBOARD_PORT}" 2>/dev/null & ;;
|
|
249
|
+
Darwin)
|
|
250
|
+
open "http://localhost:${DASHBOARD_PORT}" 2>/dev/null & ;;
|
|
251
|
+
Linux)
|
|
252
|
+
xdg-open "http://localhost:${DASHBOARD_PORT}" 2>/dev/null & ;;
|
|
253
|
+
esac
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
cmd_dashboard_stop() {
|
|
257
|
+
if [[ ! -f "$DASHBOARD_PID_FILE" ]]; then
|
|
258
|
+
echo "Dashboard not running"
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
local pid
|
|
263
|
+
pid=$(cat "$DASHBOARD_PID_FILE")
|
|
264
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
265
|
+
kill "$pid"
|
|
266
|
+
rm -f "$DASHBOARD_PID_FILE"
|
|
267
|
+
log_success "Dashboard stopped"
|
|
268
|
+
else
|
|
269
|
+
rm -f "$DASHBOARD_PID_FILE"
|
|
270
|
+
echo "Dashboard was not running (stale PID file removed)"
|
|
271
|
+
fi
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# =============================================================================
|
|
275
|
+
# Commands
|
|
276
|
+
# =============================================================================
|
|
277
|
+
|
|
278
|
+
cmd_start() {
|
|
279
|
+
# Create directories if needed (with secure permissions)
|
|
280
|
+
mkdir -p "$FORGE_ROOT/.forge"
|
|
281
|
+
chmod 700 "$FORGE_ROOT/.forge"
|
|
282
|
+
|
|
283
|
+
# SECURITY: Use flock for atomic lock acquisition to prevent TOCTOU race
|
|
284
|
+
# This prevents multiple daemon instances from starting simultaneously
|
|
285
|
+
local lock_fd=200
|
|
286
|
+
local startup_lock="$FORGE_ROOT/.forge/startup.lock"
|
|
287
|
+
|
|
288
|
+
# Try to acquire exclusive lock (non-blocking)
|
|
289
|
+
if command -v flock &>/dev/null; then
|
|
290
|
+
# flock available (Linux, some Git Bash installations)
|
|
291
|
+
exec 200>"$startup_lock"
|
|
292
|
+
if ! flock -n 200; then
|
|
293
|
+
echo "Another daemon startup is in progress"
|
|
294
|
+
return 1
|
|
295
|
+
fi
|
|
296
|
+
# Lock acquired - will be released when subshell exits or fd closes
|
|
297
|
+
fi
|
|
298
|
+
# If flock not available, fall back to PID-based check (less secure but functional)
|
|
299
|
+
|
|
300
|
+
# Check if already running
|
|
301
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
302
|
+
local pid
|
|
303
|
+
pid=$(cat "$PID_FILE")
|
|
304
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
305
|
+
echo "Daemon already running (PID: $pid)"
|
|
306
|
+
return 0
|
|
307
|
+
else
|
|
308
|
+
# Stale PID file
|
|
309
|
+
rm -f "$PID_FILE"
|
|
310
|
+
fi
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
# Check for lock file (another instance check - defense in depth)
|
|
314
|
+
if [[ -f "$LOCK_FILE" ]]; then
|
|
315
|
+
local lock_pid
|
|
316
|
+
lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
|
|
317
|
+
if kill -0 "$lock_pid" 2>/dev/null; then
|
|
318
|
+
echo "Another daemon instance is running (PID: $lock_pid)"
|
|
319
|
+
return 1
|
|
320
|
+
else
|
|
321
|
+
rm -f "$LOCK_FILE"
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
mkdir -p "$FORGE_ROOT/$TASKS_PENDING"
|
|
326
|
+
mkdir -p "$FORGE_ROOT/$TASKS_IN_PROGRESS"
|
|
327
|
+
mkdir -p "$FORGE_ROOT/$TASKS_COMPLETED"
|
|
328
|
+
mkdir -p "$FORGE_ROOT/$TASKS_REVIEW"
|
|
329
|
+
mkdir -p "$FORGE_ROOT/$TASKS_APPROVED"
|
|
330
|
+
mkdir -p "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"
|
|
331
|
+
mkdir -p "$FORGE_ROOT/$TASKS_MERGED"
|
|
332
|
+
mkdir -p "$FORGE_ROOT/$TASKS_ATTENTION"
|
|
333
|
+
mkdir -p "$FORGE_ROOT/$AGENT_STATUS_DIR"
|
|
334
|
+
|
|
335
|
+
# Start daemon under watchdog (auto-restarts on unexpected exit)
|
|
336
|
+
watchdog_loop &
|
|
337
|
+
local pid=$!
|
|
338
|
+
echo "$pid" > "$PID_FILE"
|
|
339
|
+
|
|
340
|
+
log_success "Forge daemon started (PID: $pid)"
|
|
341
|
+
echo " Log: $LOG_FILE"
|
|
342
|
+
|
|
343
|
+
# Auto-launch dashboard if voice or dashboard is enabled in config
|
|
344
|
+
if [[ "$DASHBOARD_VOICE" == "true" ]] || [[ "$DASHBOARD_ENABLED" == "true" ]]; then
|
|
345
|
+
cmd_dashboard_start
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# Note: flock is automatically released when the fd is closed (script exits)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
cmd_stop() {
|
|
352
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
353
|
+
echo "Daemon not running"
|
|
354
|
+
return 0
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
local pid
|
|
358
|
+
pid=$(cat "$PID_FILE")
|
|
359
|
+
|
|
360
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
361
|
+
# Signal the watchdog that this is an intentional stop before killing it
|
|
362
|
+
touch "$FORGE_ROOT/.forge/daemon.stopping"
|
|
363
|
+
kill "$pid"
|
|
364
|
+
rm -f "$PID_FILE" "$LOCK_FILE"
|
|
365
|
+
echo "[$(date -Iseconds)] Forge daemon stopped" >> "$LOG_FILE"
|
|
366
|
+
log_success "Daemon stopped"
|
|
367
|
+
else
|
|
368
|
+
rm -f "$PID_FILE" "$LOCK_FILE"
|
|
369
|
+
echo "Daemon was not running (stale PID file removed)"
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
# Stop dashboard if running
|
|
373
|
+
cmd_dashboard_stop
|
|
374
|
+
|
|
375
|
+
# Update state to show inactive
|
|
376
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
377
|
+
# Use cross-platform sed helper
|
|
378
|
+
sed_inplace 's/status: active/status: stopped/' "$STATE_FILE" 2>/dev/null || true
|
|
379
|
+
fi
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
cmd_status() {
|
|
383
|
+
echo ""
|
|
384
|
+
log_header "🔥 Forge Daemon Status"
|
|
385
|
+
|
|
386
|
+
display_daemon_status
|
|
387
|
+
echo ""
|
|
388
|
+
|
|
389
|
+
display_task_counts
|
|
390
|
+
echo ""
|
|
391
|
+
|
|
392
|
+
display_attention_needed
|
|
393
|
+
display_worker_status
|
|
394
|
+
display_recent_notifications
|
|
395
|
+
|
|
396
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
cmd_notifications() {
|
|
400
|
+
echo ""
|
|
401
|
+
log_header "🔔 Forge Notifications"
|
|
402
|
+
|
|
403
|
+
if [[ -f "$NOTIFY_FILE" ]]; then
|
|
404
|
+
local count="${1:-10}"
|
|
405
|
+
echo "Last $count notifications:"
|
|
406
|
+
echo ""
|
|
407
|
+
tail -"$count" "$NOTIFY_FILE"
|
|
408
|
+
else
|
|
409
|
+
echo "No notifications yet."
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
echo ""
|
|
413
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
cmd_clear_notifications() {
|
|
417
|
+
rm -f "$NOTIFY_FILE" "$NOTIFIED_FILE"
|
|
418
|
+
log_success "Notifications cleared"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# =============================================================================
|
|
422
|
+
# Main
|
|
423
|
+
# =============================================================================
|
|
424
|
+
|
|
425
|
+
main() {
|
|
426
|
+
local command="${1:-status}"
|
|
427
|
+
|
|
428
|
+
case "$command" in
|
|
429
|
+
"start")
|
|
430
|
+
cmd_start
|
|
431
|
+
;;
|
|
432
|
+
"stop")
|
|
433
|
+
cmd_stop
|
|
434
|
+
;;
|
|
435
|
+
"status")
|
|
436
|
+
cmd_status
|
|
437
|
+
;;
|
|
438
|
+
"notifications"|"notify")
|
|
439
|
+
shift
|
|
440
|
+
cmd_notifications "$@"
|
|
441
|
+
;;
|
|
442
|
+
"clear")
|
|
443
|
+
cmd_clear_notifications
|
|
444
|
+
;;
|
|
445
|
+
"dashboard")
|
|
446
|
+
shift
|
|
447
|
+
case "${1:-start}" in
|
|
448
|
+
"start") cmd_dashboard_start ;;
|
|
449
|
+
"stop") cmd_dashboard_stop ;;
|
|
450
|
+
*)
|
|
451
|
+
echo "Usage: forge-daemon.sh dashboard [start|stop]"
|
|
452
|
+
exit $EXIT_INVALID_ARGUMENT
|
|
453
|
+
;;
|
|
454
|
+
esac
|
|
455
|
+
;;
|
|
456
|
+
*)
|
|
457
|
+
echo "Usage: forge-daemon.sh [start|stop|status|notifications|clear|dashboard]"
|
|
458
|
+
exit $EXIT_INVALID_ARGUMENT
|
|
459
|
+
;;
|
|
460
|
+
esac
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
main "$@"
|