@sugar-crash-studios/vibe-forge 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- package/tasks/review/task-001.md +78 -0
|
@@ -0,0 +1,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
|
+
}
|