vibe-forge 0.4.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/.claude/commands/clear-attention.md +63 -63
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -102
  4. package/.claude/commands/forge.md +218 -171
  5. package/.claude/commands/need-help.md +77 -77
  6. package/.claude/commands/update-status.md +64 -64
  7. package/.claude/commands/worker-loop.md +106 -106
  8. package/.claude/hooks/worker-loop.js +217 -187
  9. package/.claude/scripts/setup-worker-loop.sh +45 -45
  10. package/.claude/settings.json +89 -0
  11. package/LICENSE +21 -21
  12. package/README.md +253 -232
  13. package/agents/aegis/personality.md +303 -269
  14. package/agents/anvil/personality.md +278 -240
  15. package/agents/architect/personality.md +260 -234
  16. package/agents/crucible/personality.md +362 -309
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +293 -265
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +342 -291
  21. package/agents/herald/personality.md +249 -247
  22. package/agents/loki/personality.md +108 -0
  23. package/agents/oracle/personality.md +284 -0
  24. package/agents/pixel/personality.md +140 -0
  25. package/agents/planning-hub/personality.md +473 -251
  26. package/agents/scribe/personality.md +253 -251
  27. package/agents/slag/personality.md +268 -0
  28. package/agents/temper/personality.md +270 -0
  29. package/bin/cli.js +372 -325
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +507 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
  34. package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
  35. package/bin/dashboard/public/index.html +14 -0
  36. package/bin/dashboard/server.js +645 -0
  37. package/bin/forge-daemon.sh +477 -851
  38. package/bin/forge-setup.sh +661 -645
  39. package/bin/forge-spawn.sh +164 -164
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +566 -387
  42. package/bin/lib/agents.sh +177 -177
  43. package/bin/lib/check-aliases.js +50 -0
  44. package/bin/lib/colors.sh +44 -44
  45. package/bin/lib/config.sh +347 -313
  46. package/bin/lib/constants.sh +241 -206
  47. package/bin/lib/daemon/budgets.sh +107 -0
  48. package/bin/lib/daemon/dependencies.sh +146 -0
  49. package/bin/lib/daemon/display.sh +128 -0
  50. package/bin/lib/daemon/notifications.sh +273 -0
  51. package/bin/lib/daemon/routing.sh +93 -0
  52. package/bin/lib/daemon/state.sh +163 -0
  53. package/bin/lib/daemon/sync.sh +103 -0
  54. package/bin/lib/database.sh +357 -305
  55. package/bin/lib/frontmatter.js +106 -0
  56. package/bin/lib/heimdall-setup.js +113 -0
  57. package/bin/lib/heimdall.js +265 -0
  58. package/bin/lib/json.sh +264 -258
  59. package/bin/lib/terminal.js +452 -446
  60. package/bin/lib/util.sh +126 -126
  61. package/bin/lib/vcs.js +349 -349
  62. package/config/agent-manifest.yaml +237 -243
  63. package/config/agents.json +207 -132
  64. package/config/task-template.md +159 -87
  65. package/config/task-types.yaml +111 -106
  66. package/config/templates/handoff-template.md +40 -0
  67. package/context/agent-overrides/README.md +41 -0
  68. package/context/architecture.md +42 -0
  69. package/context/modern-conventions.md +129 -129
  70. package/context/project-context-template.md +122 -122
  71. package/docs/agents.md +473 -409
  72. package/docs/architecture.md +194 -162
  73. package/docs/commands.md +451 -388
  74. package/docs/security.md +195 -144
  75. package/package.json +77 -50
  76. package/.claude/settings.local.json +0 -33
  77. package/agents/forge-master/capabilities.md +0 -144
  78. package/agents/forge-master/context-template.md +0 -128
  79. package/agents/forge-master/personality.md +0 -138
  80. package/agents/sentinel/personality.md +0 -194
  81. package/context/forge-state.yaml +0 -19
  82. package/docs/TODO.md +0 -150
  83. package/docs/getting-started.md +0 -243
  84. package/docs/npm-publishing.md +0 -95
  85. package/docs/workflows/README.md +0 -32
  86. package/docs/workflows/azure-devops.md +0 -108
  87. package/docs/workflows/bitbucket.md +0 -104
  88. package/docs/workflows/git-only.md +0 -130
  89. package/docs/workflows/gitea.md +0 -168
  90. package/docs/workflows/github.md +0 -103
  91. package/docs/workflows/gitlab.md +0 -105
  92. package/docs/workflows.md +0 -454
  93. package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
  94. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
  95. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
  96. package/tasks/completed/ARCH-009-test-organization.md +0 -78
  97. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
  98. package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
  99. package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
  100. package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
  101. package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
  102. package/tasks/completed/CLEAN-001.md +0 -38
  103. package/tasks/completed/CLEAN-003.md +0 -47
  104. package/tasks/completed/CLEAN-004.md +0 -56
  105. package/tasks/completed/CLEAN-005.md +0 -75
  106. package/tasks/completed/CLEAN-006.md +0 -47
  107. package/tasks/completed/CLEAN-007.md +0 -34
  108. package/tasks/completed/CLEAN-008.md +0 -49
  109. package/tasks/completed/CLEAN-012.md +0 -58
  110. package/tasks/completed/CLEAN-013.md +0 -45
  111. package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
  112. package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
  113. package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
  114. package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
  115. package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
  116. package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
  117. package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
  118. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
  119. package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
  120. package/tasks/pending/ARCH-006-task-template-location.md +0 -64
  121. package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
  122. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
  123. package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
  124. package/tasks/pending/CLEAN-002.md +0 -29
  125. package/tasks/pending/CLEAN-009.md +0 -31
  126. package/tasks/pending/CLEAN-010.md +0 -30
  127. package/tasks/pending/CLEAN-011.md +0 -30
  128. package/tasks/pending/CLEAN-014.md +0 -32
  129. package/tasks/review/task-001.md +0 -78
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # bin/lib/daemon/dependencies.sh
4
+ #
5
+ # Task dependency resolution (T2-H2)
6
+ #
7
+ # Checks blocked_by fields in pending task frontmatter. Tasks whose
8
+ # blockers are all resolved (in completed/ or merged/) are eligible
9
+ # for dispatch. Tasks with unresolved blockers are surfaced in the
10
+ # state file so Planning Hub and the dashboard can show them.
11
+ #
12
+ # Dependencies: frontmatter.js, constants.sh
13
+ # Globals required: FORGE_ROOT, TASKS_PENDING, TASKS_COMPLETED,
14
+ # TASKS_MERGED, LOG_FILE
15
+
16
+ # Prevent double-sourcing
17
+ [[ -n "${_DAEMON_DEPENDENCIES_LOADED:-}" ]] && return 0
18
+ _DAEMON_DEPENDENCIES_LOADED=1
19
+
20
+ # Node.js frontmatter helper
21
+ FRONTMATTER_JS="${FRONTMATTER_JS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js}"
22
+
23
+ # Count of currently blocked tasks (used by state.sh)
24
+ BLOCKED_TASK_COUNT=0
25
+
26
+ # Check if a task ID exists in completed or merged directories
27
+ _is_task_resolved() {
28
+ local task_id="$1"
29
+ local completed_dir="$FORGE_ROOT/$TASKS_COMPLETED"
30
+ local merged_dir="$FORGE_ROOT/$TASKS_MERGED"
31
+
32
+ # Check completed/
33
+ for f in "$completed_dir"/*.md; do
34
+ [[ -f "$f" ]] || continue
35
+ if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
36
+ return 0
37
+ fi
38
+ done
39
+
40
+ # Check merged/
41
+ for f in "$merged_dir"/*.md; do
42
+ [[ -f "$f" ]] || continue
43
+ if grep -q "^id: *${task_id}$" "$f" 2>/dev/null; then
44
+ return 0
45
+ fi
46
+ done
47
+
48
+ return 1
49
+ }
50
+
51
+ # Check all pending tasks for unresolved blockers.
52
+ # Sets BLOCKED_TASK_COUNT and outputs YAML for blocked tasks.
53
+ check_task_dependencies() {
54
+ BLOCKED_TASK_COUNT=0
55
+ local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
56
+
57
+ [[ -d "$pending_dir" ]] || return 0
58
+
59
+ for task_file in "$pending_dir"/*.md; do
60
+ [[ -f "$task_file" && ! -L "$task_file" ]] || continue
61
+
62
+ # Extract blocked_by field (comma or space separated list of task IDs)
63
+ local blocked_by
64
+ blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
65
+
66
+ [[ -z "$blocked_by" ]] && continue
67
+
68
+ # Parse the list (handle comma-separated, space-separated, or YAML array)
69
+ local blockers
70
+ blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
71
+
72
+ local unresolved=""
73
+ local all_resolved=true
74
+
75
+ for blocker_id in $blockers; do
76
+ # Skip empty tokens
77
+ [[ -z "$blocker_id" ]] && continue
78
+
79
+ if ! _is_task_resolved "$blocker_id"; then
80
+ all_resolved=false
81
+ if [[ -n "$unresolved" ]]; then
82
+ unresolved="$unresolved, $blocker_id"
83
+ else
84
+ unresolved="$blocker_id"
85
+ fi
86
+ fi
87
+ done
88
+
89
+ if [[ "$all_resolved" == "false" ]]; then
90
+ ((BLOCKED_TASK_COUNT++)) || true
91
+ local task_id
92
+ task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
93
+ task_id="${task_id:-$(basename "$task_file" .md)}"
94
+ echo "[$(date -Iseconds)] BLOCKED: $task_id waiting on: $unresolved" >> "$LOG_FILE"
95
+ fi
96
+ done
97
+ }
98
+
99
+ # Build YAML section for blocked tasks (called by state.sh update_state)
100
+ build_blocked_tasks() {
101
+ local pending_dir="$FORGE_ROOT/$TASKS_PENDING"
102
+
103
+ [[ -d "$pending_dir" ]] || return 0
104
+
105
+ local has_blocked=false
106
+
107
+ for task_file in "$pending_dir"/*.md; do
108
+ [[ -f "$task_file" && ! -L "$task_file" ]] || continue
109
+
110
+ local blocked_by
111
+ blocked_by=$(node "$FRONTMATTER_JS" "$task_file" "blocked_by" 2>/dev/null | sed -n 's/^blocked_by=//p')
112
+
113
+ [[ -z "$blocked_by" ]] && continue
114
+
115
+ local blockers unresolved_list=""
116
+ blockers=$(echo "$blocked_by" | tr ',[]' ' ' | tr -s ' ')
117
+
118
+ for blocker_id in $blockers; do
119
+ [[ -z "$blocker_id" ]] && continue
120
+ if ! _is_task_resolved "$blocker_id"; then
121
+ if [[ -n "$unresolved_list" ]]; then
122
+ unresolved_list="$unresolved_list, $blocker_id"
123
+ else
124
+ unresolved_list="$blocker_id"
125
+ fi
126
+ fi
127
+ done
128
+
129
+ if [[ -n "$unresolved_list" ]]; then
130
+ if [[ "$has_blocked" == "false" ]]; then
131
+ echo "blocked_tasks:"
132
+ has_blocked=true
133
+ fi
134
+
135
+ local task_id title
136
+ task_id=$(node "$FRONTMATTER_JS" "$task_file" "id" 2>/dev/null | sed -n 's/^id=//p')
137
+ title=$(node "$FRONTMATTER_JS" "$task_file" "title" 2>/dev/null | sed -n 's/^title=//p')
138
+ task_id="${task_id:-$(basename "$task_file" .md)}"
139
+ title="${title:-Untitled}"
140
+
141
+ printf ' - id: %s\n' "$task_id"
142
+ printf ' title: "%s"\n' "$title"
143
+ printf ' waiting_on: "%s"\n' "$unresolved_list"
144
+ fi
145
+ done
146
+ }
@@ -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,273 @@
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
+ # Node.js frontmatter helper (RT-20260405-001 MEDIUM-5: replaces grep/cut YAML parsing)
17
+ FRONTMATTER_JS="${FRONTMATTER_JS:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js}"
18
+
19
+ # Parse a single frontmatter field from a markdown file.
20
+ _notif_fm_field() {
21
+ node "$FRONTMATTER_JS" "$1" "$2" 2>/dev/null | sed -n "s/^${2}=//p"
22
+ }
23
+
24
+ # SECURITY: Sanitize message for safe use in shell commands
25
+ # Removes/escapes characters that could cause command injection
26
+ sanitize_notification_message() {
27
+ local msg="$1"
28
+ # Remove null bytes
29
+ msg="${msg//$'\0'/}"
30
+ # Remove characters that could escape out of string contexts
31
+ # PowerShell: $, `, ', ", (), {}
32
+ # osascript: ', ", \, $
33
+ # We allow: alphanumeric, spaces, periods, commas, colons, hyphens, underscores
34
+ msg=$(echo "$msg" | tr -cd 'a-zA-Z0-9 .,;:!?_-')
35
+ # Limit length to prevent buffer issues
36
+ msg="${msg:0:200}"
37
+ echo "$msg"
38
+ }
39
+
40
+ notify() {
41
+ local message="$1"
42
+ local urgency="${2:-normal}" # normal or urgent
43
+ local timestamp
44
+ timestamp=$(date -Iseconds)
45
+
46
+ # SECURITY: Sanitize before logging to prevent ANSI escape injection and
47
+ # control-character log poisoning. sanitize_notification_message() strips
48
+ # everything except alphanumeric, spaces, and safe punctuation.
49
+ local safe_message
50
+ safe_message=$(sanitize_notification_message "$message")
51
+
52
+ # Log to notifications file
53
+ echo "[$timestamp] $safe_message" >> "$NOTIFY_FILE"
54
+
55
+ # Log to main log
56
+ echo "[$timestamp] NOTIFY: $safe_message" >> "$LOG_FILE"
57
+
58
+ # Terminal bell (works in most terminals)
59
+ printf '\a'
60
+
61
+ # System toast notification for urgent messages
62
+ if [[ "$urgency" == "urgent" ]]; then
63
+ send_system_notification "$safe_message"
64
+ fi
65
+ }
66
+
67
+ # Send system-level notification (platform-specific)
68
+ send_system_notification() {
69
+ local message="$1"
70
+ local title="Vibe Forge"
71
+
72
+ # SECURITY: Sanitize message to prevent command injection
73
+ message=$(sanitize_notification_message "$message")
74
+
75
+ case "$(uname -s)" in
76
+ MINGW*|MSYS*|CYGWIN*)
77
+ # Windows: Use PowerShell toast notification
78
+ # SECURITY: Message is sanitized above, title is hardcoded
79
+ powershell.exe -NoProfile -Command "
80
+ \$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
81
+ \$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
82
+ \$textNodes = \$template.GetElementsByTagName('text')
83
+ \$textNodes.Item(0).AppendChild(\$template.CreateTextNode('$title')) | Out-Null
84
+ \$textNodes.Item(1).AppendChild(\$template.CreateTextNode('$message')) | Out-Null
85
+ \$toast = [Windows.UI.Notifications.ToastNotification]::new(\$template)
86
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Vibe Forge').Show(\$toast)
87
+ " 2>/dev/null &
88
+ ;;
89
+ Darwin)
90
+ # macOS: Use osascript
91
+ # SECURITY: Message is sanitized above
92
+ osascript -e "display notification \"$message\" with title \"$title\"" 2>/dev/null &
93
+ ;;
94
+ Linux)
95
+ # Linux: Use notify-send if available
96
+ # SECURITY: notify-send handles escaping, but message is sanitized anyway
97
+ if command -v notify-send &>/dev/null; then
98
+ notify-send "$title" "$message" 2>/dev/null &
99
+ fi
100
+ ;;
101
+ esac
102
+ }
103
+
104
+ check_new_pending_tasks() {
105
+ # Create notified file if it doesn't exist
106
+ touch "$NOTIFIED_FILE"
107
+
108
+ # Check for new pending tasks
109
+ for task in "$FORGE_ROOT/$TASKS_PENDING"/*.md; do
110
+ if [[ -f "$task" && ! -L "$task" ]]; then
111
+ local filename
112
+ filename=$(basename "$task")
113
+
114
+ # Check if we've already notified about this task
115
+ if ! grep -qF "$filename" "$NOTIFIED_FILE" 2>/dev/null; then
116
+ # Extract task info from frontmatter safely
117
+ local task_id task_title assigned_to
118
+
119
+ task_id=$(_notif_fm_field "$task" "id")
120
+ task_title=$(_notif_fm_field "$task" "title")
121
+ assigned_to=$(_notif_fm_field "$task" "assigned_to")
122
+
123
+ # Use filename as fallback
124
+ task_id="${task_id:-$filename}"
125
+ task_title="${task_title:-New task}"
126
+
127
+ # Notify
128
+ if [[ -n "$assigned_to" ]]; then
129
+ notify "New task for $assigned_to: $task_title ($task_id)"
130
+ else
131
+ notify "New pending task: $task_title ($task_id)"
132
+ fi
133
+
134
+ # Mark as notified (atomic append)
135
+ echo "$filename" >> "$NOTIFIED_FILE"
136
+ fi
137
+ fi
138
+ done
139
+
140
+ # Also check needs-changes for tasks that need rework
141
+ for task in "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"/*.md; do
142
+ if [[ -f "$task" && ! -L "$task" ]]; then
143
+ local filename
144
+ filename=$(basename "$task")
145
+ local notified_key="needs-changes:$filename"
146
+
147
+ if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
148
+ local task_id assigned_to
149
+ task_id=$(_notif_fm_field "$task" "id")
150
+ assigned_to=$(_notif_fm_field "$task" "assigned_to")
151
+
152
+ task_id="${task_id:-$filename}"
153
+
154
+ if [[ -n "$assigned_to" ]]; then
155
+ notify "Task needs changes ($assigned_to): $task_id"
156
+ else
157
+ notify "Task needs changes: $task_id"
158
+ fi
159
+
160
+ echo "$notified_key" >> "$NOTIFIED_FILE"
161
+ fi
162
+ fi
163
+ done
164
+ }
165
+
166
+ check_attention_needed() {
167
+ # Check for workers needing attention (urgent notifications)
168
+ if [[ ! -d "$FORGE_ROOT/$TASKS_ATTENTION" ]]; then
169
+ return 0
170
+ fi
171
+
172
+ for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
173
+ if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
174
+ local filename
175
+ filename=$(basename "$attention_file")
176
+ local notified_key="attention:$filename"
177
+
178
+ if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
179
+ # Extract attention info
180
+ local agent issue
181
+ agent=$(_notif_fm_field "$attention_file" "agent")
182
+ issue=$(node "$FRONTMATTER_JS" --section "$attention_file" "Issue" 2>/dev/null)
183
+ [[ -z "$issue" ]] && issue=$(grep -m1 "^##" "$attention_file" 2>/dev/null | sed 's/^## *//' | head -c 200)
184
+
185
+ agent="${agent:-Unknown}"
186
+ issue="${issue:-Needs attention}"
187
+
188
+ # Ring bell multiple times for attention
189
+ printf '\a\a\a'
190
+
191
+ # Send urgent notification with toast
192
+ notify "🔔 $agent needs help: $issue" "urgent"
193
+
194
+ echo "$notified_key" >> "$NOTIFIED_FILE"
195
+ fi
196
+ fi
197
+ done
198
+ }
199
+
200
+ # check_heimdall_escalations INBOX_DIR
201
+ # Scans the lab worker inbox for .escalation signal files written by Heimdall
202
+ # when a worker accumulates too many policy violations (Gjallarhorn sounded).
203
+ # Writes an escalation handoff for lab sentinel to route to human review.
204
+ check_heimdall_escalations() {
205
+ local inbox_dir="$1"
206
+
207
+ [[ -d "$inbox_dir" ]] || return 0
208
+
209
+ # shopt -s globstar needed for **; fall back to find for compatibility
210
+ local escalation_files
211
+ mapfile -t escalation_files < <(find "$inbox_dir" -name "*.escalation" -type f 2>/dev/null)
212
+
213
+ for escalation_file in "${escalation_files[@]}"; do
214
+ [[ -f "$escalation_file" ]] || continue
215
+
216
+ local story_id agent_dir agent handoff_dir
217
+ story_id=$(basename "$escalation_file" .escalation)
218
+ agent_dir=$(dirname "$escalation_file")
219
+ agent=$(basename "$agent_dir")
220
+
221
+ # Read escalation JSON for violation details
222
+ local violations last_reason
223
+ # SECURITY: Pass file path as argv, not string interpolation, to prevent
224
+ # code injection via crafted filenames (RT-20260405-001 HIGH-1)
225
+ violations=$(node -e "try{const d=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));console.log(d.violations||'?')}catch(e){console.log('?')}" -- "$escalation_file" 2>/dev/null)
226
+ last_reason=$(node -e "try{const d=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));console.log(d.last_reason||'')}catch(e){console.log('')}" -- "$escalation_file" 2>/dev/null)
227
+
228
+ echo "[$(date -Iseconds)] HEIMDALL SOUNDED: $agent/$story_id ($violations violations) -- writing escalation handoff" >> "$LOG_FILE"
229
+
230
+ # Write escalation handoff for lab sentinel
231
+ # Lab sentinel routes escalations to human review on next cycle
232
+ local handoff_dir="$FORGE_ROOT/_vibe-chain-output/handoffs"
233
+ if [[ -d "$handoff_dir" ]]; then
234
+ local handoff_file="$handoff_dir/${story_id}-worker-escalation.md"
235
+ cat > "$handoff_file" << HANDOFF
236
+ ---
237
+ type: worker-escalation
238
+ story: "$story_id"
239
+ from: forge-daemon
240
+ to: human
241
+ created: $(date -Iseconds)
242
+ status: pending
243
+ agent: "$agent"
244
+ violations: $violations
245
+ ---
246
+
247
+ ## Heimdall Escalation
248
+
249
+ Worker \`$agent\` accumulated $violations policy violations on story \`$story_id\`.
250
+ The worker has been halted for this story. Human review required before resubmitting.
251
+
252
+ ## Last Violation
253
+
254
+ $last_reason
255
+
256
+ ## Audit Log
257
+
258
+ Full violation history: \`_vibe-chain-output/heimdall-audit.log\`
259
+
260
+ ## Action Required
261
+
262
+ Review the audit log and decide:
263
+ - Fix the story spec and resubmit
264
+ - Manually complete the work
265
+ - Cancel the story
266
+ HANDOFF
267
+ echo "[$(date -Iseconds)] Escalation handoff written: $handoff_file" >> "$LOG_FILE"
268
+ fi
269
+
270
+ # Mark escalation as processed so it is not re-processed next cycle
271
+ mv "$escalation_file" "${escalation_file}.processed" 2>/dev/null || true
272
+ done
273
+ }
@@ -0,0 +1,93 @@
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_*), database.sh (db_record_status_history)
8
+ # Globals required: FORGE_ROOT, LOG_FILE, FORGE_DB,
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 — but ONLY if they have
57
+ # ready_for_review: true in frontmatter. Tasks that were moved
58
+ # to completed/ manually (e.g., already reviewed, historical)
59
+ # should not be re-routed.
60
+ for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
61
+ if [[ -f "$task" && ! -L "$task" ]]; then
62
+ # Only route tasks explicitly marked for review
63
+ local ready_for_review
64
+ ready_for_review=$(grep -m1 "^ready_for_review:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 10)
65
+ [[ "$ready_for_review" == "true" ]] || continue
66
+
67
+ local filename task_id assigned_to
68
+ filename=$(basename "$task")
69
+ task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
70
+ assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
71
+ echo "[$(date -Iseconds)] Routing $filename to review" >> "$LOG_FILE"
72
+ if safe_move_task "$task" "$FORGE_ROOT/$TASKS_REVIEW"; then
73
+ db_record_status_history "${assigned_to:-unknown}" "review" "${task_id:-$filename}" 2>/dev/null || true
74
+ fi
75
+ fi
76
+ done
77
+ }
78
+
79
+ route_approved_to_merged() {
80
+ # Move approved tasks to merged archive
81
+ for task in "$FORGE_ROOT/$TASKS_APPROVED"/*.md; do
82
+ if [[ -f "$task" && ! -L "$task" ]]; then
83
+ local filename task_id assigned_to
84
+ filename=$(basename "$task")
85
+ task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
86
+ assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d " \"'" | head -c 50)
87
+ echo "[$(date -Iseconds)] Archiving $filename to merged" >> "$LOG_FILE"
88
+ if safe_move_task "$task" "$FORGE_ROOT/$TASKS_MERGED"; then
89
+ db_record_status_history "${assigned_to:-unknown}" "merged" "${task_id:-$filename}" 2>/dev/null || true
90
+ fi
91
+ fi
92
+ done
93
+ }