vibe-forge 0.4.0 → 0.8.2
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 -63
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +5 -5
- package/.claude/commands/forge.md +50 -3
- package/.claude/commands/need-help.md +77 -77
- package/.claude/commands/update-status.md +64 -64
- package/.claude/commands/worker-loop.md +106 -106
- package/.claude/hooks/worker-loop.js +37 -4
- package/.claude/scripts/setup-worker-loop.sh +45 -45
- package/.claude/settings.json +89 -0
- package/LICENSE +21 -21
- package/README.md +211 -232
- package/agents/aegis/personality.md +35 -1
- package/agents/anvil/personality.md +39 -1
- package/agents/architect/personality.md +26 -0
- package/agents/crucible/personality.md +54 -1
- package/agents/crucible-x/personality.md +210 -0
- package/agents/ember/personality.md +29 -1
- package/agents/flux/personality.md +248 -0
- package/agents/furnace/personality.md +52 -1
- package/agents/herald/personality.md +3 -1
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +284 -0
- package/agents/pixel/personality.md +140 -0
- package/agents/planning-hub/personality.md +222 -0
- package/agents/scribe/personality.md +3 -1
- package/agents/slag/personality.md +268 -0
- package/agents/{sentinel → temper}/personality.md +85 -9
- package/bin/cli.js +77 -30
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +507 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
- package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +645 -0
- package/bin/forge-daemon.sh +176 -550
- package/bin/forge-setup.sh +28 -11
- package/bin/forge-spawn.sh +5 -5
- package/bin/forge.cmd +83 -83
- package/bin/forge.sh +210 -31
- package/config/agent-manifest.yaml +237 -243
- package/config/agents.json +207 -132
- package/config/task-types.yaml +111 -106
- package/context/agent-overrides/README.md +41 -0
- package/context/architecture.md +42 -0
- package/context/modern-conventions.md +129 -129
- package/docs/agents.md +473 -409
- package/docs/architecture.md +194 -162
- package/docs/commands.md +451 -388
- package/docs/security.md +195 -144
- package/package.json +38 -11
- package/src/lib/check-aliases.js +50 -0
- package/{bin → src}/lib/colors.sh +2 -1
- package/src/lib/config.sh +347 -0
- package/{bin → src}/lib/constants.sh +48 -13
- package/src/lib/daemon/budgets.sh +107 -0
- package/src/lib/daemon/dependencies.sh +146 -0
- package/src/lib/daemon/display.sh +128 -0
- package/src/lib/daemon/notifications.sh +273 -0
- package/src/lib/daemon/routing.sh +93 -0
- package/src/lib/daemon/state.sh +163 -0
- package/src/lib/daemon/sync.sh +103 -0
- package/{bin → src}/lib/database.sh +52 -0
- package/src/lib/frontmatter.js +106 -0
- package/src/lib/heimdall-setup.js +113 -0
- package/src/lib/heimdall.js +265 -0
- package/src/lib/index.sh +25 -0
- package/{bin → src}/lib/json.sh +7 -1
- package/{bin → src}/lib/terminal.js +7 -1
- package/.claude/settings.local.json +0 -33
- package/agents/forge-master/capabilities.md +0 -144
- package/agents/forge-master/context-template.md +0 -128
- package/agents/forge-master/personality.md +0 -138
- package/bin/lib/config.sh +0 -313
- package/config/task-template.md +0 -87
- package/context/forge-state.yaml +0 -19
- package/docs/TODO.md +0 -150
- package/docs/getting-started.md +0 -243
- package/docs/npm-publishing.md +0 -95
- package/docs/workflows/README.md +0 -32
- package/docs/workflows/azure-devops.md +0 -108
- package/docs/workflows/bitbucket.md +0 -104
- package/docs/workflows/git-only.md +0 -130
- package/docs/workflows/gitea.md +0 -168
- package/docs/workflows/github.md +0 -103
- package/docs/workflows/gitlab.md +0 -105
- package/docs/workflows.md +0 -454
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
- package/tasks/completed/ARCH-009-test-organization.md +0 -78
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
- package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
- package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
- package/tasks/completed/CLEAN-001.md +0 -38
- package/tasks/completed/CLEAN-003.md +0 -47
- package/tasks/completed/CLEAN-004.md +0 -56
- package/tasks/completed/CLEAN-005.md +0 -75
- package/tasks/completed/CLEAN-006.md +0 -47
- package/tasks/completed/CLEAN-007.md +0 -34
- package/tasks/completed/CLEAN-008.md +0 -49
- package/tasks/completed/CLEAN-012.md +0 -58
- package/tasks/completed/CLEAN-013.md +0 -45
- package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
- package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
- package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
- package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
- package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
- package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
- package/tasks/pending/ARCH-006-task-template-location.md +0 -64
- package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
- package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
- package/tasks/pending/CLEAN-002.md +0 -29
- package/tasks/pending/CLEAN-009.md +0 -31
- package/tasks/pending/CLEAN-010.md +0 -30
- package/tasks/pending/CLEAN-011.md +0 -30
- package/tasks/pending/CLEAN-014.md +0 -32
- package/tasks/review/task-001.md +0 -78
- /package/{bin → src}/lib/agents.sh +0 -0
- /package/{bin → src}/lib/util.sh +0 -0
- /package/{bin → src}/lib/vcs.js +0 -0
- /package/{context → templates}/project-context-template.md +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# src/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
|
+
# Node.js frontmatter helper (RT-20260405-001 MEDIUM-5: replaces grep/cut YAML parsing)
|
|
20
|
+
FRONTMATTER_JS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/frontmatter.js"
|
|
21
|
+
|
|
22
|
+
# Parse a single frontmatter field from a markdown file.
|
|
23
|
+
# Usage: fm_field <file> <field>
|
|
24
|
+
fm_field() {
|
|
25
|
+
node "$FRONTMATTER_JS" "$1" "$2" 2>/dev/null | sed -n "s/^${2}=//p"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Parse a markdown section's first content line.
|
|
29
|
+
# Usage: fm_section <file> <heading>
|
|
30
|
+
fm_section() {
|
|
31
|
+
node "$FRONTMATTER_JS" --section "$1" "$2" 2>/dev/null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
update_state() {
|
|
35
|
+
# Count tasks in each folder (using find with -maxdepth for safety)
|
|
36
|
+
local pending in_progress completed review approved needs_changes merged attention
|
|
37
|
+
pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
38
|
+
in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
39
|
+
completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
40
|
+
review=$(find "$FORGE_ROOT/$TASKS_REVIEW" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
41
|
+
approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
42
|
+
needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
43
|
+
merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
44
|
+
attention=$(find "$FORGE_ROOT/$TASKS_ATTENTION" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
45
|
+
|
|
46
|
+
local blocked=${BLOCKED_TASK_COUNT:-0}
|
|
47
|
+
|
|
48
|
+
# Build active task details for in-progress tasks
|
|
49
|
+
local active_tasks_details=""
|
|
50
|
+
if [[ "$in_progress" -gt 0 ]]; then
|
|
51
|
+
active_tasks_details=$(build_active_tasks)
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Build attention details if any workers need help
|
|
55
|
+
local attention_details=""
|
|
56
|
+
if [[ "$attention" -gt 0 ]]; then
|
|
57
|
+
attention_details=$(build_attention_details)
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Build blocked task details (T2-H2)
|
|
61
|
+
local blocked_details=""
|
|
62
|
+
if [[ "$blocked" -gt 0 ]]; then
|
|
63
|
+
blocked_details=$(build_blocked_tasks)
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Build worker status from agent-status files
|
|
67
|
+
local worker_status=""
|
|
68
|
+
if [[ -d "$FORGE_ROOT/$AGENT_STATUS_DIR" ]]; then
|
|
69
|
+
worker_status=$(build_worker_status)
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Write state file atomically (write to temp, then move)
|
|
73
|
+
local temp_state="${STATE_FILE}.tmp.$$"
|
|
74
|
+
cat > "$temp_state" << EOF
|
|
75
|
+
# Vibe Forge State
|
|
76
|
+
# Auto-updated by forge-daemon
|
|
77
|
+
# Last updated: $(date -Iseconds)
|
|
78
|
+
|
|
79
|
+
forge:
|
|
80
|
+
status: active
|
|
81
|
+
daemon_pid: $$
|
|
82
|
+
|
|
83
|
+
tasks:
|
|
84
|
+
pending: $pending
|
|
85
|
+
in_progress: $in_progress
|
|
86
|
+
completed: $completed
|
|
87
|
+
in_review: $review
|
|
88
|
+
approved: $approved
|
|
89
|
+
needs_changes: $needs_changes
|
|
90
|
+
merged: $merged
|
|
91
|
+
blocked: $blocked
|
|
92
|
+
attention_needed: $attention
|
|
93
|
+
|
|
94
|
+
$active_tasks_details
|
|
95
|
+
$attention_details
|
|
96
|
+
$blocked_details
|
|
97
|
+
$worker_status
|
|
98
|
+
last_updated: $(date -Iseconds)
|
|
99
|
+
EOF
|
|
100
|
+
mv "$temp_state" "$STATE_FILE"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
build_attention_details() {
|
|
104
|
+
echo "attention:"
|
|
105
|
+
for attention_file in "$FORGE_ROOT/$TASKS_ATTENTION"/*.md; do
|
|
106
|
+
if [[ -f "$attention_file" && ! -L "$attention_file" ]]; then
|
|
107
|
+
local agent created issue
|
|
108
|
+
agent=$(fm_field "$attention_file" "agent")
|
|
109
|
+
created=$(fm_field "$attention_file" "created")
|
|
110
|
+
issue=$(fm_section "$attention_file" "Issue")
|
|
111
|
+
issue="${issue:-Needs attention}"
|
|
112
|
+
|
|
113
|
+
printf ' - agent: %s\n' "$agent"
|
|
114
|
+
printf ' since: %s\n' "$created"
|
|
115
|
+
printf ' issue: "%s"\n' "$issue"
|
|
116
|
+
fi
|
|
117
|
+
done
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
build_active_tasks() {
|
|
121
|
+
echo "active_tasks:"
|
|
122
|
+
for task_file in "$FORGE_ROOT/$TASKS_IN_PROGRESS"/*.md; do
|
|
123
|
+
if [[ -f "$task_file" && ! -L "$task_file" ]]; then
|
|
124
|
+
local task_id title assigned_to
|
|
125
|
+
task_id=$(fm_field "$task_file" "id")
|
|
126
|
+
title=$(fm_field "$task_file" "title")
|
|
127
|
+
assigned_to=$(fm_field "$task_file" "assigned_to")
|
|
128
|
+
|
|
129
|
+
task_id="${task_id:-$(basename "$task_file" .md)}"
|
|
130
|
+
title="${title:-Untitled}"
|
|
131
|
+
assigned_to="${assigned_to:-unassigned}"
|
|
132
|
+
|
|
133
|
+
printf ' - id: %s\n' "$task_id"
|
|
134
|
+
printf ' title: "%s"\n' "$title"
|
|
135
|
+
printf ' assigned_to: %s\n' "$assigned_to"
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Determine daemon state based on activity (for adaptive polling)
|
|
141
|
+
determine_daemon_state() {
|
|
142
|
+
# Check if there are in-progress tasks
|
|
143
|
+
local in_progress_count
|
|
144
|
+
in_progress_count=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
145
|
+
|
|
146
|
+
# Check if there are active workers
|
|
147
|
+
local active_workers
|
|
148
|
+
active_workers=$(db_count_active_workers 2>/dev/null || echo "0")
|
|
149
|
+
|
|
150
|
+
if [[ "$in_progress_count" -gt 0 ]] || [[ "$active_workers" -gt 0 ]]; then
|
|
151
|
+
echo "active"
|
|
152
|
+
else
|
|
153
|
+
echo "idle"
|
|
154
|
+
fi
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Get current poll interval in seconds (from DB, with fallback)
|
|
158
|
+
get_poll_interval() {
|
|
159
|
+
local interval_ms
|
|
160
|
+
interval_ms=$(db_get_poll_interval_ms 2>/dev/null || echo "30000")
|
|
161
|
+
# Convert ms to seconds (bash integer division)
|
|
162
|
+
echo $((interval_ms / 1000))
|
|
163
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# src/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
|
+
# Check if status actually changed (T1-D3: record transitions)
|
|
47
|
+
local old_status
|
|
48
|
+
old_status=$(sqlite3 "$FORGE_DB" \
|
|
49
|
+
"SELECT status FROM agent_status WHERE agent = '$(db_escape "$agent")';" 2>/dev/null || echo "")
|
|
50
|
+
if [[ "$old_status" != "$status" && -n "$status" ]]; then
|
|
51
|
+
db_record_status_history "$agent" "$status" "$task"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Upsert to database
|
|
55
|
+
db_upsert_agent_status "$agent" "$status" "$task" "$message" "$updated" "$file_mtime"
|
|
56
|
+
|
|
57
|
+
echo "[$(date -Iseconds)] Synced status for $agent: $status" >> "$LOG_FILE"
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Build worker status from SQLite (for YAML output)
|
|
64
|
+
build_worker_status() {
|
|
65
|
+
local now_epoch
|
|
66
|
+
now_epoch=$(date +%s)
|
|
67
|
+
local stale_threshold=$STALE_STATUS_THRESHOLD
|
|
68
|
+
|
|
69
|
+
# Check if we have any agent status in DB
|
|
70
|
+
local agent_count
|
|
71
|
+
agent_count=$(sqlite3 "$FORGE_DB" "SELECT COUNT(*) FROM agent_status;" 2>/dev/null || echo "0")
|
|
72
|
+
|
|
73
|
+
if [[ "$agent_count" -eq 0 ]]; then
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
echo "workers:"
|
|
78
|
+
|
|
79
|
+
# Read from database
|
|
80
|
+
while IFS='|' read -r agent status task message updated; do
|
|
81
|
+
local stale_marker=""
|
|
82
|
+
|
|
83
|
+
# Check if stale
|
|
84
|
+
if [[ -n "$updated" ]]; then
|
|
85
|
+
local updated_epoch age
|
|
86
|
+
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")
|
|
87
|
+
age=$((now_epoch - updated_epoch))
|
|
88
|
+
if [[ "$age" -gt "$stale_threshold" ]]; then
|
|
89
|
+
stale_marker=" (stale)"
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo " - agent: $agent"
|
|
94
|
+
echo " status: $status$stale_marker"
|
|
95
|
+
if [[ -n "$task" ]]; then
|
|
96
|
+
echo " task: $task"
|
|
97
|
+
fi
|
|
98
|
+
if [[ -n "$message" ]]; then
|
|
99
|
+
echo " message: \"$message\""
|
|
100
|
+
fi
|
|
101
|
+
echo " updated: $updated"
|
|
102
|
+
done < <(db_get_all_agent_statuses)
|
|
103
|
+
}
|
|
@@ -51,10 +51,57 @@ db_require_init() {
|
|
|
51
51
|
return 0
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
# Check if sqlite3 is available; attempt install if missing
|
|
55
|
+
db_require_sqlite3() {
|
|
56
|
+
if command -v sqlite3 &>/dev/null; then
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
echo "sqlite3 not found. Attempting to install..." >&2
|
|
61
|
+
|
|
62
|
+
# Try platform-appropriate package manager
|
|
63
|
+
if command -v apt-get &>/dev/null; then
|
|
64
|
+
echo "Installing sqlite3 via apt (may require sudo password)..." >&2
|
|
65
|
+
sudo apt-get install -y sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
66
|
+
elif command -v brew &>/dev/null; then
|
|
67
|
+
echo "Installing sqlite3 via Homebrew..." >&2
|
|
68
|
+
brew install sqlite3 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
69
|
+
elif command -v pacman &>/dev/null; then
|
|
70
|
+
echo "Installing sqlite via pacman (may require sudo password)..." >&2
|
|
71
|
+
sudo pacman -S --noconfirm sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
72
|
+
elif command -v dnf &>/dev/null; then
|
|
73
|
+
echo "Installing sqlite via dnf (may require sudo password)..." >&2
|
|
74
|
+
sudo dnf install -y sqlite 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
75
|
+
elif command -v winget &>/dev/null; then
|
|
76
|
+
echo "Installing SQLite via winget. You may see a UAC prompt; please approve it." >&2
|
|
77
|
+
winget install --id SQLite.SQLite -e --silent 2>/dev/null
|
|
78
|
+
# winget installs to Program Files but doesn't add to Git Bash PATH
|
|
79
|
+
local winget_sqlite="/c/Program Files/SQLite/sqlite3.exe"
|
|
80
|
+
if [[ -f "$winget_sqlite" ]]; then
|
|
81
|
+
export PATH="$PATH:/c/Program Files/SQLite"
|
|
82
|
+
command -v sqlite3 &>/dev/null && return 0
|
|
83
|
+
fi
|
|
84
|
+
elif command -v choco &>/dev/null; then
|
|
85
|
+
echo "Installing sqlite via Chocolatey. You may see a UAC prompt; please approve it." >&2
|
|
86
|
+
choco install sqlite -y 2>/dev/null && command -v sqlite3 &>/dev/null && return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "ERROR: sqlite3 is required but could not be installed automatically." >&2
|
|
90
|
+
echo "Please install sqlite3 manually:" >&2
|
|
91
|
+
echo " macOS: brew install sqlite3" >&2
|
|
92
|
+
echo " Ubuntu: sudo apt-get install sqlite3" >&2
|
|
93
|
+
echo " Fedora: sudo dnf install sqlite" >&2
|
|
94
|
+
echo " Windows: choco install sqlite" >&2
|
|
95
|
+
echo " or: winget install SQLite.SQLite" >&2
|
|
96
|
+
echo " (then add the install folder to your PATH)" >&2
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
|
|
54
100
|
# Initialize the database with schema
|
|
55
101
|
# Requires: FORGE_DB must be set before calling
|
|
56
102
|
db_init() {
|
|
57
103
|
db_require_init || return 1
|
|
104
|
+
db_require_sqlite3 || return 1
|
|
58
105
|
local db_dir
|
|
59
106
|
db_dir=$(dirname "$FORGE_DB")
|
|
60
107
|
|
|
@@ -62,6 +109,11 @@ db_init() {
|
|
|
62
109
|
mkdir -p "$db_dir"
|
|
63
110
|
chmod 700 "$db_dir"
|
|
64
111
|
|
|
112
|
+
# Enable WAL mode for concurrent reads without blocking writes.
|
|
113
|
+
# WAL + NORMAL synchronous is the recommended pairing for local tooling:
|
|
114
|
+
# faster than DELETE+FULL while still durable against OS crashes.
|
|
115
|
+
sqlite3 "$FORGE_DB" "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;"
|
|
116
|
+
|
|
65
117
|
# Create tables if they don't exist
|
|
66
118
|
sqlite3 "$FORGE_DB" <<'SQL'
|
|
67
119
|
-- Daemon configuration (single row)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* frontmatter.js - Safe YAML frontmatter field extractor
|
|
4
|
+
*
|
|
5
|
+
* Replaces grep/cut pipelines for extracting frontmatter fields from
|
|
6
|
+
* task and attention markdown files (RT-20260405-001 MEDIUM-5).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node frontmatter.js <file> <field1> [field2] ...
|
|
10
|
+
* node frontmatter.js --section <file> <heading>
|
|
11
|
+
*
|
|
12
|
+
* Output (field mode):
|
|
13
|
+
* field1=value1
|
|
14
|
+
* field2=value2
|
|
15
|
+
*
|
|
16
|
+
* Output (section mode):
|
|
17
|
+
* First non-heading line under the matched ## heading
|
|
18
|
+
*
|
|
19
|
+
* Values are sanitized for safe shell consumption:
|
|
20
|
+
* - Shell metacharacters removed
|
|
21
|
+
* - Length capped at 200 chars
|
|
22
|
+
* - Missing fields output as empty: field=
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const yaml = require('js-yaml');
|
|
29
|
+
|
|
30
|
+
const MAX_VALUE_LENGTH = 200;
|
|
31
|
+
|
|
32
|
+
// Strip characters unsafe for shell interpolation
|
|
33
|
+
function sanitize(val) {
|
|
34
|
+
if (val == null) return '';
|
|
35
|
+
const str = String(val)
|
|
36
|
+
.replace(/[\0\r]/g, '')
|
|
37
|
+
.replace(/[\n]/g, ' ')
|
|
38
|
+
.replace(/[$`"'\\(){}[\]!#;|&<>]/g, '')
|
|
39
|
+
.trim();
|
|
40
|
+
return str.substring(0, MAX_VALUE_LENGTH);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractFrontmatter(content) {
|
|
44
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
45
|
+
if (!match) return {};
|
|
46
|
+
try {
|
|
47
|
+
const parsed = yaml.load(match[1]);
|
|
48
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function extractSection(content, heading) {
|
|
55
|
+
const pattern = new RegExp(`^## ${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'im');
|
|
56
|
+
const idx = content.search(pattern);
|
|
57
|
+
if (idx === -1) return '';
|
|
58
|
+
const after = content.substring(idx).split('\n').slice(1);
|
|
59
|
+
for (const line of after) {
|
|
60
|
+
if (line.startsWith('## ')) break;
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (trimmed) return sanitize(trimmed);
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Main
|
|
68
|
+
const args = process.argv.slice(2);
|
|
69
|
+
|
|
70
|
+
if (args.length < 2) {
|
|
71
|
+
process.stderr.write('Usage: node frontmatter.js <file> <field1> [field2] ...\n');
|
|
72
|
+
process.stderr.write(' node frontmatter.js --section <file> <heading>\n');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Section mode: --section <file> <heading>
|
|
77
|
+
if (args[0] === '--section') {
|
|
78
|
+
const file = args[1];
|
|
79
|
+
const heading = args[2];
|
|
80
|
+
if (!file || !heading) {
|
|
81
|
+
process.stderr.write('Usage: node frontmatter.js --section <file> <heading>\n');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
let content;
|
|
85
|
+
try { content = fs.readFileSync(file, 'utf8'); } catch (_) { process.exit(0); }
|
|
86
|
+
process.stdout.write(extractSection(content, heading) + '\n');
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Field mode: <file> <field1> [field2] ...
|
|
91
|
+
const file = args[0];
|
|
92
|
+
const fields = args.slice(1);
|
|
93
|
+
|
|
94
|
+
let content;
|
|
95
|
+
try { content = fs.readFileSync(file, 'utf8'); } catch (_) {
|
|
96
|
+
// File unreadable: output empty values
|
|
97
|
+
for (const f of fields) process.stdout.write(`${f}=\n`);
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = extractFrontmatter(content);
|
|
102
|
+
|
|
103
|
+
for (const field of fields) {
|
|
104
|
+
const val = sanitize(data[field]);
|
|
105
|
+
process.stdout.write(`${field}=${val}\n`);
|
|
106
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Heimdall Setup -- writes .claude/settings.local.json into a worker's
|
|
4
|
+
* working directory to register Heimdall as a PreToolUse hook.
|
|
5
|
+
*
|
|
6
|
+
* Called by the forge daemon at inbox-write time, before the worker
|
|
7
|
+
* picks up a lab task.
|
|
8
|
+
*
|
|
9
|
+
* Uses a merge strategy: if settings.local.json already exists, the
|
|
10
|
+
* Heimdall hooks are merged into the existing PreToolUse array rather
|
|
11
|
+
* than overwriting the file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict'
|
|
15
|
+
|
|
16
|
+
const fs = require('fs')
|
|
17
|
+
const path = require('path')
|
|
18
|
+
|
|
19
|
+
// Absolute path to heimdall.js -- resolvable from any working directory
|
|
20
|
+
const HEIMDALL_PATH = path.resolve(__dirname, 'heimdall.js').replace(/\\/g, '/')
|
|
21
|
+
|
|
22
|
+
const HEIMDALL_HOOKS = ['Bash', 'Write', 'Edit'].map(matcher => ({
|
|
23
|
+
matcher,
|
|
24
|
+
hooks: [{ type: 'command', command: `node "${HEIMDALL_PATH}"` }],
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* writeHeimdallHooks(worktreePath)
|
|
29
|
+
*
|
|
30
|
+
* Writes or merges Heimdall PreToolUse hooks into:
|
|
31
|
+
* <worktreePath>/.claude/settings.local.json
|
|
32
|
+
*
|
|
33
|
+
* Safe to call multiple times -- idempotent.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} worktreePath Absolute path to the worker's worktree root
|
|
36
|
+
*/
|
|
37
|
+
function writeHeimdallHooks(worktreePath) {
|
|
38
|
+
const claudeDir = path.join(worktreePath, '.claude')
|
|
39
|
+
const settingsPath = path.join(claudeDir, 'settings.local.json')
|
|
40
|
+
|
|
41
|
+
// Ensure .claude/ exists
|
|
42
|
+
if (!fs.existsSync(claudeDir)) {
|
|
43
|
+
fs.mkdirSync(claudeDir, { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Read existing settings if present
|
|
47
|
+
let existing = {}
|
|
48
|
+
if (fs.existsSync(settingsPath)) {
|
|
49
|
+
try {
|
|
50
|
+
existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
|
|
51
|
+
} catch (_) {
|
|
52
|
+
// Corrupt file -- start fresh
|
|
53
|
+
existing = {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Ensure hooks structure exists
|
|
58
|
+
if (!existing.hooks) existing.hooks = {}
|
|
59
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = []
|
|
60
|
+
|
|
61
|
+
// Merge: add Heimdall hook entries for matchers not already registered
|
|
62
|
+
const existingMatchers = new Set(
|
|
63
|
+
existing.hooks.PreToolUse.map(h => h.matcher)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
for (const heimdallHook of HEIMDALL_HOOKS) {
|
|
67
|
+
if (!existingMatchers.has(heimdallHook.matcher)) {
|
|
68
|
+
existing.hooks.PreToolUse.push(heimdallHook)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + '\n')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* writeContextFile(worktreePath, context)
|
|
77
|
+
*
|
|
78
|
+
* Writes the Heimdall context file to the worktree root so Heimdall
|
|
79
|
+
* can read per-task policy on every invocation.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} worktreePath Absolute path to the worker's worktree root
|
|
82
|
+
* @param {object} context Context object matching the schema below
|
|
83
|
+
*
|
|
84
|
+
* Context schema:
|
|
85
|
+
* {
|
|
86
|
+
* story_id: string -- lab story ID (e.g. "FORGE-3")
|
|
87
|
+
* agent: string -- worker name (e.g. "anvil")
|
|
88
|
+
* worktree_path: string -- absolute path to worktree (same as worktreePath)
|
|
89
|
+
* assigned_branch: string -- git branch for this story
|
|
90
|
+
* handoff_dir: string -- absolute path to _vibe-chain-output/handoffs/
|
|
91
|
+
* escalation_dir: string -- absolute path to worker-inbox/<agent>/ dir
|
|
92
|
+
* audit_log: string -- absolute path to heimdall-audit.log
|
|
93
|
+
* has_db_migration: boolean
|
|
94
|
+
* has_api_changes: boolean
|
|
95
|
+
* allowed_paths: string[] -- absolute paths the worker may read/write
|
|
96
|
+
* }
|
|
97
|
+
*/
|
|
98
|
+
function writeContextFile(worktreePath, context) {
|
|
99
|
+
const contextPath = path.join(worktreePath, '.context.json')
|
|
100
|
+
fs.writeFileSync(contextPath, JSON.stringify(context, null, 2) + '\n')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* setup(worktreePath, context)
|
|
105
|
+
*
|
|
106
|
+
* Convenience function: writes both hooks and context file in one call.
|
|
107
|
+
*/
|
|
108
|
+
function setup(worktreePath, context) {
|
|
109
|
+
writeHeimdallHooks(worktreePath)
|
|
110
|
+
writeContextFile(worktreePath, context)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { setup, writeHeimdallHooks, writeContextFile, HEIMDALL_PATH }
|