@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,128 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/display.sh
4
+ #
5
+ # Status display functions for the daemon status command
6
+ #
7
+ # Dependencies: colors.sh, constants.sh, json.sh
8
+ # Globals required: FORGE_ROOT, PID_FILE, STATE_FILE, NOTIFY_FILE,
9
+ # AGENT_STATUS_DIR, TASKS_ATTENTION, STALE_STATUS_THRESHOLD
10
+
11
+ # Prevent double-sourcing
12
+ [[ -n "${_DAEMON_DISPLAY_LOADED:-}" ]] && return 0
13
+ _DAEMON_DISPLAY_LOADED=1
14
+
15
+ # Display daemon running status (PID check)
16
+ display_daemon_status() {
17
+ if [[ -f "$PID_FILE" ]]; then
18
+ local pid
19
+ pid=$(cat "$PID_FILE")
20
+ if kill -0 "$pid" 2>/dev/null; then
21
+ log_success "Running (PID: $pid)"
22
+ else
23
+ log_warn "Stopped (stale PID file)"
24
+ fi
25
+ else
26
+ echo "Status: Stopped"
27
+ fi
28
+ }
29
+
30
+ # Display task counts from state file
31
+ display_task_counts() {
32
+ if [[ -f "$STATE_FILE" ]]; then
33
+ echo "Task Counts:"
34
+ grep -E "pending:|in_progress:|completed:|in_review:|approved:|needs_changes:|merged:|attention_needed:" "$STATE_FILE" | sed 's/^/ /'
35
+ fi
36
+ }
37
+
38
+ # Display workers needing attention (urgent alerts)
39
+ display_attention_needed() {
40
+ local attention_count
41
+ attention_count=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
42
+ if [[ "$attention_count" -gt 0 ]]; then
43
+ echo -e "${RED}🔔 ATTENTION NEEDED:${NC}"
44
+ for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
45
+ if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
46
+ local agent issue
47
+ agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
48
+ issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | head -c 80)
49
+ echo -e " ${YELLOW}$agent${NC}: $issue"
50
+ fi
51
+ done
52
+ echo ""
53
+ fi
54
+ }
55
+
56
+ # Get status icon for worker status
57
+ get_status_icon() {
58
+ local status="$1"
59
+ case "$status" in
60
+ "working") echo "🔨" ;;
61
+ "idle") echo "💤" ;;
62
+ "blocked") echo "🚫" ;;
63
+ "testing") echo "🧪" ;;
64
+ "reviewing") echo "👁️" ;;
65
+ *) echo "❓" ;;
66
+ esac
67
+ }
68
+
69
+ # Display active worker statuses with staleness indicators
70
+ display_worker_status() {
71
+ if [[ ! -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
72
+ return
73
+ fi
74
+
75
+ local status_count
76
+ status_count=$(find "$FORGE_ROOT/$AGENT_STATUS_DIR" -maxdepth 1 -name "*.json" -type f 2>/dev/null | wc -l)
77
+ if [[ "$status_count" -eq 0 ]]; then
78
+ return
79
+ fi
80
+
81
+ echo "Active Workers:"
82
+ local now_epoch stale_threshold
83
+ now_epoch=$(date +%s)
84
+ stale_threshold=$STALE_STATUS_THRESHOLD
85
+
86
+ for status_file in "$FORGE_ROOT/$AGENT_STATUS_DIR"/*.json; do
87
+ if [[ -f "$status_file" && ! -L "$status_file" ]]; then
88
+ local agent status task updated stale_marker icon
89
+ agent=$(json_read "$status_file" "agent" "unknown")
90
+ status=$(json_read "$status_file" "status" "unknown")
91
+ task=$(json_read "$status_file" "task" "")
92
+ updated=$(json_read "$status_file" "updated" "")
93
+
94
+ # Check staleness
95
+ stale_marker=""
96
+ if [[ -n "$updated" ]]; then
97
+ local updated_epoch age
98
+ 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")
99
+ age=$((now_epoch - updated_epoch))
100
+ if [[ "$age" -gt "$stale_threshold" ]]; then
101
+ stale_marker=" ${YELLOW}(stale)${NC}"
102
+ fi
103
+ fi
104
+
105
+ icon=$(get_status_icon "$status")
106
+
107
+ if [[ -n "$task" ]]; then
108
+ echo -e " $icon ${CYAN}$agent${NC}: $status ($task)$stale_marker"
109
+ else
110
+ echo -e " $icon ${CYAN}$agent${NC}: $status$stale_marker"
111
+ fi
112
+ fi
113
+ done
114
+ echo ""
115
+ }
116
+
117
+ # Display recent notifications from log
118
+ display_recent_notifications() {
119
+ if [[ -f "$NOTIFY_FILE" ]]; then
120
+ local notify_count
121
+ notify_count=$(wc -l < "$NOTIFY_FILE" 2>/dev/null || echo "0")
122
+ if [[ "$notify_count" -gt 0 ]]; then
123
+ echo "Recent Notifications (last 5):"
124
+ tail -5 "$NOTIFY_FILE" | sed 's/^/ /'
125
+ echo ""
126
+ fi
127
+ fi
128
+ }
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/notifications.sh
4
+ #
5
+ # Daemon notification functions - pending task alerts, attention signals,
6
+ # system toasts, and Heimdall escalation handling
7
+ #
8
+ # Dependencies: colors.sh, constants.sh
9
+ # Globals required: FORGE_ROOT, NOTIFY_FILE, NOTIFIED_FILE, LOG_FILE,
10
+ # TASKS_PENDING, TASKS_NEEDS_CHANGES, TASKS_ATTENTION
11
+
12
+ # Prevent double-sourcing
13
+ [[ -n "${_DAEMON_NOTIFICATIONS_LOADED:-}" ]] && return 0
14
+ _DAEMON_NOTIFICATIONS_LOADED=1
15
+
16
+ # SECURITY: Sanitize message for safe use in shell commands
17
+ # Removes/escapes characters that could cause command injection
18
+ sanitize_notification_message() {
19
+ local msg="$1"
20
+ # Remove null bytes
21
+ msg="${msg//$'\0'/}"
22
+ # Remove characters that could escape out of string contexts
23
+ # PowerShell: $, `, ', ", (), {}
24
+ # osascript: ', ", \, $
25
+ # We allow: alphanumeric, spaces, periods, commas, colons, hyphens, underscores
26
+ msg=$(echo "$msg" | tr -cd 'a-zA-Z0-9 .,;:!?_-')
27
+ # Limit length to prevent buffer issues
28
+ msg="${msg:0:200}"
29
+ echo "$msg"
30
+ }
31
+
32
+ notify() {
33
+ local message="$1"
34
+ local urgency="${2:-normal}" # normal or urgent
35
+ local timestamp
36
+ timestamp=$(date -Iseconds)
37
+
38
+ # SECURITY: Sanitize before logging to prevent ANSI escape injection and
39
+ # control-character log poisoning. sanitize_notification_message() strips
40
+ # everything except alphanumeric, spaces, and safe punctuation.
41
+ local safe_message
42
+ safe_message=$(sanitize_notification_message "$message")
43
+
44
+ # Log to notifications file
45
+ echo "[$timestamp] $safe_message" >> "$NOTIFY_FILE"
46
+
47
+ # Log to main log
48
+ echo "[$timestamp] NOTIFY: $safe_message" >> "$LOG_FILE"
49
+
50
+ # Terminal bell (works in most terminals)
51
+ printf '\a'
52
+
53
+ # System toast notification for urgent messages
54
+ if [[ "$urgency" == "urgent" ]]; then
55
+ send_system_notification "$safe_message"
56
+ fi
57
+ }
58
+
59
+ # Send system-level notification (platform-specific)
60
+ send_system_notification() {
61
+ local message="$1"
62
+ local title="Vibe Forge"
63
+
64
+ # SECURITY: Sanitize message to prevent command injection
65
+ message=$(sanitize_notification_message "$message")
66
+
67
+ case "$(uname -s)" in
68
+ MINGW*|MSYS*|CYGWIN*)
69
+ # Windows: Use PowerShell toast notification
70
+ # SECURITY: Message is sanitized above, title is hardcoded
71
+ powershell.exe -NoProfile -Command "
72
+ \$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
73
+ \$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
74
+ \$textNodes = \$template.GetElementsByTagName('text')
75
+ \$textNodes.Item(0).AppendChild(\$template.CreateTextNode('$title')) | Out-Null
76
+ \$textNodes.Item(1).AppendChild(\$template.CreateTextNode('$message')) | Out-Null
77
+ \$toast = [Windows.UI.Notifications.ToastNotification]::new(\$template)
78
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Vibe Forge').Show(\$toast)
79
+ " 2>/dev/null &
80
+ ;;
81
+ Darwin)
82
+ # macOS: Use osascript
83
+ # SECURITY: Message is sanitized above
84
+ osascript -e "display notification \"$message\" with title \"$title\"" 2>/dev/null &
85
+ ;;
86
+ Linux)
87
+ # Linux: Use notify-send if available
88
+ # SECURITY: notify-send handles escaping, but message is sanitized anyway
89
+ if command -v notify-send &>/dev/null; then
90
+ notify-send "$title" "$message" 2>/dev/null &
91
+ fi
92
+ ;;
93
+ esac
94
+ }
95
+
96
+ check_new_pending_tasks() {
97
+ # Create notified file if it doesn't exist
98
+ touch "$NOTIFIED_FILE"
99
+
100
+ # Check for new pending tasks
101
+ for task in "$FORGE_ROOT/$TASKS_PENDING"/*.md; do
102
+ if [[ -f "$task" && ! -L "$task" ]]; then
103
+ local filename
104
+ filename=$(basename "$task")
105
+
106
+ # Check if we've already notified about this task
107
+ if ! grep -qF "$filename" "$NOTIFIED_FILE" 2>/dev/null; then
108
+ # Extract task info from frontmatter safely
109
+ local task_id task_title assigned_to
110
+
111
+ # Use head to limit read, tr to sanitize, and strip ANSI escape sequences
112
+ 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)
113
+ 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)
114
+ 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)
115
+
116
+ # Use filename as fallback
117
+ task_id="${task_id:-$filename}"
118
+ task_title="${task_title:-New task}"
119
+
120
+ # Notify
121
+ if [[ -n "$assigned_to" ]]; then
122
+ notify "New task for $assigned_to: $task_title ($task_id)"
123
+ else
124
+ notify "New pending task: $task_title ($task_id)"
125
+ fi
126
+
127
+ # Mark as notified (atomic append)
128
+ echo "$filename" >> "$NOTIFIED_FILE"
129
+ fi
130
+ fi
131
+ done
132
+
133
+ # Also check needs-changes for tasks that need rework
134
+ for task in "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"/*.md; do
135
+ if [[ -f "$task" && ! -L "$task" ]]; then
136
+ local filename
137
+ filename=$(basename "$task")
138
+ local notified_key="needs-changes:$filename"
139
+
140
+ if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
141
+ local task_id assigned_to
142
+ task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 100)
143
+ assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
144
+
145
+ task_id="${task_id:-$filename}"
146
+
147
+ if [[ -n "$assigned_to" ]]; then
148
+ notify "Task needs changes ($assigned_to): $task_id"
149
+ else
150
+ notify "Task needs changes: $task_id"
151
+ fi
152
+
153
+ echo "$notified_key" >> "$NOTIFIED_FILE"
154
+ fi
155
+ fi
156
+ done
157
+ }
158
+
159
+ check_attention_needed() {
160
+ # Check for workers needing attention (urgent notifications)
161
+ if [[ ! -d "$FORGE_ROOT/$TASKS_ATTENTION" ]]; then
162
+ return 0
163
+ fi
164
+
165
+ for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
166
+ if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
167
+ local filename
168
+ filename=$(basename "$attention_file")
169
+ local notified_key="attention:$filename"
170
+
171
+ if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
172
+ # Extract attention info
173
+ local agent issue
174
+ agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
175
+ issue=$(grep -m1 "^##" "$attention_file" 2>/dev/null | sed 's/^## *//' | head -c 200)
176
+
177
+ agent="${agent:-Unknown}"
178
+ issue="${issue:-Needs attention}"
179
+
180
+ # Ring bell multiple times for attention
181
+ printf '\a\a\a'
182
+
183
+ # Send urgent notification with toast
184
+ notify "🔔 $agent needs help: $issue" "urgent"
185
+
186
+ echo "$notified_key" >> "$NOTIFIED_FILE"
187
+ fi
188
+ fi
189
+ done
190
+ }
191
+
192
+ # check_heimdall_escalations INBOX_DIR
193
+ # Scans the lab worker inbox for .escalation signal files written by Heimdall
194
+ # when a worker accumulates too many policy violations (Gjallarhorn sounded).
195
+ # Writes an escalation handoff for lab sentinel to route to human review.
196
+ check_heimdall_escalations() {
197
+ local inbox_dir="$1"
198
+
199
+ [[ -d "$inbox_dir" ]] || return 0
200
+
201
+ # shopt -s globstar needed for **; fall back to find for compatibility
202
+ local escalation_files
203
+ mapfile -t escalation_files < <(find "$inbox_dir" -name "*.escalation" -type f 2>/dev/null)
204
+
205
+ for escalation_file in "${escalation_files[@]}"; do
206
+ [[ -f "$escalation_file" ]] || continue
207
+
208
+ local story_id agent_dir agent handoff_dir
209
+ story_id=$(basename "$escalation_file" .escalation)
210
+ agent_dir=$(dirname "$escalation_file")
211
+ agent=$(basename "$agent_dir")
212
+
213
+ # Read escalation JSON for violation details
214
+ local violations last_reason
215
+ violations=$(node -e "try{const d=JSON.parse(require('fs').readFileSync('$escalation_file','utf8'));console.log(d.violations||'?')}catch(e){console.log('?')}" 2>/dev/null)
216
+ last_reason=$(node -e "try{const d=JSON.parse(require('fs').readFileSync('$escalation_file','utf8'));console.log(d.last_reason||'')}catch(e){console.log('')}" 2>/dev/null)
217
+
218
+ echo "[$(date -Iseconds)] HEIMDALL SOUNDED: $agent/$story_id ($violations violations) -- writing escalation handoff" >> "$LOG_FILE"
219
+
220
+ # Write escalation handoff for lab sentinel
221
+ # Lab sentinel routes escalations to human review on next cycle
222
+ local handoff_dir="$FORGE_ROOT/_vibe-chain-output/handoffs"
223
+ if [[ -d "$handoff_dir" ]]; then
224
+ local handoff_file="$handoff_dir/${story_id}-worker-escalation.md"
225
+ cat > "$handoff_file" << HANDOFF
226
+ ---
227
+ type: worker-escalation
228
+ story: "$story_id"
229
+ from: forge-daemon
230
+ to: human
231
+ created: $(date -Iseconds)
232
+ status: pending
233
+ agent: "$agent"
234
+ violations: $violations
235
+ ---
236
+
237
+ ## Heimdall Escalation
238
+
239
+ Worker \`$agent\` accumulated $violations policy violations on story \`$story_id\`.
240
+ The worker has been halted for this story. Human review required before resubmitting.
241
+
242
+ ## Last Violation
243
+
244
+ $last_reason
245
+
246
+ ## Audit Log
247
+
248
+ Full violation history: \`_vibe-chain-output/heimdall-audit.log\`
249
+
250
+ ## Action Required
251
+
252
+ Review the audit log and decide:
253
+ - Fix the story spec and resubmit
254
+ - Manually complete the work
255
+ - Cancel the story
256
+ HANDOFF
257
+ echo "[$(date -Iseconds)] Escalation handoff written: $handoff_file" >> "$LOG_FILE"
258
+ fi
259
+
260
+ # Mark escalation as processed so it is not re-processed next cycle
261
+ mv "$escalation_file" "${escalation_file}.processed" 2>/dev/null || true
262
+ done
263
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/routing.sh
4
+ #
5
+ # Task routing functions - moves tasks between state folders
6
+ #
7
+ # Dependencies: constants.sh, util.sh (log_*)
8
+ # Globals required: FORGE_ROOT, LOG_FILE,
9
+ # TASKS_COMPLETED, TASKS_REVIEW, TASKS_APPROVED, TASKS_MERGED
10
+
11
+ # Prevent double-sourcing
12
+ [[ -n "${_DAEMON_ROUTING_LOADED:-}" ]] && return 0
13
+ _DAEMON_ROUTING_LOADED=1
14
+
15
+ # Safe file move with symlink protection
16
+ safe_move_task() {
17
+ local src="$1"
18
+ local dest_dir="$2"
19
+
20
+ # SECURITY: Skip symlinks to prevent symlink attacks
21
+ if [[ -L "$src" ]]; then
22
+ echo "[$(date -Iseconds)] WARNING: Skipping symlink: $src" >> "$LOG_FILE"
23
+ return 1
24
+ fi
25
+
26
+ # SECURITY: Verify source is a regular file
27
+ if [[ ! -f "$src" ]]; then
28
+ return 1
29
+ fi
30
+
31
+ # SECURITY: Verify destination is within FORGE_ROOT
32
+ local real_dest
33
+ real_dest=$(cd "$dest_dir" 2>/dev/null && pwd)
34
+ local forge_root_real
35
+ forge_root_real=$(cd "$FORGE_ROOT" 2>/dev/null && pwd)
36
+
37
+ if [[ "$real_dest" != "$forge_root_real"/* ]]; then
38
+ echo "[$(date -Iseconds)] ERROR: Destination outside FORGE_ROOT: $dest_dir" >> "$LOG_FILE"
39
+ return 1
40
+ fi
41
+
42
+ local filename
43
+ filename=$(basename "$src")
44
+
45
+ # SECURITY: TOCTOU note - there is a theoretical race between the symlink
46
+ # check above and this mv. A local attacker with precise timing could swap
47
+ # the file for a symlink in that window. This risk is accepted because:
48
+ # 1. Exploiting it requires local filesystem write access (already privileged)
49
+ # 2. The destination boundary check above limits the blast radius
50
+ # 3. A fully atomic check+move in bash would require platform-specific tools
51
+ # Mitigation: the destination is always within FORGE_ROOT (verified above).
52
+ mv "$src" "$dest_dir/$filename"
53
+ }
54
+
55
+ route_completed_to_review() {
56
+ # Move completed tasks to review queue
57
+ for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
58
+ if [[ -f "$task" && ! -L "$task" ]]; then
59
+ local filename
60
+ filename=$(basename "$task")
61
+ echo "[$(date -Iseconds)] Routing $filename to review" >> "$LOG_FILE"
62
+ safe_move_task "$task" "$FORGE_ROOT/$TASKS_REVIEW"
63
+ fi
64
+ done
65
+ }
66
+
67
+ route_approved_to_merged() {
68
+ # Move approved tasks to merged archive
69
+ for task in "$FORGE_ROOT/$TASKS_APPROVED"/*.md; do
70
+ if [[ -f "$task" && ! -L "$task" ]]; then
71
+ local filename
72
+ filename=$(basename "$task")
73
+ echo "[$(date -Iseconds)] Archiving $filename to merged" >> "$LOG_FILE"
74
+ safe_move_task "$task" "$FORGE_ROOT/$TASKS_MERGED"
75
+ fi
76
+ done
77
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/state.sh
4
+ #
5
+ # Daemon state management - forge-state.yaml updates, attention tracking,
6
+ # adaptive polling
7
+ #
8
+ # Dependencies: database.sh, constants.sh
9
+ # Requires: sync.sh (for build_worker_status)
10
+ # Globals required: FORGE_ROOT, STATE_FILE, AGENT_STATUS_DIR,
11
+ # TASKS_PENDING, TASKS_IN_PROGRESS, TASKS_COMPLETED,
12
+ # TASKS_REVIEW, TASKS_APPROVED, TASKS_NEEDS_CHANGES,
13
+ # TASKS_MERGED, TASKS_ATTENTION
14
+
15
+ # Prevent double-sourcing
16
+ [[ -n "${_DAEMON_STATE_LOADED:-}" ]] && return 0
17
+ _DAEMON_STATE_LOADED=1
18
+
19
+ update_state() {
20
+ # Count tasks in each folder (using find with -maxdepth for safety)
21
+ local pending in_progress completed review approved needs_changes merged attention
22
+ pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
23
+ in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
24
+ completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
25
+ review=$(find "$FORGE_ROOT/$TASKS_REVIEW" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
26
+ approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
27
+ needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
28
+ merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
29
+ attention=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
30
+
31
+ local blocked=0
32
+
33
+ # Build attention details if any workers need help
34
+ local attention_details=""
35
+ if [[ "$attention" -gt 0 ]]; then
36
+ attention_details=$(build_attention_details)
37
+ fi
38
+
39
+ # Build worker status from agent-status files
40
+ local worker_status=""
41
+ if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
42
+ worker_status=$(build_worker_status)
43
+ fi
44
+
45
+ # Write state file atomically (write to temp, then move)
46
+ local temp_state="${STATE_FILE}.tmp.$$"
47
+ cat > "$temp_state" << EOF
48
+ # Vibe Forge State
49
+ # Auto-updated by forge-daemon
50
+ # Last updated: $(date -Iseconds)
51
+
52
+ forge:
53
+ status: active
54
+ daemon_pid: $$
55
+
56
+ tasks:
57
+ pending: $pending
58
+ in_progress: $in_progress
59
+ completed: $completed
60
+ in_review: $review
61
+ approved: $approved
62
+ needs_changes: $needs_changes
63
+ merged: $merged
64
+ blocked: $blocked
65
+ attention_needed: $attention
66
+
67
+ $attention_details
68
+ $worker_status
69
+ last_updated: $(date -Iseconds)
70
+ EOF
71
+ mv "$temp_state" "$STATE_FILE"
72
+ }
73
+
74
+ build_attention_details() {
75
+ echo "attention:"
76
+ for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
77
+ if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
78
+ local agent created issue
79
+ agent=$(grep -m1 "^agent:" "$attention_file" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
80
+ created=$(grep -m1 "^created:" "$attention_file" 2>/dev/null | cut -d':' -f2- | tr -d ' ' | head -c 30)
81
+ # Get the issue line (first ## heading content or fallback)
82
+ issue=$(sed -n '/^## Issue/,/^##/p' "$attention_file" 2>/dev/null | grep -v "^##" | head -1 | tr -d '\n' | head -c 100)
83
+ issue="${issue:-Needs attention}"
84
+
85
+ echo " - agent: $agent"
86
+ echo " since: $created"
87
+ echo " issue: \"$issue\""
88
+ fi
89
+ done
90
+ }
91
+
92
+ # Determine daemon state based on activity (for adaptive polling)
93
+ determine_daemon_state() {
94
+ # Check if there are in-progress tasks
95
+ local in_progress_count
96
+ in_progress_count=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
97
+
98
+ # Check if there are active workers
99
+ local active_workers
100
+ active_workers=$(db_count_active_workers 2>/dev/null || echo "0")
101
+
102
+ if [[ "$in_progress_count" -gt 0 ]] || [[ "$active_workers" -gt 0 ]]; then
103
+ echo "active"
104
+ else
105
+ echo "idle"
106
+ fi
107
+ }
108
+
109
+ # Get current poll interval in seconds (from DB, with fallback)
110
+ get_poll_interval() {
111
+ local interval_ms
112
+ interval_ms=$(db_get_poll_interval_ms 2>/dev/null || echo "30000")
113
+ # Convert ms to seconds (bash integer division)
114
+ echo $((interval_ms / 1000))
115
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/sync.sh
4
+ #
5
+ # Agent status synchronization functions - JSON files to SQLite
6
+ #
7
+ # Dependencies: json.sh, database.sh, constants.sh
8
+ # Globals required: FORGE_ROOT, FORGE_DB, AGENT_STATUS_DIR, LOG_FILE,
9
+ # STALE_STATUS_THRESHOLD
10
+
11
+ # Prevent double-sourcing
12
+ [[ -n "${_DAEMON_SYNC_LOADED:-}" ]] && return 0
13
+ _DAEMON_SYNC_LOADED=1
14
+
15
+ # Sync agent status from JSON files to SQLite (with mtime filtering)
16
+ sync_agent_status_to_db() {
17
+ local status_dir="$FORGE_ROOT/$AGENT_STATUS_DIR"
18
+
19
+ if [[ ! -d "$status_dir" ]]; then
20
+ return 0
21
+ fi
22
+
23
+ for status_file in "$status_dir"/*.json; do
24
+ if [[ -f "$status_file" && ! -L "$status_file" ]]; then
25
+ # Get file modification time
26
+ local file_mtime
27
+ file_mtime=$(stat -c %Y "$status_file" 2>/dev/null || stat -f %m "$status_file" 2>/dev/null || echo "0")
28
+
29
+ # Get agent name from filename
30
+ local agent_name
31
+ agent_name=$(basename "$status_file" .json)
32
+
33
+ # Check if file has changed since last read
34
+ local stored_mtime
35
+ stored_mtime=$(db_get_agent_mtime "$agent_name")
36
+
37
+ if [[ "$file_mtime" -gt "$stored_mtime" ]]; then
38
+ # File changed - parse and update DB
39
+ local agent status task message updated
40
+ agent=$(json_read "$status_file" "agent" "unknown")
41
+ status=$(json_read "$status_file" "status" "unknown")
42
+ task=$(json_read "$status_file" "task" "")
43
+ message=$(json_read "$status_file" "message" "" | head -c 80)
44
+ updated=$(json_read "$status_file" "updated" "")
45
+
46
+ # Upsert to database
47
+ db_upsert_agent_status "$agent" "$status" "$task" "$message" "$updated" "$file_mtime"
48
+
49
+ echo "[$(date -Iseconds)] Synced status for $agent: $status" >> "$LOG_FILE"
50
+ fi
51
+ fi
52
+ done
53
+ }
54
+
55
+ # Build worker status from SQLite (for YAML output)
56
+ build_worker_status() {
57
+ local now_epoch
58
+ now_epoch=$(date +%s)
59
+ local stale_threshold=$STALE_STATUS_THRESHOLD
60
+
61
+ # Check if we have any agent status in DB
62
+ local agent_count
63
+ agent_count=$(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;" 2>/dev/null || echo "0")
64
+
65
+ if [[ "$agent_count" -eq 0 ]]; then
66
+ return 0
67
+ fi
68
+
69
+ echo "workers:"
70
+
71
+ # Read from database
72
+ while IFS='|' read -r agent status task message updated; do
73
+ local stale_marker=""
74
+
75
+ # Check if stale
76
+ if [[ -n "$updated" ]]; then
77
+ local updated_epoch age
78
+ 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")
79
+ age=$((now_epoch - updated_epoch))
80
+ if [[ "$age" -gt "$stale_threshold" ]]; then
81
+ stale_marker=" (stale)"
82
+ fi
83
+ fi
84
+
85
+ echo " - agent: $agent"
86
+ echo " status: $status$stale_marker"
87
+ if [[ -n "$task" ]]; then
88
+ echo " task: $task"
89
+ fi
90
+ if [[ -n "$message" ]]; then
91
+ echo " message: \"$message\""
92
+ fi
93
+ echo " updated: $updated"
94
+ done < <(db_get_all_agent_statuses)
95
+ }