@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.
Files changed (201) hide show
  1. package/.claude/commands/clear-attention.md +63 -0
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +171 -0
  5. package/.claude/commands/need-help.md +77 -0
  6. package/.claude/commands/update-status.md +64 -0
  7. package/.claude/commands/worker-loop.md +106 -0
  8. package/.claude/hooks/worker-loop.js +198 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -0
  10. package/.claude/settings.local.json +46 -0
  11. package/LICENSE +21 -0
  12. package/README.md +238 -0
  13. package/agents/aegis/personality.md +294 -0
  14. package/agents/anvil/personality.md +276 -0
  15. package/agents/architect/personality.md +258 -0
  16. package/agents/crucible/personality.md +360 -0
  17. package/agents/ember/personality.md +291 -0
  18. package/agents/forge-master/capabilities.md +144 -0
  19. package/agents/forge-master/context-template.md +128 -0
  20. package/agents/forge-master/personality.md +138 -0
  21. package/agents/furnace/personality.md +340 -0
  22. package/agents/herald/personality.md +247 -0
  23. package/agents/loki/personality.md +108 -0
  24. package/agents/oracle/personality.md +283 -0
  25. package/agents/pixel/personality.md +113 -0
  26. package/agents/planning-hub/personality.md +320 -0
  27. package/agents/scribe/personality.md +251 -0
  28. package/agents/temper/personality.md +218 -0
  29. package/bin/cli.js +375 -0
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +483 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/frontend/index.html +13 -0
  34. package/bin/dashboard/frontend/package.json +16 -0
  35. package/bin/dashboard/frontend/src/App.svelte +222 -0
  36. package/bin/dashboard/frontend/src/app.css +1777 -0
  37. package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
  38. package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
  39. package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
  40. package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
  41. package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
  42. package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
  43. package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
  44. package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
  45. package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
  46. package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
  47. package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
  48. package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
  49. package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
  50. package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
  51. package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
  52. package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
  53. package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
  54. package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
  55. package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
  56. package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
  57. package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
  58. package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
  59. package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
  60. package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
  61. package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
  62. package/bin/dashboard/frontend/src/main.js +9 -0
  63. package/bin/dashboard/frontend/svelte.config.js +5 -0
  64. package/bin/dashboard/frontend/vite.config.js +20 -0
  65. package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
  66. package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
  67. package/bin/dashboard/public/index.html +14 -0
  68. package/bin/dashboard/server.js +566 -0
  69. package/bin/forge-daemon.sh +463 -0
  70. package/bin/forge-setup.sh +645 -0
  71. package/bin/forge-spawn.sh +164 -0
  72. package/bin/forge.cmd +83 -0
  73. package/bin/forge.sh +533 -0
  74. package/bin/lib/agents.sh +177 -0
  75. package/bin/lib/colors.sh +44 -0
  76. package/bin/lib/config.sh +347 -0
  77. package/bin/lib/constants.sh +241 -0
  78. package/bin/lib/daemon/display.sh +128 -0
  79. package/bin/lib/daemon/notifications.sh +263 -0
  80. package/bin/lib/daemon/routing.sh +77 -0
  81. package/bin/lib/daemon/state.sh +115 -0
  82. package/bin/lib/daemon/sync.sh +95 -0
  83. package/bin/lib/database.sh +310 -0
  84. package/bin/lib/heimdall-setup.js +113 -0
  85. package/bin/lib/heimdall.js +265 -0
  86. package/bin/lib/json.sh +264 -0
  87. package/bin/lib/terminal.js +451 -0
  88. package/bin/lib/util.sh +126 -0
  89. package/bin/lib/vcs.js +349 -0
  90. package/config/agent-manifest.yaml +203 -0
  91. package/config/agents.json +168 -0
  92. package/config/task-template.md +159 -0
  93. package/config/task-types.yaml +106 -0
  94. package/context/agent-status/aegis.json +7 -0
  95. package/context/agent-status/anvil.json +7 -0
  96. package/context/agent-status/architect.json +7 -0
  97. package/context/agent-status/crucible.json +7 -0
  98. package/context/agent-status/ember.json +7 -0
  99. package/context/agent-status/furnace.json +7 -0
  100. package/context/agent-status/loki.json +7 -0
  101. package/context/agent-status/oracle.json +7 -0
  102. package/context/agent-status/pixel.json +7 -0
  103. package/context/agent-status/planning-hub.json +7 -0
  104. package/context/agent-status/scribe.json +7 -0
  105. package/context/agent-status/temper.json +7 -0
  106. package/context/feature-brainstorm.md +426 -0
  107. package/context/forge-state.yaml +19 -0
  108. package/context/modern-conventions.md +129 -0
  109. package/context/project-context-template.md +122 -0
  110. package/context/project-context.md +122 -0
  111. package/docs/TODO.md +150 -0
  112. package/docs/agents.md +409 -0
  113. package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
  114. package/docs/architecture/vibe-lab-integration.md +684 -0
  115. package/docs/architecture.md +194 -0
  116. package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
  117. package/docs/cleanup-workflow.md +329 -0
  118. package/docs/commands.md +451 -0
  119. package/docs/dashboard-mockup.html +989 -0
  120. package/docs/getting-started.md +261 -0
  121. package/docs/integration/forge-ownership-policy.md +112 -0
  122. package/docs/npm-publishing.md +132 -0
  123. package/docs/roadmap-2026.md +519 -0
  124. package/docs/security.md +144 -0
  125. package/docs/wireframes/dashboard-mvp.md +1164 -0
  126. package/docs/workflows/README.md +32 -0
  127. package/docs/workflows/azure-devops.md +108 -0
  128. package/docs/workflows/bitbucket.md +104 -0
  129. package/docs/workflows/git-only.md +130 -0
  130. package/docs/workflows/gitea.md +168 -0
  131. package/docs/workflows/github.md +103 -0
  132. package/docs/workflows/gitlab.md +105 -0
  133. package/docs/workflows.md +454 -0
  134. package/package.json +73 -0
  135. package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
  136. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
  137. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
  138. package/tasks/completed/ARCH-009-test-organization.md +78 -0
  139. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
  140. package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
  141. package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
  142. package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
  143. package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
  144. package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
  145. package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
  146. package/tasks/completed/CLEAN-001.md +38 -0
  147. package/tasks/completed/CLEAN-002.md +43 -0
  148. package/tasks/completed/CLEAN-003.md +47 -0
  149. package/tasks/completed/CLEAN-004.md +56 -0
  150. package/tasks/completed/CLEAN-005.md +75 -0
  151. package/tasks/completed/CLEAN-006.md +47 -0
  152. package/tasks/completed/CLEAN-007.md +34 -0
  153. package/tasks/completed/CLEAN-008.md +49 -0
  154. package/tasks/completed/CLEAN-012.md +58 -0
  155. package/tasks/completed/CLEAN-013.md +45 -0
  156. package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
  157. package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
  158. package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
  159. package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
  160. package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
  161. package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
  162. package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
  163. package/tasks/completed/PLAT-1-heimdall.md +420 -0
  164. package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
  165. package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
  166. package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
  167. package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
  168. package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
  169. package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
  170. package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
  171. package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
  172. package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
  173. package/tasks/completed/review-bmad-aegis.md +89 -0
  174. package/tasks/completed/review-bmad-anvil.md +80 -0
  175. package/tasks/completed/review-bmad-crucible.md +81 -0
  176. package/tasks/completed/review-bmad-ember.md +90 -0
  177. package/tasks/completed/review-bmad-furnace.md +79 -0
  178. package/tasks/completed/review-bmad-pixel.md +82 -0
  179. package/tasks/completed/review-bmad-scribe.md +92 -0
  180. package/tasks/completed/review-bmad-sentinel.md +83 -0
  181. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
  182. package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
  183. package/tasks/pending/ARCH-006-task-template-location.md +64 -0
  184. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
  185. package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
  186. package/tasks/pending/CLEAN-009.md +31 -0
  187. package/tasks/pending/CLEAN-010.md +30 -0
  188. package/tasks/pending/CLEAN-011.md +30 -0
  189. package/tasks/pending/CLEAN-014.md +32 -0
  190. package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
  191. package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
  192. package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
  193. package/tasks/review/bmad-review-aegis.md +349 -0
  194. package/tasks/review/bmad-review-anvil.md +259 -0
  195. package/tasks/review/bmad-review-crucible.md +277 -0
  196. package/tasks/review/bmad-review-ember.md +307 -0
  197. package/tasks/review/bmad-review-furnace.md +285 -0
  198. package/tasks/review/bmad-review-pixel.md +329 -0
  199. package/tasks/review/bmad-review-scribe.md +361 -0
  200. package/tasks/review/bmad-review-sentinel.md +242 -0
  201. 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 "$@"