vibe-forge 0.1.0 → 0.3.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/forge.md +147 -0
- package/.claude/settings.local.json +16 -0
- package/LICENSE +21 -21
- package/README.md +230 -211
- package/agents/forge-master/capabilities.md +144 -144
- package/agents/forge-master/context-template.md +128 -128
- package/bin/cli.js +3 -1
- package/bin/forge-daemon.sh +195 -71
- package/bin/forge-setup.sh +43 -12
- package/bin/forge-spawn.sh +46 -46
- package/bin/forge.sh +76 -127
- package/bin/lib/agents.sh +157 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +259 -0
- package/bin/lib/constants.sh +143 -0
- package/config/agents.json +76 -0
- package/config/task-template.md +87 -87
- package/docs/TODO.md +65 -0
- package/docs/npm-publishing.md +95 -0
- package/docs/security.md +144 -0
- package/package.json +11 -2
package/bin/forge-daemon.sh
CHANGED
|
@@ -11,28 +11,110 @@
|
|
|
11
11
|
|
|
12
12
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
13
|
FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Load Shared Libraries
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
# shellcheck source=lib/colors.sh
|
|
20
|
+
source "$SCRIPT_DIR/lib/colors.sh"
|
|
21
|
+
# shellcheck source=lib/constants.sh
|
|
22
|
+
source "$SCRIPT_DIR/lib/constants.sh"
|
|
23
|
+
# shellcheck source=lib/config.sh
|
|
24
|
+
source "$SCRIPT_DIR/lib/config.sh"
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Daemon Configuration
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
14
30
|
CONFIG_FILE="$FORGE_ROOT/.forge/config.json"
|
|
15
31
|
PID_FILE="$FORGE_ROOT/.forge/daemon.pid"
|
|
16
32
|
LOG_FILE="$FORGE_ROOT/.forge/daemon.log"
|
|
17
33
|
NOTIFY_FILE="$FORGE_ROOT/.forge/notifications.log"
|
|
18
34
|
NOTIFIED_FILE="$FORGE_ROOT/.forge/notified-tasks.txt"
|
|
19
|
-
STATE_FILE="$FORGE_ROOT/
|
|
35
|
+
STATE_FILE="$FORGE_ROOT/$CONTEXT_DIR/forge-state.yaml"
|
|
36
|
+
LOCK_FILE="$FORGE_ROOT/.forge/daemon.lock"
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
# Log file rotation settings
|
|
39
|
+
MAX_LOG_SIZE=1048576 # 1MB
|
|
40
|
+
MAX_NOTIFY_ENTRIES=1000
|
|
22
41
|
|
|
23
|
-
# Load terminal type from config
|
|
42
|
+
# Load terminal type from config (safe parsing)
|
|
24
43
|
TERMINAL_TYPE="manual"
|
|
25
44
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
26
|
-
TERMINAL_TYPE=$(
|
|
45
|
+
TERMINAL_TYPE=$(json_get_string "$CONFIG_FILE" "terminal_type") || TERMINAL_TYPE="manual"
|
|
27
46
|
fi
|
|
28
47
|
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Utility Functions
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
# Rotate log file if it gets too large
|
|
53
|
+
rotate_log_if_needed() {
|
|
54
|
+
local log="$1"
|
|
55
|
+
if [[ -f "$log" ]]; then
|
|
56
|
+
local size
|
|
57
|
+
size=$(stat -f%z "$log" 2>/dev/null || stat --format=%s "$log" 2>/dev/null || echo 0)
|
|
58
|
+
if [[ "$size" -gt "$MAX_LOG_SIZE" ]]; then
|
|
59
|
+
mv "$log" "${log}.old"
|
|
60
|
+
touch "$log"
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Trim notification entries to prevent unbounded growth
|
|
66
|
+
trim_notified_file() {
|
|
67
|
+
if [[ -f "$NOTIFIED_FILE" ]]; then
|
|
68
|
+
local count
|
|
69
|
+
count=$(wc -l < "$NOTIFIED_FILE" 2>/dev/null || echo 0)
|
|
70
|
+
if [[ "$count" -gt "$MAX_NOTIFY_ENTRIES" ]]; then
|
|
71
|
+
# Keep last 500 entries
|
|
72
|
+
tail -500 "$NOTIFIED_FILE" > "${NOTIFIED_FILE}.tmp"
|
|
73
|
+
mv "${NOTIFIED_FILE}.tmp" "$NOTIFIED_FILE"
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Safe file move with symlink protection
|
|
79
|
+
safe_move_task() {
|
|
80
|
+
local src="$1"
|
|
81
|
+
local dest_dir="$2"
|
|
82
|
+
|
|
83
|
+
# SECURITY: Skip symlinks to prevent symlink attacks
|
|
84
|
+
if [[ -L "$src" ]]; then
|
|
85
|
+
echo "[$(date -Iseconds)] WARNING: Skipping symlink: $src" >> "$LOG_FILE"
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# SECURITY: Verify source is a regular file
|
|
90
|
+
if [[ ! -f "$src" ]]; then
|
|
91
|
+
return 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# SECURITY: Verify destination is within FORGE_ROOT
|
|
95
|
+
local real_dest
|
|
96
|
+
real_dest=$(cd "$dest_dir" 2>/dev/null && pwd)
|
|
97
|
+
local forge_root_real
|
|
98
|
+
forge_root_real=$(cd "$FORGE_ROOT" 2>/dev/null && pwd)
|
|
99
|
+
|
|
100
|
+
if [[ "$real_dest" != "$forge_root_real"/* ]]; then
|
|
101
|
+
echo "[$(date -Iseconds)] ERROR: Destination outside FORGE_ROOT: $dest_dir" >> "$LOG_FILE"
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
local filename
|
|
106
|
+
filename=$(basename "$src")
|
|
107
|
+
mv "$src" "$dest_dir/$filename"
|
|
108
|
+
}
|
|
109
|
+
|
|
29
110
|
# =============================================================================
|
|
30
111
|
# Notification Functions
|
|
31
112
|
# =============================================================================
|
|
32
113
|
|
|
33
114
|
notify() {
|
|
34
115
|
local message="$1"
|
|
35
|
-
local timestamp
|
|
116
|
+
local timestamp
|
|
117
|
+
timestamp=$(date -Iseconds)
|
|
36
118
|
|
|
37
119
|
# Log to notifications file
|
|
38
120
|
echo "[$timestamp] $message" >> "$NOTIFY_FILE"
|
|
@@ -43,13 +125,7 @@ notify() {
|
|
|
43
125
|
# Terminal bell (works in most terminals)
|
|
44
126
|
printf '\a'
|
|
45
127
|
|
|
46
|
-
# Terminal-specific notifications
|
|
47
|
-
case "$TERMINAL_TYPE" in
|
|
48
|
-
"windows-terminal")
|
|
49
|
-
# Windows Terminal doesn't have a native notify, but bell works
|
|
50
|
-
# Could potentially use PowerShell toast notification in future
|
|
51
|
-
;;
|
|
52
|
-
esac
|
|
128
|
+
# Terminal-specific notifications could be added here
|
|
53
129
|
}
|
|
54
130
|
|
|
55
131
|
check_new_pending_tasks() {
|
|
@@ -57,16 +133,20 @@ check_new_pending_tasks() {
|
|
|
57
133
|
touch "$NOTIFIED_FILE"
|
|
58
134
|
|
|
59
135
|
# Check for new pending tasks
|
|
60
|
-
for task in "$FORGE_ROOT
|
|
61
|
-
if [[ -f "$task" ]]; then
|
|
62
|
-
local filename
|
|
136
|
+
for task in "$FORGE_ROOT/$TASKS_PENDING"/*.md; do
|
|
137
|
+
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
138
|
+
local filename
|
|
139
|
+
filename=$(basename "$task")
|
|
63
140
|
|
|
64
141
|
# Check if we've already notified about this task
|
|
65
|
-
if ! grep -
|
|
66
|
-
# Extract task info from frontmatter
|
|
67
|
-
local task_id
|
|
68
|
-
|
|
69
|
-
|
|
142
|
+
if ! grep -qF "$filename" "$NOTIFIED_FILE" 2>/dev/null; then
|
|
143
|
+
# Extract task info from frontmatter safely
|
|
144
|
+
local task_id task_title assigned_to
|
|
145
|
+
|
|
146
|
+
# Use head to limit read and tr to sanitize
|
|
147
|
+
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 100)
|
|
148
|
+
task_title=$(grep -m1 "^title:" "$task" 2>/dev/null | cut -d':' -f2- | tr -d '"' | head -c 200)
|
|
149
|
+
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
|
|
70
150
|
|
|
71
151
|
# Use filename as fallback
|
|
72
152
|
task_id="${task_id:-$filename}"
|
|
@@ -79,21 +159,23 @@ check_new_pending_tasks() {
|
|
|
79
159
|
notify "New pending task: $task_title ($task_id)"
|
|
80
160
|
fi
|
|
81
161
|
|
|
82
|
-
# Mark as notified
|
|
162
|
+
# Mark as notified (atomic append)
|
|
83
163
|
echo "$filename" >> "$NOTIFIED_FILE"
|
|
84
164
|
fi
|
|
85
165
|
fi
|
|
86
166
|
done
|
|
87
167
|
|
|
88
168
|
# Also check needs-changes for tasks that need rework
|
|
89
|
-
for task in "$FORGE_ROOT
|
|
90
|
-
if [[ -f "$task" ]]; then
|
|
91
|
-
local filename
|
|
169
|
+
for task in "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"/*.md; do
|
|
170
|
+
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
171
|
+
local filename
|
|
172
|
+
filename=$(basename "$task")
|
|
92
173
|
local notified_key="needs-changes:$filename"
|
|
93
174
|
|
|
94
|
-
if ! grep -
|
|
95
|
-
local task_id
|
|
96
|
-
|
|
175
|
+
if ! grep -qF "$notified_key" "$NOTIFIED_FILE" 2>/dev/null; then
|
|
176
|
+
local task_id assigned_to
|
|
177
|
+
task_id=$(grep -m1 "^id:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 100)
|
|
178
|
+
assigned_to=$(grep -m1 "^assigned_to:" "$task" 2>/dev/null | cut -d':' -f2 | tr -d ' "' | head -c 50)
|
|
97
179
|
|
|
98
180
|
task_id="${task_id:-$filename}"
|
|
99
181
|
|
|
@@ -114,20 +196,21 @@ check_new_pending_tasks() {
|
|
|
114
196
|
# =============================================================================
|
|
115
197
|
|
|
116
198
|
update_state() {
|
|
117
|
-
# Count tasks in each folder
|
|
118
|
-
local pending
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
199
|
+
# Count tasks in each folder (using find with -maxdepth for safety)
|
|
200
|
+
local pending in_progress completed review approved needs_changes merged
|
|
201
|
+
pending=$(find "$FORGE_ROOT/$TASKS_PENDING" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
202
|
+
in_progress=$(find "$FORGE_ROOT/$TASKS_IN_PROGRESS" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
203
|
+
completed=$(find "$FORGE_ROOT/$TASKS_COMPLETED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
204
|
+
review=$(find "$FORGE_ROOT/$TASKS_REVIEW" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
205
|
+
approved=$(find "$FORGE_ROOT/$TASKS_APPROVED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
206
|
+
needs_changes=$(find "$FORGE_ROOT/$TASKS_NEEDS_CHANGES" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
207
|
+
merged=$(find "$FORGE_ROOT/$TASKS_MERGED" -maxdepth 1 -name "*.md" -type f 2>/dev/null | wc -l)
|
|
208
|
+
|
|
127
209
|
local blocked=0
|
|
128
210
|
|
|
129
|
-
# Write state file
|
|
130
|
-
|
|
211
|
+
# Write state file atomically (write to temp, then move)
|
|
212
|
+
local temp_state="${STATE_FILE}.tmp.$$"
|
|
213
|
+
cat > "$temp_state" << EOF
|
|
131
214
|
# Vibe Forge State
|
|
132
215
|
# Auto-updated by forge-daemon
|
|
133
216
|
# Last updated: $(date -Iseconds)
|
|
@@ -148,26 +231,29 @@ tasks:
|
|
|
148
231
|
|
|
149
232
|
last_updated: $(date -Iseconds)
|
|
150
233
|
EOF
|
|
234
|
+
mv "$temp_state" "$STATE_FILE"
|
|
151
235
|
}
|
|
152
236
|
|
|
153
237
|
route_completed_to_review() {
|
|
154
238
|
# Move completed tasks to review queue
|
|
155
|
-
for task in "$FORGE_ROOT
|
|
156
|
-
if [[ -f "$task" ]]; then
|
|
157
|
-
local filename
|
|
239
|
+
for task in "$FORGE_ROOT/$TASKS_COMPLETED"/*.md; do
|
|
240
|
+
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
241
|
+
local filename
|
|
242
|
+
filename=$(basename "$task")
|
|
158
243
|
echo "[$(date -Iseconds)] Routing $filename to review" >> "$LOG_FILE"
|
|
159
|
-
|
|
244
|
+
safe_move_task "$task" "$FORGE_ROOT/$TASKS_REVIEW"
|
|
160
245
|
fi
|
|
161
246
|
done
|
|
162
247
|
}
|
|
163
248
|
|
|
164
249
|
route_approved_to_merged() {
|
|
165
250
|
# Move approved tasks to merged archive
|
|
166
|
-
for task in "$FORGE_ROOT
|
|
167
|
-
if [[ -f "$task" ]]; then
|
|
168
|
-
local filename
|
|
251
|
+
for task in "$FORGE_ROOT/$TASKS_APPROVED"/*.md; do
|
|
252
|
+
if [[ -f "$task" && ! -L "$task" ]]; then
|
|
253
|
+
local filename
|
|
254
|
+
filename=$(basename "$task")
|
|
169
255
|
echo "[$(date -Iseconds)] Archiving $filename to merged" >> "$LOG_FILE"
|
|
170
|
-
|
|
256
|
+
safe_move_task "$task" "$FORGE_ROOT/$TASKS_MERGED"
|
|
171
257
|
fi
|
|
172
258
|
done
|
|
173
259
|
}
|
|
@@ -175,7 +261,17 @@ route_approved_to_merged() {
|
|
|
175
261
|
daemon_loop() {
|
|
176
262
|
echo "[$(date -Iseconds)] Forge daemon started (PID: $$)" >> "$LOG_FILE"
|
|
177
263
|
|
|
264
|
+
# Create lock file
|
|
265
|
+
echo $$ > "$LOCK_FILE"
|
|
266
|
+
|
|
267
|
+
# Cleanup on exit
|
|
268
|
+
trap 'rm -f "$LOCK_FILE"; echo "[$(date -Iseconds)] Daemon exiting" >> "$LOG_FILE"' EXIT
|
|
269
|
+
|
|
270
|
+
local iteration=0
|
|
178
271
|
while true; do
|
|
272
|
+
# Increment iteration counter
|
|
273
|
+
((iteration++)) || true
|
|
274
|
+
|
|
179
275
|
# Check for new tasks and notify
|
|
180
276
|
check_new_pending_tasks
|
|
181
277
|
|
|
@@ -186,6 +282,13 @@ daemon_loop() {
|
|
|
186
282
|
# Update state
|
|
187
283
|
update_state
|
|
188
284
|
|
|
285
|
+
# Periodic maintenance (every 100 iterations = ~3 minutes)
|
|
286
|
+
if [[ $((iteration % 100)) -eq 0 ]]; then
|
|
287
|
+
rotate_log_if_needed "$LOG_FILE"
|
|
288
|
+
rotate_log_if_needed "$NOTIFY_FILE"
|
|
289
|
+
trim_notified_file
|
|
290
|
+
fi
|
|
291
|
+
|
|
189
292
|
sleep "$POLL_INTERVAL"
|
|
190
293
|
done
|
|
191
294
|
}
|
|
@@ -197,7 +300,8 @@ daemon_loop() {
|
|
|
197
300
|
cmd_start() {
|
|
198
301
|
# Check if already running
|
|
199
302
|
if [[ -f "$PID_FILE" ]]; then
|
|
200
|
-
local pid
|
|
303
|
+
local pid
|
|
304
|
+
pid=$(cat "$PID_FILE")
|
|
201
305
|
if kill -0 "$pid" 2>/dev/null; then
|
|
202
306
|
echo "Daemon already running (PID: $pid)"
|
|
203
307
|
return 0
|
|
@@ -207,22 +311,36 @@ cmd_start() {
|
|
|
207
311
|
fi
|
|
208
312
|
fi
|
|
209
313
|
|
|
210
|
-
#
|
|
314
|
+
# Check for lock file (another instance check)
|
|
315
|
+
if [[ -f "$LOCK_FILE" ]]; then
|
|
316
|
+
local lock_pid
|
|
317
|
+
lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
|
|
318
|
+
if kill -0 "$lock_pid" 2>/dev/null; then
|
|
319
|
+
echo "Another daemon instance is running (PID: $lock_pid)"
|
|
320
|
+
return 1
|
|
321
|
+
else
|
|
322
|
+
rm -f "$LOCK_FILE"
|
|
323
|
+
fi
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# Create directories if needed (with secure permissions)
|
|
211
327
|
mkdir -p "$FORGE_ROOT/.forge"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
mkdir -p "$FORGE_ROOT
|
|
215
|
-
mkdir -p "$FORGE_ROOT
|
|
216
|
-
mkdir -p "$FORGE_ROOT
|
|
217
|
-
mkdir -p "$FORGE_ROOT
|
|
218
|
-
mkdir -p "$FORGE_ROOT
|
|
328
|
+
chmod 700 "$FORGE_ROOT/.forge"
|
|
329
|
+
|
|
330
|
+
mkdir -p "$FORGE_ROOT/$TASKS_PENDING"
|
|
331
|
+
mkdir -p "$FORGE_ROOT/$TASKS_IN_PROGRESS"
|
|
332
|
+
mkdir -p "$FORGE_ROOT/$TASKS_COMPLETED"
|
|
333
|
+
mkdir -p "$FORGE_ROOT/$TASKS_REVIEW"
|
|
334
|
+
mkdir -p "$FORGE_ROOT/$TASKS_APPROVED"
|
|
335
|
+
mkdir -p "$FORGE_ROOT/$TASKS_NEEDS_CHANGES"
|
|
336
|
+
mkdir -p "$FORGE_ROOT/$TASKS_MERGED"
|
|
219
337
|
|
|
220
338
|
# Start daemon in background
|
|
221
339
|
daemon_loop &
|
|
222
340
|
local pid=$!
|
|
223
341
|
echo "$pid" > "$PID_FILE"
|
|
224
342
|
|
|
225
|
-
|
|
343
|
+
log_success "Forge daemon started (PID: $pid)"
|
|
226
344
|
echo " Log: $LOG_FILE"
|
|
227
345
|
}
|
|
228
346
|
|
|
@@ -232,35 +350,41 @@ cmd_stop() {
|
|
|
232
350
|
return 0
|
|
233
351
|
fi
|
|
234
352
|
|
|
235
|
-
local pid
|
|
353
|
+
local pid
|
|
354
|
+
pid=$(cat "$PID_FILE")
|
|
236
355
|
|
|
237
356
|
if kill -0 "$pid" 2>/dev/null; then
|
|
238
357
|
kill "$pid"
|
|
239
|
-
rm -f "$PID_FILE"
|
|
358
|
+
rm -f "$PID_FILE" "$LOCK_FILE"
|
|
240
359
|
echo "[$(date -Iseconds)] Forge daemon stopped" >> "$LOG_FILE"
|
|
241
|
-
|
|
360
|
+
log_success "Daemon stopped"
|
|
242
361
|
else
|
|
243
|
-
rm -f "$PID_FILE"
|
|
362
|
+
rm -f "$PID_FILE" "$LOCK_FILE"
|
|
244
363
|
echo "Daemon was not running (stale PID file removed)"
|
|
245
364
|
fi
|
|
246
365
|
|
|
247
366
|
# Update state to show inactive
|
|
248
367
|
if [[ -f "$STATE_FILE" ]]; then
|
|
249
|
-
sed -
|
|
368
|
+
# Use sed carefully - different syntax on macOS vs Linux
|
|
369
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
370
|
+
sed -i '' 's/status: active/status: stopped/' "$STATE_FILE" 2>/dev/null || true
|
|
371
|
+
else
|
|
372
|
+
sed -i 's/status: active/status: stopped/' "$STATE_FILE" 2>/dev/null || true
|
|
373
|
+
fi
|
|
250
374
|
fi
|
|
251
375
|
}
|
|
252
376
|
|
|
253
377
|
cmd_status() {
|
|
254
378
|
echo ""
|
|
255
|
-
|
|
256
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
379
|
+
log_header "🔥 Forge Daemon Status"
|
|
257
380
|
|
|
258
381
|
if [[ -f "$PID_FILE" ]]; then
|
|
259
|
-
local pid
|
|
382
|
+
local pid
|
|
383
|
+
pid=$(cat "$PID_FILE")
|
|
260
384
|
if kill -0 "$pid" 2>/dev/null; then
|
|
261
|
-
|
|
385
|
+
log_success "Running (PID: $pid)"
|
|
262
386
|
else
|
|
263
|
-
|
|
387
|
+
log_warn "Stopped (stale PID file)"
|
|
264
388
|
fi
|
|
265
389
|
else
|
|
266
390
|
echo "Status: Stopped"
|
|
@@ -277,7 +401,8 @@ cmd_status() {
|
|
|
277
401
|
|
|
278
402
|
# Show recent notifications
|
|
279
403
|
if [[ -f "$NOTIFY_FILE" ]]; then
|
|
280
|
-
local notify_count
|
|
404
|
+
local notify_count
|
|
405
|
+
notify_count=$(wc -l < "$NOTIFY_FILE" 2>/dev/null || echo "0")
|
|
281
406
|
if [[ "$notify_count" -gt 0 ]]; then
|
|
282
407
|
echo "Recent Notifications (last 5):"
|
|
283
408
|
tail -5 "$NOTIFY_FILE" | sed 's/^/ /'
|
|
@@ -290,8 +415,7 @@ cmd_status() {
|
|
|
290
415
|
|
|
291
416
|
cmd_notifications() {
|
|
292
417
|
echo ""
|
|
293
|
-
|
|
294
|
-
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
418
|
+
log_header "🔔 Forge Notifications"
|
|
295
419
|
|
|
296
420
|
if [[ -f "$NOTIFY_FILE" ]]; then
|
|
297
421
|
local count="${1:-10}"
|
|
@@ -308,7 +432,7 @@ cmd_notifications() {
|
|
|
308
432
|
|
|
309
433
|
cmd_clear_notifications() {
|
|
310
434
|
rm -f "$NOTIFY_FILE" "$NOTIFIED_FILE"
|
|
311
|
-
|
|
435
|
+
log_success "Notifications cleared"
|
|
312
436
|
}
|
|
313
437
|
|
|
314
438
|
# =============================================================================
|
package/bin/forge-setup.sh
CHANGED
|
@@ -11,12 +11,12 @@ set -e
|
|
|
11
11
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
12
|
FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
# =============================================================================
|
|
15
|
+
# Load Shared Libraries
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
# shellcheck source=lib/colors.sh
|
|
19
|
+
source "$SCRIPT_DIR/lib/colors.sh"
|
|
20
20
|
|
|
21
21
|
# Parse arguments
|
|
22
22
|
NON_INTERACTIVE=false
|
|
@@ -305,7 +305,36 @@ configure_daemon() {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
# =============================================================================
|
|
308
|
-
# STEP 8:
|
|
308
|
+
# STEP 8: Install Slash Command
|
|
309
|
+
# =============================================================================
|
|
310
|
+
|
|
311
|
+
install_slash_command() {
|
|
312
|
+
echo ""
|
|
313
|
+
echo "Installing /forge slash command..."
|
|
314
|
+
|
|
315
|
+
# Parent project directory (one level up from _vibe-forge)
|
|
316
|
+
local parent_dir
|
|
317
|
+
parent_dir="$(dirname "$FORGE_ROOT")"
|
|
318
|
+
|
|
319
|
+
local target_dir="$parent_dir/.claude/commands"
|
|
320
|
+
local source_file="$FORGE_ROOT/.claude/commands/forge.md"
|
|
321
|
+
|
|
322
|
+
# Create .claude/commands in parent project if it doesn't exist
|
|
323
|
+
mkdir -p "$target_dir"
|
|
324
|
+
|
|
325
|
+
# Copy the forge command
|
|
326
|
+
if [[ -f "$source_file" ]]; then
|
|
327
|
+
cp "$source_file" "$target_dir/forge.md"
|
|
328
|
+
echo -e "${GREEN}✅ Slash command installed:${NC} $target_dir/forge.md"
|
|
329
|
+
echo " Use /forge in Claude Code to access Vibe Forge"
|
|
330
|
+
else
|
|
331
|
+
echo -e "${YELLOW}⚠️ Could not find forge command template${NC}"
|
|
332
|
+
echo " You may need to manually copy .claude/commands/forge.md"
|
|
333
|
+
fi
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# =============================================================================
|
|
337
|
+
# STEP 9: Create Project Context
|
|
309
338
|
# =============================================================================
|
|
310
339
|
|
|
311
340
|
detect_project_info() {
|
|
@@ -414,7 +443,7 @@ EOF
|
|
|
414
443
|
}
|
|
415
444
|
|
|
416
445
|
# =============================================================================
|
|
417
|
-
# STEP
|
|
446
|
+
# STEP 10: Setup Complete
|
|
418
447
|
# =============================================================================
|
|
419
448
|
|
|
420
449
|
setup_complete() {
|
|
@@ -423,11 +452,12 @@ setup_complete() {
|
|
|
423
452
|
echo -e "${GREEN}🔥 Vibe Forge initialized!${NC}"
|
|
424
453
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
425
454
|
echo ""
|
|
426
|
-
echo "Ready to start!
|
|
455
|
+
echo "Ready to start! Open Claude Code in this project and run:"
|
|
427
456
|
echo ""
|
|
428
|
-
echo -e " ${BLUE}forge${NC} Start the Planning Hub"
|
|
429
|
-
echo -e " ${BLUE}forge status${NC}
|
|
430
|
-
echo -e " ${BLUE}forge
|
|
457
|
+
echo -e " ${BLUE}/forge${NC} Start the Planning Hub"
|
|
458
|
+
echo -e " ${BLUE}/forge status${NC} Show status dashboard"
|
|
459
|
+
echo -e " ${BLUE}/forge spawn anvil${NC} Spawn a worker agent"
|
|
460
|
+
echo -e " ${BLUE}/forge help${NC} See all commands"
|
|
431
461
|
echo ""
|
|
432
462
|
echo "Optional: Edit context/project-context.md to add more details."
|
|
433
463
|
echo ""
|
|
@@ -446,6 +476,7 @@ main() {
|
|
|
446
476
|
if validate_setup; then
|
|
447
477
|
configure_terminal
|
|
448
478
|
configure_daemon
|
|
479
|
+
install_slash_command
|
|
449
480
|
create_project_context
|
|
450
481
|
setup_complete
|
|
451
482
|
else
|
package/bin/forge-spawn.sh
CHANGED
|
@@ -16,33 +16,23 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
16
16
|
FORGE_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
17
17
|
CONFIG_FILE="$FORGE_ROOT/.forge/config.json"
|
|
18
18
|
|
|
19
|
-
# Colors
|
|
20
|
-
RED='\033[0;31m'
|
|
21
|
-
GREEN='\033[0;32m'
|
|
22
|
-
YELLOW='\033[1;33m'
|
|
23
|
-
BLUE='\033[0;34m'
|
|
24
|
-
NC='\033[0m'
|
|
25
|
-
|
|
26
19
|
# =============================================================================
|
|
27
|
-
# Load
|
|
20
|
+
# Load Shared Libraries
|
|
28
21
|
# =============================================================================
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
TERMINAL_TYPE=$(grep -o '"terminal_type": *"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
|
|
39
|
-
GIT_BASH_PATH=$(grep -o '"git_bash_path": *"[^"]*"' "$CONFIG_FILE" | cut -d'"' -f4)
|
|
23
|
+
# shellcheck source=lib/colors.sh
|
|
24
|
+
source "$SCRIPT_DIR/lib/colors.sh"
|
|
25
|
+
# shellcheck source=lib/constants.sh
|
|
26
|
+
source "$SCRIPT_DIR/lib/constants.sh"
|
|
27
|
+
# shellcheck source=lib/config.sh
|
|
28
|
+
source "$SCRIPT_DIR/lib/config.sh"
|
|
29
|
+
# shellcheck source=lib/agents.sh
|
|
30
|
+
source "$SCRIPT_DIR/lib/agents.sh"
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
32
|
+
# Load agent configuration from JSON if available
|
|
33
|
+
if [[ -f "$FORGE_ROOT/$AGENTS_CONFIG" ]]; then
|
|
34
|
+
load_agents_from_json "$FORGE_ROOT/$AGENTS_CONFIG" 2>/dev/null || true
|
|
35
|
+
fi
|
|
46
36
|
|
|
47
37
|
# =============================================================================
|
|
48
38
|
# Spawn Functions
|
|
@@ -51,20 +41,24 @@ load_config() {
|
|
|
51
41
|
spawn_windows_terminal() {
|
|
52
42
|
local agent="$1"
|
|
53
43
|
|
|
54
|
-
|
|
44
|
+
log_info "Spawning $agent in Windows Terminal..."
|
|
55
45
|
|
|
56
46
|
if command -v wt &> /dev/null || command -v wt.exe &> /dev/null; then
|
|
47
|
+
# Get display name for tab title
|
|
48
|
+
local display_name
|
|
49
|
+
display_name=$(get_agent_display_name "$agent")
|
|
50
|
+
|
|
57
51
|
# Windows Terminal: open new tab with forge command
|
|
58
|
-
#
|
|
52
|
+
# SECURITY: $agent has already been validated through resolve_agent
|
|
59
53
|
if [[ -n "$GIT_BASH_PATH" ]]; then
|
|
60
54
|
local bash_path="${GIT_BASH_PATH//\//\\}"
|
|
61
|
-
wt.exe new-tab --title "$
|
|
55
|
+
wt.exe new-tab --title "$display_name" "$bash_path" -c "cd '$FORGE_ROOT' && ./bin/forge.sh start '$agent'"
|
|
62
56
|
else
|
|
63
|
-
wt.exe new-tab --title "$
|
|
57
|
+
wt.exe new-tab --title "$display_name" bash -c "cd '$FORGE_ROOT' && ./bin/forge.sh start '$agent'"
|
|
64
58
|
fi
|
|
65
|
-
|
|
59
|
+
log_success "$display_name spawned in new Windows Terminal tab"
|
|
66
60
|
else
|
|
67
|
-
|
|
61
|
+
log_error "wt command not found."
|
|
68
62
|
echo "Make sure Windows Terminal is installed."
|
|
69
63
|
echo ""
|
|
70
64
|
echo "Manual fallback:"
|
|
@@ -75,9 +69,11 @@ spawn_windows_terminal() {
|
|
|
75
69
|
|
|
76
70
|
print_manual_instructions() {
|
|
77
71
|
local agent="$1"
|
|
72
|
+
local display_name
|
|
73
|
+
display_name=$(get_agent_display_name "$agent")
|
|
78
74
|
|
|
79
75
|
echo ""
|
|
80
|
-
|
|
76
|
+
log_info "To start $display_name manually:"
|
|
81
77
|
echo ""
|
|
82
78
|
echo " 1. Open a new terminal window/tab"
|
|
83
79
|
echo " 2. Navigate to: $FORGE_ROOT"
|
|
@@ -85,7 +81,7 @@ print_manual_instructions() {
|
|
|
85
81
|
echo ""
|
|
86
82
|
echo "Or copy this command:"
|
|
87
83
|
echo ""
|
|
88
|
-
echo " cd '$FORGE_ROOT' && ./bin/forge.sh start $agent"
|
|
84
|
+
echo " cd '$FORGE_ROOT' && ./bin/forge.sh start '$agent'"
|
|
89
85
|
echo ""
|
|
90
86
|
}
|
|
91
87
|
|
|
@@ -97,34 +93,38 @@ main() {
|
|
|
97
93
|
local agent="${1:-}"
|
|
98
94
|
|
|
99
95
|
if [[ -z "$agent" ]]; then
|
|
100
|
-
|
|
96
|
+
log_error "No agent specified."
|
|
101
97
|
echo ""
|
|
102
98
|
echo "Usage: forge-spawn.sh <agent-name>"
|
|
103
99
|
echo ""
|
|
104
|
-
|
|
105
|
-
echo " anvil - Frontend Developer"
|
|
106
|
-
echo " furnace - Backend Developer"
|
|
107
|
-
echo " crucible - Tester / QA"
|
|
108
|
-
echo " sentinel - Code Reviewer"
|
|
109
|
-
echo " scribe - Documentation"
|
|
110
|
-
echo " herald - Release Manager"
|
|
111
|
-
echo " ember - DevOps"
|
|
112
|
-
echo " aegis - Security"
|
|
100
|
+
show_available_agents
|
|
113
101
|
exit 1
|
|
114
102
|
fi
|
|
115
103
|
|
|
116
|
-
|
|
104
|
+
# Resolve alias to canonical agent name (SECURITY: whitelist validation)
|
|
105
|
+
local resolved
|
|
106
|
+
resolved=$(resolve_agent "$agent") || {
|
|
107
|
+
log_error "Unknown agent: $agent"
|
|
108
|
+
echo ""
|
|
109
|
+
show_available_agents
|
|
110
|
+
exit 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Load config
|
|
114
|
+
require_forge_config "$FORGE_ROOT"
|
|
117
115
|
|
|
118
116
|
echo ""
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
log_header "🔥 Forge Spawn: $(get_agent_display_name "$resolved")"
|
|
118
|
+
if [[ "$agent" != "$resolved" ]]; then
|
|
119
|
+
echo " (resolved from '$agent')"
|
|
120
|
+
fi
|
|
121
121
|
|
|
122
122
|
case "$TERMINAL_TYPE" in
|
|
123
123
|
"windows-terminal")
|
|
124
|
-
spawn_windows_terminal "$
|
|
124
|
+
spawn_windows_terminal "$resolved"
|
|
125
125
|
;;
|
|
126
126
|
*)
|
|
127
|
-
print_manual_instructions "$
|
|
127
|
+
print_manual_instructions "$resolved"
|
|
128
128
|
;;
|
|
129
129
|
esac
|
|
130
130
|
}
|