vibe-forge 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/configure-vcs.md +102 -102
- package/.claude/commands/forge.md +218 -218
- package/.claude/hooks/worker-loop.js +220 -217
- package/.claude/settings.json +89 -89
- package/README.md +149 -191
- package/agents/aegis/personality.md +303 -303
- package/agents/anvil/personality.md +278 -278
- package/agents/architect/personality.md +260 -260
- package/agents/crucible/personality.md +362 -362
- package/agents/crucible-x/personality.md +210 -210
- package/agents/ember/personality.md +293 -293
- package/agents/flux/personality.md +248 -248
- package/agents/furnace/personality.md +342 -342
- package/agents/herald/personality.md +249 -249
- package/agents/oracle/personality.md +284 -284
- package/agents/pixel/personality.md +140 -140
- package/agents/planning-hub/personality.md +473 -473
- package/agents/scribe/personality.md +253 -253
- package/agents/slag/personality.md +268 -268
- package/agents/temper/personality.md +270 -270
- package/bin/cli.js +372 -372
- package/bin/forge-daemon.sh +477 -477
- package/bin/forge-setup.sh +662 -661
- package/bin/forge-spawn.sh +164 -164
- package/bin/forge.sh +566 -566
- package/docs/commands.md +8 -8
- package/package.json +77 -77
- package/{bin → src}/lib/agents.sh +177 -177
- package/{bin → src}/lib/check-aliases.js +50 -50
- package/{bin → src}/lib/colors.sh +45 -44
- package/{bin → src}/lib/config.sh +347 -347
- package/{bin → src}/lib/constants.sh +241 -241
- package/{bin → src}/lib/daemon/budgets.sh +107 -107
- package/{bin → src}/lib/daemon/dependencies.sh +146 -146
- package/{bin → src}/lib/daemon/display.sh +128 -128
- package/{bin → src}/lib/daemon/notifications.sh +273 -273
- package/{bin → src}/lib/daemon/routing.sh +93 -93
- package/{bin → src}/lib/daemon/state.sh +163 -163
- package/{bin → src}/lib/daemon/sync.sh +103 -103
- package/{bin → src}/lib/database.sh +357 -357
- package/{bin → src}/lib/frontmatter.js +106 -106
- package/{bin → src}/lib/heimdall-setup.js +113 -113
- package/{bin → src}/lib/heimdall.js +265 -265
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +264 -264
- package/{bin → src}/lib/terminal.js +452 -452
- package/{bin → src}/lib/util.sh +126 -126
- package/{bin → src}/lib/vcs.js +349 -349
- package/{context → templates}/project-context-template.md +122 -122
- package/config/task-template.md +0 -159
- package/config/templates/handoff-template.md +0 -40
|
@@ -1,273 +1,273 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
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
|
-
}
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# src/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
|
+
}
|